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

news/2024/10/3 19:29:36 标签: linux, java, spring boot, spring, 后端

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

1. 代码概述

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

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

完整代码

java">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");
    }
}

工具类

java">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() 检查默认端口的使用状态。如果端口被占用,修改启动参数。

java">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;
        }
    }
}

修改启动参数

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

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

优雅关闭

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

 
java"> /**
     * 杀死占用 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 服务器并重启。

java">    /**
     * 如果端口占用,则尝试杀死占用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一下,不然还会显示端口占用)

java">  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.niftyadmin.cn/n/5688874.html

相关文章

【DRF】DRF基本使用

系列文章目录 第一章 DRF基本使用 文章目录 系列文章目录一、DRF介绍:二、安装:三、基本使用:三、添加测试数据:四、编写Serializers:五、编写视图:六、编写路由: 一、DRF介绍: DR…

OSINT技术情报精选·2024年9月第4周

OSINT技术情报精选2024年9月第4周 2024.10.1版权声明:本文为博主chszs的原创文章,未经博主允许不得转载。 1、大模型行业可信应用框架研究报告 在2024年9月5日举行的Inclusion外滩大会“大模型的创造力边界与应用想象力”分论坛上,蚂蚁集团…

基于深度学习的任务序列中的快速适应

基于深度学习的任务序列中的快速适应是指模型在接连处理不同任务时,能够迅速调整和优化自身以适应新任务的能力。这种能力在动态环境和多任务学习中尤为重要,旨在减少训练时间和资源需求。以下是这一主题的关键要素: 1. 快速适应的背景 动态…

C# 表达式与运算符

本课要点: 1、表达式的基本概念 2、常用的几种运算符 3、运算符的优先级 4、常见问题 一 表达式 表达式是由运算符和操作数组成的。、-、*和/等都是运算符,操作数包括文本、常量、变量和表达式等。 二 算术运算符 2.1 算术运算符的使用 三 常见错误 …

基于CNN+Transformer混合模型实现交通流量时序预测(PyTorch版)

前言 系列专栏:【深度学习:算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域,讨论了各种复杂的深度神经网络思想,如卷积神经网络、循环神经网络、生成对…

微信小程序中的 `<block>` 元素:高效渲染与结构清晰的利器

微信小程序中的 <block> 元素&#xff1a;高效渲染与结构清晰的利器 在微信小程序的开发中&#xff0c;<block> 元素扮演着举足轻重的角色。尽管它不会在页面中渲染任何可见的节点&#xff0c;但作为一个逻辑上的容器&#xff0c;<block> 在条件渲染和循环渲…

系统架构设计师教程 第11章 11.2 人工智能技术概述 笔记

11.2 人工智能技术概述 ★★★☆☆ 11.2.1 人工智能的概念 人工智能 (Artificial Intelligence,AI) 是利用数字计算机或者数字计算机控制的机器模拟、延伸和扩展人的智能&#xff0c;感知环境、获取知识并使用知识获得最佳结果的理论、方法、技术及应用系统。 根据人工智能是…

树--二叉树

1.树的概念和结构 1.1树的概念 树是一种非线性的数据结构&#xff0c;它是由n(n>0)个有限节点组成的一个具有层次关系的集合&#xff0c;它的外形像像一棵倒挂的树&#xff0c;根向上&#xff0c;叶子是向下的&#xff0c;所以我们称之为树。 树有一个特殊节点&#xff0…