网易首页 > 网易号 > 正文 申请入驻

代码更新不停机:SpringBoot应用实现热更新机制!

0
分享至

Java精选面试题 (微信小程序): 5000+ 道面试题和选择题, 真实面经 , 简历模版 ,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计、大厂真题等,在线随时刷题!

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

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

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

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

推荐划水摸鱼地址: https://www.yoodb.com/slack-off/home.html

答案是有的。

设计思路

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

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

  2. 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服务,他有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的。

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

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

  1. 判断端口是否占用

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

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

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

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

实现代码

@SpringBootApplication()
@EnableScheduling
public class 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) {
            throw new RuntimeException(e);
        }

    }

    private static boolean isPortInUse(int port) {
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            return false;
        } catch (IOException e) {
            return true;
        }
    }

    protected static Collection
      
  getServletContextInitializerBeans(ConfigurableApplicationContext context) {          return new 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秒。

妙不妙?

来源:网络

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

最近有很多人问,有没有技术或摸鱼交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!在线摸鱼:https://www.yoodb.com/

Java精选面试题(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!

特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注

文章有帮助的话,点在看,转发吧!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
涉三罪!江西省卫生健康委员会原党组书记龚建平被公诉

涉三罪!江西省卫生健康委员会原党组书记龚建平被公诉

极目新闻
2026-06-24 16:16:35
吸取俄罗斯血的教训,台海之战可能成为人类史上第一场零伤亡战争

吸取俄罗斯血的教训,台海之战可能成为人类史上第一场零伤亡战争

霁寒飘雪
2026-06-24 16:46:07
梅西冲击生涯唯一缺失奖杯:世界杯金靴

梅西冲击生涯唯一缺失奖杯:世界杯金靴

体坛观察猿
2026-06-24 00:52:09
男子让好友继续睡自己妻子,遭好友拒绝,2010年他杀死好友妻和娃

男子让好友继续睡自己妻子,遭好友拒绝,2010年他杀死好友妻和娃

汉史趣闻
2026-06-24 06:53:00
越扒越有!成都地铁大爷强拽女孩让座后续,和家长互殴,官方回应

越扒越有!成都地铁大爷强拽女孩让座后续,和家长互殴,官方回应

削桐作琴
2026-06-24 14:34:48
臻宝科技首日暴涨超1200%,触发临停!中一签赚23万元

臻宝科技首日暴涨超1200%,触发临停!中一签赚23万元

新浪财经
2026-06-24 14:51:27
官媒2天4次点名雷军,释放三个强烈信号,刘强东的话真没说错

官媒2天4次点名雷军,释放三个强烈信号,刘强东的话真没说错

叹为观止易
2026-06-23 10:25:28
A股:大家做好准备了,明天,大的变化很可能要来了

A股:大家做好准备了,明天,大的变化很可能要来了

财经大拿
2026-06-24 13:57:01
我如今已68了,以亲身血泪教训告诉你:不要跟任何人,包括你的父母、子女、枕边人,分享这两件事

我如今已68了,以亲身血泪教训告诉你:不要跟任何人,包括你的父母、子女、枕边人,分享这两件事

心理观察局
2026-05-23 07:00:06
4年集齐6个前7顺位!双核年薪1.1亿!最后一支靠摆烂上岸的球队?

4年集齐6个前7顺位!双核年薪1.1亿!最后一支靠摆烂上岸的球队?

阿浪的篮球故事
2026-06-24 16:05:12
给老人的忠告:永远不要在女婿、儿媳妇面前,表现出以下4种行为

给老人的忠告:永远不要在女婿、儿媳妇面前,表现出以下4种行为

热心市民小黄
2026-06-23 15:10:51
国庆节,老公给公婆每人1万,我说给我爸妈5000,他怒吼:养你全家?

国庆节,老公给公婆每人1万,我说给我爸妈5000,他怒吼:养你全家?

风起见你
2026-06-24 16:31:05
娜然辱华言论曝光,霍家婚讯紧急刹车,郭晶晶一句话把门堵死了

娜然辱华言论曝光,霍家婚讯紧急刹车,郭晶晶一句话把门堵死了

李橑在北漂
2026-06-22 16:20:54
2026年最强反腐来了!中纪委:害群之马将清除到底!

2026年最强反腐来了!中纪委:害群之马将清除到底!

职场资深秘书
2026-06-24 15:35:08
50岁李小冉机场吃面,褪去滤镜才懂,普通人的衰老藏不住

50岁李小冉机场吃面,褪去滤镜才懂,普通人的衰老藏不住

庭小娱
2026-05-13 12:06:40
广东高考分数线公布

广东高考分数线公布

界面新闻
2026-06-24 11:14:00
悲剧!奥克兰公园池塘遗体身份确认,失踪近3周后不幸身亡!家属“震惊且心碎”!

悲剧!奥克兰公园池塘遗体身份确认,失踪近3周后不幸身亡!家属“震惊且心碎”!

新西兰天维网
2026-06-24 14:25:09
立讯精密、工业富联、领益智造、中国长城谁是液冷服务器突破老大

立讯精密、工业富联、领益智造、中国长城谁是液冷服务器突破老大

长风价值掘金
2026-06-24 15:44:17
今日金价:大家要有心理准备了,6月24日,金价或将重现15年历史

今日金价:大家要有心理准备了,6月24日,金价或将重现15年历史

生活新鲜市
2026-06-24 13:46:53
AI焦虑?市值焦虑?李彦宏投了10来年,百度跌了5560亿

AI焦虑?市值焦虑?李彦宏投了10来年,百度跌了5560亿

无冕财经
2026-06-24 14:25:56
2026-06-24 18:59:00
Java精选
Java精选
一场永远也演不完的戏
1795文章数 3859关注度
往期回顾 全部

科技要闻

豆包专业版上线:定价68-500元每月

头条要闻

使用隔夜过期食材、徒手制作饮品 "1点点"奶茶致歉

头条要闻

使用隔夜过期食材、徒手制作饮品 "1点点"奶茶致歉

体育要闻

字母哥,会把凯尔特人拆了吗?

娱乐要闻

向佐向佑兄弟合体直播!母子终于和解

财经要闻

爆料人:如果我错了,赔偿坐牢都接受

汽车要闻

施鹏泽:为什么奥迪E7X强调座舱气味安全?

态度原创

健康
家居
亲子
教育
时尚

神经内科专家破解中风十大谣言

家居要闻

绿意盎然 自然之境

亲子要闻

5个月宝宝纸尿裤,2026全新升级好奇小森林,低敏透氧给足安全感

教育要闻

“考考”说高考|

适合7月的三种风格,照着穿

无障碍浏览 进入关怀版