当前位置:首页>java>零停机更新代码:SpringBoot新技能,太6了~

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

  • 2026-10-21 13:27:34
零停机更新代码:SpringBoot新技能,太6了~
推荐关注
顶级架构师后台回复 1024 有特别礼包
来源:i听风逝夜
链接:juejin.cn/post/7385905203647184934

上一篇:这些 SpringBoot 默认配置不改,迟早踩坑!

大家好,我是顶级架构师。

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

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

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

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

答案是有的。

设计思路

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

  • SpringBoot内嵌Servlet容器的原理是什么
  • DispatcherServlet是如何传递给Servlet容器的

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

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

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

publicclassMain{publicstaticvoidmain(String[] args){try {            Tomcat tomcat =new Tomcat();            tomcat.getConnector();            tomcat.getHost();            Context context = tomcat.addContext("/", null);            tomcat.addServlet("/","index",new HttpServlet(){@OverrideprotectedvoiddoGet(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。

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

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

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

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

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

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

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

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

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

protectedstatic Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context){returnnew ServletContextInitializerBeans(context.getBeanFactory());}

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

  • 判断端口是否占用
  • 占用则先通过其他端口启动
  • 等待启动完毕后终止老进程
  • 重新创建容器实例并且关联DispatcherServlet

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

实现代码

@SpringBootApplication()@EnableSchedulingpublicclassWebMainApplication{publicstaticvoidmain(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) {            }        }    }privatestatic 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);        }    }privatestaticbooleanisPortInUse(int port){try (ServerSocket serverSocket = new ServerSocket(port)) {returnfalse;        } catch (IOException e) {returntrue;        }    }protectedstatic Collection<ServletContextInitializer> getServletContextInitializerBeans(ConfigurableApplicationContext context){returnnew ServletContextInitializerBeans(context.getBeanFactory());    }privatestatic ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context){        String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);    }}

测试

我们先写一个小demo。

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

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

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

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

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

妙不妙?

欢迎大家进行观点的探讨和碰撞,各抒己见。如果你有疑问,也可以找我沟通和交流。扩展:接私活儿

最后给读者整理了一份BAT大厂面试真题,需要的可扫码回复“面试题”即可获取。

公众号后台回复 架构 或者 架构整洁 有惊喜礼包!
顶级架构师交流群

「顶级架构师」建立了读者架构师交流群,大家可以添加小编微信进行加群。欢迎有想法、乐于分享的朋友们一起交流学习。

扫描添加好友邀你进架构师群,加我时注明【姓名+公司+职位】

版权申明:内容来源网络,版权归原作者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

猜你还想看

推荐一套开源通用后台管理系统(附源码)

牛逼啊!接私活必备的 N 个开源项目!赶快收藏吧(附源码合集第二期)!

看看人家那 IM 即时通讯系统,那叫一个优雅(附源码)

面试官:生成订单30分钟未支付,则自动取消,该怎么实现?

阿里技术专家:一文教你高效画出技术架构图

新一代项目管理系统,全行业覆盖的一站式项目协作工具!

架构师流程引擎的架构设计

一款面向个人的轻量级笔记系统,个人安全、可扩展的知识库!

面试官:什么是脚手架?为什么需要脚手架?常用的脚手架有哪些?

我已经不用 try catch 处理异常!太酸菜了!

MES管理系统,最值得信赖的新一代工厂管家!

ES+Redis+MySQL,这个高可用架构设计太顶了!

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 手册 领取,更多内容陆续奉上。
明天见(。・ω・。)

最新文章

随机文章