开发者

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

目录
  • 1. 代码概述
    • 完整代码
    • 工具类
  • 2. 主要功能
    • 3. 代码实现
      • 4. 配置优雅关闭
        • 5. 小结

          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的nujyUykhill数据
                  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);
          php        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 端口的进程
                      Stri编程ng 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;
                  }
              }
          }

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

          测试效果

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

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

          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.infowww.devze.com("停止旧服务器.");
                      webServer.stop();
                  }
                  //((TomcatServletWebServerFactory) servletContextInitializer).setPort(DEFAULT_PORT_8080);
                  ((TomcatServletWebServerFactory) webServerFactory).setPort(DEFAULT_PORT_8080);
                  webServer = webServerFactory.getWebServer(servletContextInitializer);
                  webServer.start();
                  log.info("新服务启动成功.");
              }

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

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

          4. 配置优雅关闭

          application.yml 中设置优雅关闭:

          server:
            shutdown: graceful

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

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

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

          以上就是SpringBoot项目请求不中断动态更新代码的实现的详细内容,更多关于SpringBoot不中断更新代码的资料请关注编程客栈(www.devze.com)其它相关文章!

          0

          上一篇:

          下一篇:

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          最新开发

          开发排行榜