SpringBoot项目请求不中断动态更新代码

news2024/12/24 3:13:24

在开发中,有时候不停机动态更新代码热部署是一项至关重要的功能,它可以在请求不中断的情况下下更新代码。这种方式不仅提高了开发效率,还能加速测试和调试过程。本文将详细介绍如何在 Spring Boot 项目在Linux系统中实现热部署,特别关注优雅关闭功能的实现。

1. 代码概述

我们实现了一个简单的 Spring Boot 应用程序,它可以自动检测端口是否被占用,并在必要时切换到备用端口,然后再将目标端口程序关闭再将备用端口切换为目标端口。具体功能包括:

  • 检查默认端口(8080)是否被占用。
  • 如果被占用,自动切换到备用端口(8086)。
  • 在 Linux 系统下,优雅地关闭占用该端口的进程。
  • 修改Tomcat端口并重启容器。

完整代码

import com.lps.utils.PortUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

/**
 * @author 阿水
 */
@SpringBootApplication
@Slf4j
public class MybatisDemoApplication {

    private static final int DEFAULT_PORT_8080 = 8080;
    private static final int ALTERNATE_PORT_8086 = 8086;

    public static void main(String[] args) {
        boolean isNeedChangePort = PortUtil.isPortInUse(DEFAULT_PORT_8080);
        String[] newArgs = Arrays.copyOf(args, args.length + 1);
        if (isNeedChangePort) {
            log.info("端口 {} 正在使用中, 正在尝试端口切换到 {}.", DEFAULT_PORT_8080, ALTERNATE_PORT_8086);
            newArgs[newArgs.length - 1] = "--server.port=" + ALTERNATE_PORT_8086;
        }
        log.info("启动参数: {}", Arrays.toString(newArgs));
        //去除newArgs的null数据
        newArgs = Arrays.stream(newArgs).filter(Objects::nonNull).toArray(String[]::new);

        ConfigurableApplicationContext context = SpringApplication.run(MybatisDemoApplication.class, newArgs);
        //判断是否是linux系统,如果是linux系统,则尝试杀死占用8080端口的进程
        System.out.println("是否需要修改端口: "+isNeedChangePort);
        if (isNeedChangePort && isLinuxOS()) {
            changePortAndRestart(context);
        }
    }

    /**
     * 如果端口占用,则尝试杀死占用8080端口的进程,并修改端口并重启服务
     *
     * @param context
     */
    private static void changePortAndRestart(ConfigurableApplicationContext context) {
        log.info("尝试杀死占用 8080 端口的进程.");
        killOldServiceInLinux();
        log.info("正在修改端口更改为 {}.", DEFAULT_PORT_8080);
        ServletWebServerFactory webServerFactory = context.getBean(ServletWebServerFactory.class);
        ServletContextInitializer servletContextInitializer = context.getBean(ServletContextInitializer.class);
        WebServer webServer = webServerFactory.getWebServer(servletContextInitializer);

        if (webServer != null) {
            log.info("停止旧服务器.");
            webServer.stop();
        }
        //((TomcatServletWebServerFactory) servletContextInitializer).setPort(DEFAULT_PORT_8080);
        ((TomcatServletWebServerFactory) webServerFactory).setPort(DEFAULT_PORT_8080);
        webServer = webServerFactory.getWebServer(servletContextInitializer);
        webServer.start();
        log.info("新服务启动成功.");
    }

    /**
     * 杀死占用 8080 端口的进程
     */
    private static void killOldServiceInLinux() {
        try {
            // 查找占用 8080 端口的进程
            String command = "lsof -t -i:" + DEFAULT_PORT_8080;
            log.info("正在执行命令: {}", command);
            Process process = Runtime.getRuntime().exec(command);
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String pid;
            while ((pid = reader.readLine()) != null) {
                // 发送 SIGINT 信号以优雅关闭
                Runtime.getRuntime().exec("kill -2 " + pid);
                log.info("Killed process: {}", pid);
            }
        } catch (IOException e) {
            log.error("Failed to stop old service", e);
        }
    }

    /**
     * 判断是否是linux系统
     *
     * @return
     */
    private static boolean isLinuxOS() {
        return System.getProperty("os.name").toLowerCase().contains("linux");
    }
}

工具类

import java.io.IOException;
import java.net.ServerSocket;

/**
 * @author 阿水
 */
public class PortUtil {
    public static boolean isPortInUse(int port) {
        try (ServerSocket ignored = new ServerSocket(port)) {
            // 端口未被占用
            return false;
        } catch (IOException e) {
            // 端口已被占用
            return true;
        }
    }
}

测试效果

2. 主要功能

检测端口状态

通过 PortUtil.isPortInUse() 检查默认端口的使用状态。如果端口被占用,修改启动参数。

import java.io.IOException;
import java.net.ServerSocket;

/**
 * @author 阿水
 */
public class PortUtil {
    public static boolean isPortInUse(int port) {
        try (ServerSocket ignored = new ServerSocket(port)) {
            // 端口未被占用
            return false;
        } catch (IOException e) {
            // 端口已被占用
            return true;
        }
    }
}

修改启动参数

当发现端口被占用时,我们动态调整启动参数,以便在启动时使用新的端口。

     if (isNeedChangePort) {
            log.info("端口 {} 正在使用中, 正在尝试端口切换到 {}.", DEFAULT_PORT_8080, ALTERNATE_PORT_8086);
            newArgs[newArgs.length - 1] = "--server.port=" + ALTERNATE_PORT_8086;
        }

优雅关闭

在 Linux 系统中,如果检测到端口被占用,调用 killOldServiceInLinux() 方法,优雅地关闭占用该端口的进程。这是通过发送 SIGINT 信号实现的,允许应用程序进行清理工作并优雅退出。

 
 /**
     * 杀死占用 8080 端口的进程
     */
    private static void killOldServiceInLinux() {
        try {
            // 查找占用 8080 端口的进程
            String command = "lsof -t -i:" + DEFAULT_PORT_8080;
            log.info("正在执行命令: {}", command);
            Process process = Runtime.getRuntime().exec(command);
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String pid;
            while ((pid = reader.readLine()) != null) {
                // 发送 SIGINT 信号以优雅关闭
                Runtime.getRuntime().exec("kill -2 " + pid);
                log.info("Killed process: {}", pid);
            }
        } catch (IOException e) {
            log.error("Failed to stop old service", e);
        }
    }

3. 代码实现

代码的核心逻辑在 changePortAndRestart() 方法中实现,主要步骤包括停止当前 Web 服务器并重启。

    /**
     * 如果端口占用,则尝试杀死占用8080端口的进程,并修改端口并重启服务
     *
     * @param context
     */
    private static void changePortAndRestart(ConfigurableApplicationContext context) {
        log.info("尝试杀死占用 8080 端口的进程.");
        killOldServiceInLinux();
        log.info("正在修改端口更改为 {}.", DEFAULT_PORT_8080);
        ServletWebServerFactory webServerFactory = context.getBean(ServletWebServerFactory.class);
        ServletContextInitializer servletContextInitializer = context.getBean(ServletContextInitializer.class);
        WebServer webServer = webServerFactory.getWebServer(servletContextInitializer);

        if (webServer != null) {
            log.info("停止旧服务器.");
            webServer.stop();
        }
        //((TomcatServletWebServerFactory) servletContextInitializer).setPort(DEFAULT_PORT_8080);
        ((TomcatServletWebServerFactory) webServerFactory).setPort(DEFAULT_PORT_8080);
        webServer = webServerFactory.getWebServer(servletContextInitializer);
        webServer.start();
        log.info("新服务启动成功.");
    }

4. 配置优雅关闭

application.yml 中设置优雅关闭:

server:
  shutdown: graceful

这个配置允许 Spring Boot 在接收到关闭请求时,等待当前请求完成后再停止服务。 (因此代码使用的是kill -2命令)

5. 小结

通过以上实现,我们能够灵活应对端口占用问题,并提升开发效率。热部署功能不仅依赖于 Spring Boot 提供的丰富 API,还需要结合操作系统特性,以确保在生产环境中的稳定性和可用性。

附带Window关闭端口程序代码

(Window关闭程序后可能得需要sleep一下,不然还会显示端口占用)

  private static void killOldServiceInWindows() {
            try {
                // 查找占用 8080 端口的进程 ID
                ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/c", "netstat -ano | findstr :8080");
                Process process = builder.start();
                BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                    String[] parts = line.trim().split("\\s+");
                    if (parts.length > 4) {
                        String pid = parts[parts.length - 1];
                        // 杀死该进程
                        Runtime.getRuntime().exec("taskkill /F /PID " + pid);
                        log.info("Killed process: {}", pid);
                    }
                }
            } catch (IOException e) {
                log.error("Failed to stop old service", e);
            }
        }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2178224.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GPT与大模型行业落地实践探索

简介 本课程探讨GPT和大模型技术在行业中的实际应用和发展。课程将涵盖GPT的基础知识、原理、及其在行业中的应用案例,如财报分析和客服机器人。重点在于结合实际案例中的使用效果,讲解如何利用GPT的API开发企业级应用以及利用更高级的功能构造AI Agent。…

根据给定的相机和镜头参数,估算相机的内参。

1. 相机分辨率和传感器尺寸 最高分辨率:6000 4000 像素传感器尺寸:22.3 mm 14.9 mm 2. 计算像素大小 需要计算每个像素对应的实际尺寸(mm/pixel): 水平方向像素大小: 垂直方向像素大小: …

TypeScript 基本使用指南【前端 26】

TypeScript 基本使用指南 引言 TypeScript 是 JavaScript 的一个超集,它添加了类型系统和一些其他特性,使得开发大型应用时更加高效和可靠。TypeScript 代码最终会被编译成普通的 JavaScript 代码,这意味着你可以在任何支持 JavaScript 的环…

作家依靠AI一年内创作120部作品

近期,Tim Boucher因声称自己依托人工智能(AI)完成了逾120部作品而在社交网络上引起广泛关注。 Boucher的这种创作手法引发了众多讨论和争议。一些批评者对他依靠AI写作表示不满,认为这种做法缺乏诚实性,甚至涉嫌抄袭。…

区间预测 | Matlab实现ARIMA-KDE的时间序列结合核密度估计区间预测

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现ARIMA-KDE的时间序列结合核密度估计区间预测,ARIMA的核密度估计下置信区间预测。 2.含点预测图、置信区间预测图、核密度估计图,区间预测(区间覆盖率PICP、区间平均宽度百分比PIN…

Mac电脑快速回复的神器-快捷短语

我在使用Mac的时候,很多常用的句子、词语或者一些代码都需要手动输入,拷贝粘贴总是会被新内容覆盖,在需要高频输入的时候这样效率太低了,然后我就找到一个可以快速输入的神器——快捷短语 快捷短语是Mac上的一款非常强大的快速回…

Java_TestNg

TestNg 前言支持特性 使用步骤1.引入库 常用注解Test注解BeforeSuite AfterSuiteAfterClass BeforeClassAfterTest BeforeTestAfterGroups BeforeGroupsBeforeMethod AfterMethodDataProviderFactoryListenersPatameters断言相等 不相等true/falsenull / !nullequals / !equals…

CUDA 参考文章

CUDA:NVCC编译过程和兼容性详解_nvcc把cuda代码转换成什么-CSDN博客https://blog.csdn.net/fb_help/article/details/80462853 1、CUDA:NVCC编译过程和兼容性详解 CUDA:NVCC编译过程和兼容性详解 https://codeyarns.com/2014/03/03/how-to-sp…

Appinventor2 多屏幕之间如何共享过程?

先说结论:不能共享,但可以变通,这个问题上没有完美方案! Appinventor2 多屏幕之间如何共享过程?或者说如何跨屏幕调用其他屏幕的过程? 相信有很多人有过这样的问题,但是目前来看每个屏幕都是独…

自动化测试常见的面试题(超详细整理)

“ 今天我给大家介绍一些python自动化测试中常见的面试题,涵盖了Python基础、测试框架、测试工具、测试方法等方面的内容,希望能够帮助你提升自己的水平和信心。” 项目相关 1.什么项目适合做自动化测试? 答:一般来说&#xff…

物联网行业中心跳机制的介绍以及如何实现

01 概述 心跳机制出现在TCP长连接中,客户端和服务端之间定时发送一种特殊的数据包通知对方还在线,以确保TCP连接地可靠性,有可能TCP连接由于某些原因(例如网线被拔了,突然断电)导致客户端断了&#xff0c…

DC00021基于springboot问卷调查管理系统web项目调查问卷管理系统MySQL(附源码)

1、项目功能演示 DC00021基于springboot问卷调查管理系统web项目调查问卷管理系统MySQL 2、项目功能描述 基于springboot问卷调查管理系统包括以下功能: 1、系统登录、系统注册 2、创建题目、题目信息查看 3、创建问卷、我的问卷信息查看 4、创建活动、我的活动信息…

个人常用AI工具集合

人工智能AI发展到今天,个人也研究了一段时间, 这里把自己常见的 AI软件整理在这,方便需要者。 一、AI写作: 1、国外的claude3.5_sonnet , 官方地址:https://www.anthropic.com/ ,需要魔法访问…

MySQL | excel数据输出insert语句

需求 在日常生产运维过程中,有很多需要进行人工梳理的excel数据,到了研发这一侧需要转为sql语句进行数据修正,如何输出insert插入语句? 方案 在空白列插入,选择需要的列 "INSERT INTO tab_name1 (name, desc) …

怎么查看网站是否被谷歌收录,查看网站是否被搜索引擎收录5个方法与步骤

要查看网站是否被谷歌(Google)或其他搜索引擎收录,是网站管理和SEO(搜索引擎优化)中的一个重要环节。以下是查看网站是否被搜索引擎收录5个方法与步骤,帮助您确认网站是否被搜索引擎成功索引: …

MySql的慢查询(慢日志)

1.什么是慢查询? 慢查询日志,就是查询花费大量时间的日志,是指mysql记录所有执行超过long_query_time参数设定的时间阈值的SQL语句的日志,以帮助开发者分析和优化数据库查询性能。默认情况下,慢查询日志是关闭的&#…

推荐4款2024年热门的PDF转ppt工具

有时候,我们为了方便,需要将PDF里面的内容直接转换的PPT的格式,既方便自己演示和讲解,也让我们可以更加灵活的进行文件的编辑和修改。如果大家不知道要如何进行操作的话,我可以为大家推荐几个比窘方便实用的PDF转换工具…

报错Invalid HADOOP_HDFS_HOME

使用env命令查看已有环境变量 果然多了一个变量,因为不需要,所以删除,再次使用env命令查看,无此变量 再输入hadoop,显示正确

并发面试合集

1.创建线程的方式 区分线程和线程体的概念,线程体通俗点说就是任务。创建线程体的方式:像实现Runnable、Callable接口、继承Thread类、创建线程池等等,这些方式并没有真正创建出线程,严格来说,Java就只有一种方式可以…

Arthas watch (方法执行数据观测)

文章目录 二、命令列表2.3 monitor/watch/trace/stack/tt 相关2.3.5 watch (方法执行数据观测)举例1:监控方法举例2:同时观察函数调用前和函数返回后 本人其他相关文章链接 二、命令列表 2.3 monitor/watch/trace/stack/tt 相关 2.3.5 watc…