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

embedded/2024/9/29 23:46:03/

在开发中,有时候不停机动态更新代码热部署是一项至关重要的功能,它可以在请求不中断的情况下下更新代码。这种方式不仅提高了开发效率,还能加速测试和调试过程。本文将详细介绍如何在 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,还需要结合操作系统特性,以确保在生产环境中的稳定性和可用性。


http://www.ppmy.cn/embedded/119636.html

相关文章

深度学习——D2(数据操作)

N维数组 创建数组 访问元素 一列: [ : , 1 ] 反向累积、正向累积(自动求导) 梯度 梯度(Gradient)是微积分中的一个重要概念,主要用于描述一个函数在某个区域内的变化情况。以下是对梯度的详细解释: 一…

基于微信小程序的商品展示+ssm(lw+演示+源码+运行)

商品展示系统 摘 要 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,微信小程序被用户普遍使用,为方…

小米笔记本电脑笔记

键盘灯不亮 出现方法:扣上电脑后等风扇关闭,再打开,键盘灯就打不开了。 解决方法:1.重启可以打开;2.把外接屏幕拔掉,键盘灯亮起,再接上外接屏幕。 多显示器自动改成屏幕1显示 我用外接屏幕&…

2024年一区极光优化+分解+深度学习!VMD-PLO-Transformer-GRU多变量时间序列光伏功率预测

2024年一区极光优化分解深度学习!VMD-PLO-Transformer-GRU多变量时间序列光伏功率预测 目录 2024年一区极光优化分解深度学习!VMD-PLO-Transformer-GRU多变量时间序列光伏功率预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.中秋献礼&#…

无法定位程序输入点 qt_version_tag_6_7 于动态链接库

解决问题: 1.使用该项目的qt版本下的windeployqt.exe打包你的exe生成需要的库; 原因: qt的dll库版本和生成exe时的版本不对应,需要重新打包对应版本的库。

深入探讨AI 神经网络:类型、特点与创新应用

一、引言 1.1 背景 随着科技的飞速发展,人工智能已经成为当今社会的热门领域。在人工智能的发展过程中,神经网络扮演着至关重要的角色。神经网络是一种模拟人类大脑神经元结构的计算模型,它通过大量的神经元相互连接并进行信息处理,从而实现对数据的分析和预测。不同类型…

Windows错误报告服务本地权限提升漏洞(CVE-2023-36874)

文章简要描述 针对Windows错误报告服务本地权限提升漏洞(CVE-2023-36874)进行漏洞复现,包括复现过程中的环境搭建、如何实现poc构建、以及改漏洞在实际场景下的检测建议、漏洞利用的主要行为分析以及相关的规则提取等内容。 漏洞背景 背景知识 Windows错误报告服务(Windo…

Go基础学习05-数组和切片关系深度解析

切片和数组的联系 数组(array)和切片(slice)都属于集合类的类型,它们的值也都可以用来存储某一种类型的值(或者说元素)。数组和切片最重要的不同在于: 数组类型的值的长度是固定的…