news 2026/7/10 6:35:28

零停机更新代码:SpringBoot新技能,太6了~

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
零停机更新代码:SpringBoot新技能,太6了~

在个人或者企业服务器上,总归有要更新代码的时候,普通的做法必须先终止原来进程,因为新进程和老进程端口是一个,新进程在启动时候,必定会出现端口占用的情况,但是,还有黑科技可以让两个SpringBoot进程真正的共用同一个端口,这是另一种解决办法,我们下回分解。

那么就会出现一个问题,如果此时有大量的用户在访问,但是你的代码又必须要更新,这时候如果采用上面的做法,那么必定会导致一段时间内的用户无法访问,这段时间还取决于你的项目启动速度,那么在单体应用下,如何解决这种事情?

一种简单办法是,新代码先用其他端口启动,启动完毕后,更改nginx的转发地址,nginx重启非常快,这样就避免了大量的用户访问失败,最后终止老进程就可以。

但是还是比较麻烦,端口换来换去,即使你写个脚本,也是比较麻烦,有没有一种可能,新进程直接启动,自动处理好这些事情?

答案是有的。

设计思路

这里涉及到几处源码类的知识,如下。

  • SpringBoot内嵌Servlet容器的原理是什么

  • DispatcherServlet是如何传递给Servlet容器的

先看第一个问题,用Tomcat来说,这个首先得Tomcat本身支持,如果Tomcat不支持内嵌,SpringBoot估计也没办法,或者可能会另找出路。

Tomcat本身有一个Tomcat类,没错就叫Tomcat,全路径是org.apache.catalina.startup.Tomcat,我们想启动一个Tomcat,直接new Tomcat(),之后调用start()就可以了。

并且他提供了添加Servlet、配置连接器这些基本操作。

public class Main { public static void main(String[] args) { try { Tomcat tomcat =new Tomcat(); tomcat.getConnector(); tomcat.getHost(); Context context = tomcat.addContext("/", null); tomcat.addServlet("/","index",new HttpServlet(){ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().append("hello"); } }); context.addServletMappingDecoded("/","index"); tomcat.init(); tomcat.start(); }catch (Exception e){} } }

在SpringBoot源码中,根据你引入的Servlet容器依赖,通过下面代码可以获取创建对应容器的工厂,拿Tomcat来说,创建Tomcat容器的工厂类是TomcatServletWebServerFactory

private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) { String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class); return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class); }

调用ServletWebServerFactory.getWebServer就可以获取一个Web服务,他有startstop方法启动、关闭Web服务。

getWebServer方法的参数很关键,也是第二个问题,DispatcherServlet是如何传递给Servlet容器的。

SpringBoot并不像上面Tomcat的例子一样简单的通过tomcat.addServletDispatcherServlet传递给Tomcat,而是通过个Tomcat主动回调来完成的,具体的回调通过ServletContainerInitializer接口协议,它允许我们动态地配置Servlet、过滤器。

SpringBoot在创建Tomcat后,会向Tomcat添加一个此接口的实现,类名是TomcatStarter,但是TomcatStarter也只是一堆SpringBoot内部ServletContextInitializer的集合,简单的封装了一下,这些集合中有一个类会向Tomcat添加DispatcherServlet

在Tomcat内部启动后,会通过此接口回调到SpringBoot内部,SpringBoot在内部会调用所有ServletContextInitializer集合来初始化,

getWebServer的参数正好就是一堆ServletContextInitializer集合。

那么这时候还有一个问题,怎么获取ServletContextInitializer集合?

非常简单,注意,ServletContextInitializerBeans是实现Collection的。

protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) { return new ServletContextInitializerBeans(context.getBeanFactory()); }

到这里所有用到的都准备完毕了,思路也很简单。

  • 判断端口是否占用

  • 占用则先通过其他端口启动

  • 等待启动完毕后终止老进程

  • 重新创建容器实例并且关联DispatcherServlet

在第三步和第四步之间,速度很快的,这样就达到了无缝更新代码的目的。

实现代码

@SpringBootApplication() @EnableScheduling publicclass WebMainApplication { public static void main(String[] args) { String[] newArgs = args.clone(); int defaultPort = 8088; boolean needChangePort = false; if (isPortInUse(defaultPort)) { newArgs = new String[args.length + 1]; System.arraycopy(args, 0, newArgs, 0, args.length); newArgs[newArgs.length - 1] = "--server.port=9090"; needChangePort = true; } ConfigurableApplicationContext run = SpringApplication.run(WebMainApplication.class, newArgs); if (needChangePort) { String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort); try { Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor(); while (isPortInUse(defaultPort)) { } ServletWebServerFactory webServerFactory = getWebServerFactory(run); ((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort); WebServer webServer = webServerFactory.getWebServer(invokeSelfInitialize(((ServletWebServerApplicationContext) run))); webServer.start(); ((ServletWebServerApplicationContext) run).getWebServer().stop(); } catch (IOException | InterruptedException ignored) { } } } private static ServletContextInitializer invokeSelfInitialize(ServletWebServerApplicationContext context) { try { Method method = ServletWebServerApplicationContext.class.getDeclaredMethod("getSelfInitializer"); method.setAccessible(true); return (ServletContextInitializer) method.invoke(context); } catch (Throwable e) { thrownew RuntimeException(e); } } private static boolean isPortInUse(int port) { try (ServerSocket serverSocket = new ServerSocket(port)) { returnfalse; } catch (IOException e) { returntrue; } } protected static Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context) { returnnew ServletContextInitializerBeans(context.getBeanFactory()); } private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) { String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class); return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class); } }

测试

我们先写一个小demo。

@RestController() @RequestMapping("port/test") public class TestPortController { @GetMapping("test") public String test() { return "1"; } }

并且打包成jar,然后更改返回值为2,并打包成v2版本的jar包,此时有两个代码,一个新的一个旧的。

我们先启动v1版本,并且使用IDEA中最好用的接口调试插件Cool Request测试,可以发现此时都正常。

好的我们不用关闭v1的进程,直接启动v2的jar包,并且启动后,可以一直在Cool Request测试接口时间内的可用程度。

稍等后,就会看到v2代码已经生效,而在这个过程中,服务只有极短的时间不可用,不会超过1秒。

妙不妙?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 15:59:34

测试方法创新:从自动化到智能化的转型之路‌

随着数字化转型加速&#xff0c;软件测试正面临前所未有的挑战与机遇。测试方法创新不仅是技术演进的结果&#xff0c;更是应对复杂系统、快速交付和用户期望的必然选择。本文将深入分析测试创新的核心驱动力&#xff0c;探讨关键创新方法&#xff0c;并展望未来趋势&#xff0…

作者头像 李华
网站建设 2026/7/10 4:33:48

基于多智能体深度强化学习的车联网通信资源分配优化探索

X00105-基于多智能体深度强化学习的车联网通信资源分配优化 无线网络的高速发展为车联网提供了更好的支持&#xff0c;但是如何为高速移动车辆提供更高质量的服务仍然是一个挑战 . 通过分析多个车对车&#xff08;Vehicle-to-Vehicle&#xff0c;V2V&#xff09;链路重用的车对…

作者头像 李华
网站建设 2026/7/9 9:25:14

小程序毕设项目推荐-基于微信小程序的学生宿舍管理系统基于springboot+微信小程序的高校学生公寓道闸管理平台的设计与实现【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/7/8 18:31:40

小程序毕设项目推荐-基于springboot+vue的微信小程序的快递代取系统的设计与实现基于springboot+微信小程序的快递代取系统的设计与实小程序【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/7/9 7:37:31

小程序毕设项目推荐-基于微信小程序的宠物服务系统基于springboot+微信小程序的宠物服务系统小程序【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/7/8 23:13:45

给AI装上“海马体”:三层类人记忆架构如何让多Agent系统真正懂你

当你的AI助手不仅能记住“你喜欢咖啡加奶”,还能理解“上次那张京都照片里的寺庙,就是我想去的地方”——它才真正从工具变成了伙伴。 在2025年,大模型的能力已不再是瓶颈。GPT-4o、Claude 3.5、Qwen-Max 等模型在单轮任务中表现惊艳。但一旦进入长期、多轮、多模态的真实场…

作者头像 李华