SpringBoot - 动态端口切换黑魔法

news2025/2/27 11:30:35

文章目录

  • 关键技术点
  • 核心原理
  • Code

在这里插入图片描述


关键技术点

利用 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 ,观察返回结果是否是修改后的返回值


参考:https://mp.weixin.qq.com/s/_rt1NP_LPfzatb0EYXry9Q

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2261307.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux(CentOS8)安装PostgreSQL16详解

文章目录 1 下载安装包2 安装3 修改远程连接4 开放端口 1 下载安装包 官网下载地址:https://www.postgresql.org/download/ 选择对应版本 2 安装 #yum源 yum -y install wget https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redha…

spring学习(spring-bean实例化(无参构造与有参构造方法实现)详解)

目录 一、spring容器之bean的实例化。 (1)"bean"基本概念。 (2)spring-bean实例化的几种方式。 二、spring容器使用"构造方法"的方式实例化bean。 (1)无参构造方法实例化bean。 &#…

ElasticSearch学习5

基本Rest命令说明: method url地址 描述 PUT(创建,修改) localhost:9200/索引名称/类型名称/文档id 创建文档(指定文档id) POST(创建) localhost:9200/索引名称/类型名称 创建文档&…

分享本周所学——三维重建算法3D Gaussian Splatting(3DGS)

大家好,欢迎来到《分享本周所学》第十二期。本人是一名人工智能初学者,刚刚读完大二。前几天自学了一下3D Gaussian Splatting(3DGS),觉得非常有意思。写这篇文章主要是因为网上大部分关于3DGS的文章都比较晦涩&#x…

【中工开发者】鸿蒙商城app

这学期我学习了鸿蒙,想用鸿蒙做一个鸿蒙商城app,来展示一下。 项目环境搭建: 1.开发环境:DevEco Studio2.开发语言:ArkTS3.运行环境:Harmony NEXT base1 软件要求: DevEco Studio 5.0.0 Rel…

【Qt】按钮类控件:QPushButton、QRadioButton、QCheckBox、ToolButton

目录 QPushButton 例子: QRadioButton 例子: 按钮的常见信号函数 单选按钮分组 例子: QCheckButton 例子: QToolButton QWidget的常见属性及其功能对于它的派生类控件都是有效的(也就是Qt中的各种控件),包括…

UI框架DevExpress XAF v24.2新功能预览 - .NET Core / .NET增强

DevExpress XAF是一款强大的现代应用程序框架,允许同时开发ASP.NET和WinForms。DevExpress XAF采用模块化设计,开发人员可以选择内建模块,也可以自行创建,从而以更快的速度和比开发人员当前更强有力的方式创建应用程序。 在上文中…

ArrayList源码分析、扩容机制面试题,数组和List的相互转换,ArrayList与LinkedList的区别

目录 1.java集合框架体系 2. 前置知识-数组 2.1 数组 2.1.1 定义: 2.1.2 数组如何获取其他元素的地址值?(寻址公式) 2.1.3 为什么数组索引从0开始呢?从1开始不行吗? 3. ArrayList 3.1 ArrayList和和…

阿里云服务器手动部署LNMP环境【官方文档注意事项】

这是官方文档 注意&#xff1a; 要添加安全组&#xff0c;端口为80。否则最后用浏览器访问公网IP没有结果。 Mysql密码策略要求密码至少包含一个大写字母、一个小写字母、一个数字和一个特殊字符&#xff0c;并且密码总长度至少为 8 个字符。sudo mysqladmin -uroot -p<ol…

Invalid default value for ‘gender‘,mysql在idea中字符集设置,default

默认值default创建错误的&#xff0c;设置数据库字符集 我的错误&#xff1a;Invalid default value for ‘gender’ -- 修改数据库字符集 alter database db01 charset utf8;

240004基于Jamva+ssm+maven+mysql的房屋租赁系统的设计与实现

基于ssmmavenmysql的房屋租赁系统的设计与实现 1.项目描述2.运行环境3.项目截图4.源码获取 1.项目描述 该项目在原有的基础上进行了优化&#xff0c;包括新增了注册功能&#xff0c;房屋模糊查询功能&#xff0c;管理员和用户信息管理等功能&#xff0c;以及对网站界面进行了优…

使用Navicat从SQL Server导入表数据到MySQL

在表上右键选择导入向导 选择ODBC 1.内输入ip即可&#xff0c;不需要端口号 一定要勾选允许保存密码 选择需要的表&#xff0c;下一步 根据需求&#xff0c;可修改表名、是否新建表 根据需求修改不同表的字段类型和长度 按需选择导入方式

STM32F407+LAN8720A +LWIP +FreeRTOS ping通

使用STM32CUBEIDE自带的 LWIP和FreeROTS 版本说明STM32CUBEIDE 操作如下1. 配置RCC/SYS2. 配置ETH/USART3. 配置EHT_RESET/LED4. 配置FreeRTOS5. 配置LWIP6. 配置时钟7. 生成单独的源文件和头文件,并生成代码8. printf重定义9. ethernetif.c添加lan8720a复位10. MY_LWIP_Init …

用 Python Turtle 绘制经典汤姆猫:重温卡通角色的经典魅力

用 Python Turtle 绘制经典汤姆猫&#xff1a;重温卡通角色的经典魅力 &#x1f438; 前言 &#x1f438;&#x1f41e;往期绘画>>点击进所有绘画&#x1f41e;&#x1f40b; 效果图 &#x1f40b;&#x1f409; 代码 &#x1f409; &#x1f438; 前言 &#x1f438; 汤…

RabbitMQ个人理解与基本使用

目录 一. 作用&#xff1a; 二. RabbitMQ的5中队列模式&#xff1a; 1. 简单模式 2. Work模式 3. 发布/订阅模式 4. 路由模式 5. 主题模式 三. 消息持久化&#xff1a; 消息过期时间 ACK应答 四. 同步接收和异步接收&#xff1a; 应用场景 五. 基本使用 &#xff…

Y3编辑器文档4:触发器1(界面及使用简介、变量作用域、入门案例)

文章目录 一、触发器简介1.1 触发器界面1.2 ECA语句编辑及快捷键1.3 参数设置1.4 变量设置1.5 实体触发器1.6 触发器复用 二、触发器的多层结构2.1 子触发器&#xff08;在游戏内对新的事件进行注册&#xff09;2.2 触发器变量作用域 三、入门案例3.1 使用触发器实现瞬间移动3.…

【DBeaver】连接带kerberos的hive[Apache|HDP]

目录 一、安装配置Kerberos客户端环境 1.1 安装Kerberos客户端 1.2 环境配置 二、基于Cloudera驱动创建连接 三、基于Hive原生驱动创建连接 一、安装配置Kerberos客户端环境 1.1 安装Kerberos客户端 在Kerberos官网下载,地址如下&#xff1a;https://web.mit.edu/kerberos…

bug:uniapp运行到微信开发者工具 白屏 页面空白

1、没有报错信息 2、预览和真机调试都能正常显示&#xff0c;说明代码没错 3、微信开发者工具版本已经是win7能装的最高版本了&#xff0c;1.05版 链接 不打算回滚旧版本 4、解决&#xff1a;最后改调试基础库为2.25.4解决了&#xff0c;使用更高版本的都会报错&#xff0c;所…

【前端】JavaScript自定义 forEach方法详解与原理分析

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 &#x1f4af;前言&#x1f4af;题目演示与效果演示代码控制台输出结果 &#x1f4af;代码分析与源理解释1. 构造函数 Brray2. 实例化 Brray3. 调用自定义的 forEach &#x1f4af;比较与拓展1. 比较原…

基于卷积神经网络的Caser算法

将一段交互序列嵌入到一个以时间为纵轴的平面空间中形成“一张图”后&#xff0c;基于卷积序列嵌入的推荐&#xff08;Caser&#xff09;算法利用多个不同大小的卷积滤波器&#xff0c;来捕捉序列中物品间的点级&#xff08;point-level&#xff09;、联合的&#xff08;union-…