当前位置: 首页 > article >正文

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

在开发中,有时候不停机动态更新代码热部署是一项至关重要的功能,它可以在请求不中断的情况下下更新代码。这种方式不仅提高了开发效率,还能加速测试和调试过程。本文将详细介绍如何在 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.kler.cn/news/326321.html

相关文章:

  • 宝塔部署vue项目出现的各种问题
  • PostgreSQL的扩展(extensions)-常用的扩展-pgstattuple
  • Pygame中Sprite实现逃亡游戏5
  • 如何使用ssm实现基于SpringMVC网上选课系统的设计与实现
  • 基于Springboot+Vue的网上书店(含源码数据库)
  • C++-list使用学习
  • 前端工程化之vite
  • 从零开发操作系统
  • 【Unity基础】Unity中跨平台使用SQLite
  • php-cgi漏洞利用
  • php 平滑重启 kill -SIGUSR2 <PID> pgrep命令查看进程号
  • SpinalHDL之结构(八)
  • 记一次pycharm在使用git提交时需要输入ssh key的密码的问题
  • 第十一届蓝桥杯嵌入式省赛程序设计题解析(基于HAL库)(大学组)
  • HTTPS协议详解:从原理到流程,全面解析安全传输的奥秘
  • 资产管理之源代码防泄密的挑战
  • Redis 简单的消息队列
  • 并发、并行和异步设计
  • Linux 信号保存
  • 【菜菜的sklearn机器学习】(4)随机森林
  • 六,MyBatis-Plus 扩展功能(逻辑删除,通用枚举,字段类型处理,自动填充功能,防全表更新与删除插件,MybatisX快速开发插件)
  • 探索基于知识图谱和 ChatGPT 结合制造服务推荐前沿
  • 【Android 14源码分析】WMS-窗口显示-流程概览与应用端流程分析
  • C语言中的日志机制:打造全面强大的日志系统
  • 翻译:Recent Event Camera Innovations: A Survey
  • 30秒内交易股票,程序化交易的定义与特点
  • 【Windows】自定义显示器的分辨率
  • @Transactional的实现原理
  • openKylin--安装 .net6.0
  • 【linux】gdb