SpringBoot实现动态端口切换黑魔法
作者:小小工匠
关键技术点
利用 Spring Boot 内嵌 Servlet 容器 和 动态端口切换 的方式实现平滑更新的方案,关键技术点如下:
Servlet 容器重新绑定端口:Spring Boot 使用 ServletWebServerFactory 动态设置新端口。
零停机切换:通过先启动备用服务、释放主端口,再切换新服务到主端口,实现服务的无缝切换。
端口检测和进程终止:使用 ServerSocket 和系统命令来检测和操作端口。
这种设计允许服务在不完全停止的情况下切换到更新的版本,从而极大地缩短了不可用时间,实现了接近于零停机的效果。
核心原理
1.内嵌 Tomcat 容器动态启动:
使用 TomcatServletWebServerFactory 实现容器的动态创建和启动。
动态绑定 DispatcherServlet 通过 ServletContextInitializer 集合完成 Servlet 注册。
2.端口检查和动态切换:
通过 ServerSocket 判断端口是否占用。
如果占用,则先用备用端口启动新服务,再通过关闭老服务释放主端口,最后切换新服务到主端口。
3.运行时自动处理:
利用 Runtime.exec 执行系统命令,释放端口并终止旧进程。
在极短时间内完成新旧服务切换,避免长时间的停机。
Code
package com.artisan; 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.ServletContextInitializerBeans; import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ConfigurableApplicationContext; import java.io.IOException; import java.net.ServerSocket; import java.util.Collections; @SpringBootApplication() public class BootMainApplication { public static void main(String[] args) { // 默认端口设置 int defaultPort = 8080; // 备选端口设置 int alternativePort = 9090; // 检查默认端口是否已被占用 boolean isPortOccupied = isPortInUse(defaultPort); // 动态端口分配 int portToUse = isPortOccupied ? alternativePort : defaultPort; // 创建Spring Boot应用实例 SpringApplication app = new SpringApplication(WebMainApplication2.class); // 设置端口配置 app.setDefaultProperties(Collections.singletonMap("server.port", portToUse)); // 运行应用并获取上下文 ConfigurableApplicationContext context = app.run(args); // 如果默认端口被占用,则尝试切换回默认端口 if (isPortOccupied) { switchToDefaultPort(context, defaultPort, portToUse); } } /** * 切换到默认端口 * * 当默认端口被其他进程占用时,此方法尝试释放该端口,并启动一个新的Web服务器实例绑定到默认端口 * 同时,它会停止当前的Web服务器实例 * * @param context 当前应用上下文,用于访问Web服务器工厂和停止当前Web服务器 * @param defaultPort 默认端口号,希望切换到的目标端口 * @param currentPort 当前Web服务器正在使用的端口号 */ private static void switchToDefaultPort(ConfigurableApplicationContext context, int defaultPort, int currentPort) { try { // 释放默认端口 terminateProcessUsingPort(defaultPort); // 等待端口释放 while (isPortInUse(defaultPort)) { Thread.sleep(100); } // 启动新容器绑定默认端口 ServletWebServerFactory webServerFactory = getWebServerFactory(context); ((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort); WebServer newServer = webServerFactory.getWebServer(getServletContextInitializers(context)); newServer.start(); // 停止当前容器 ((ServletWebServerApplicationContext) context).getWebServer().stop(); } catch (Exception e) { e.printStackTrace(); } } /** * 检查指定的端口是否正在使用 * * @param port 要检查的端口号 * @return 如果端口正在使用,则返回true;否则返回false */ private static boolean isPortInUse(int port) { try (ServerSocket serverSocket = new ServerSocket(port)) { // 如果能够成功创建ServerSocket实例,说明端口可用,返回false return false; } catch (IOException e) { // 如果创建ServerSocket实例时抛出IOException,说明端口已被占用,返回true return true; } } /** * 终止使用指定端口的进程 * * @param port 需要释放的端口号 * @throws IOException 如果执行命令发生错误 * @throws InterruptedException 如果线程被中断 */ private static void terminateProcessUsingPort(int port) throws IOException, InterruptedException { // 构建终止使用指定端口的进程的命令 String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", port); // 执行命令并等待命令执行完成 Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor(); } /** * 获取ServletContextInitializer实例 * 该方法用于将Spring应用上下文中的所有ServletContextInitializerBeans实例 * 转换为ServletContextInitializer接口的实现,以便在应用启动时初始化ServletContext * * @param context Spring的应用上下文,用于获取BeanFactory * @return 返回一个实现了ServletContextInitializer接口的实例 */ private static ServletContextInitializer getServletContextInitializers(ConfigurableApplicationContext context) { // 使用ApplicationContext中的BeanFactory创建ServletContextInitializerBeans实例 // 这里将ServletContextInitializerBeans作为ServletContextInitializer的实现类返回 // ServletContextInitializerBeans将会负责收集应用上下文中所有ServletContextInitializer的实现 // 并在应用启动时依次调用它们的onStartup方法来初始化ServletContext return (ServletContextInitializer) new ServletContextInitializerBeans(context.getBeanFactory()); } /** * 获取Servlet Web服务器工厂 * * @param context 可配置的应用上下文,用于获取Bean工厂 * @return ServletWebServerFactory实例,用于配置和创建Web服务器 */ private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) { // 从应用上下文中获取Bean工厂,并从中获取ServletWebServerFactory实例 return context.getBeanFactory().getBean(ServletWebServerFactory.class); } }
测试
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController() @RequestMapping("port/") public class TestPortController { @GetMapping("test") public String test() { return "artisan-old"; } }
启动后,访问 http://localhost:8080/port/test
修改TestPortController 的返回值, 打个jar包, 启动新的jar包,
重新访问 http://localhost:8080/port/test ,观察返回结果是否是修改后的返回值
到此这篇关于SpringBoot实现动态端口切换黑魔法的文章就介绍到这了,更多相关SpringBoot动态端口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!