久久精品精选,精品九九视频,www久久只有这里有精品,亚洲熟女乱色综合一区
    分享

    看好了,我只教一遍,SpringBoot單體應用真正的零停機無縫更新代碼

     jacklopy 2025-09-02

    前言

    在個人或者企業服務器上,總歸有要更新代碼的時候,普通的做法必須先終止原來進程,因為新進程和老進程端口是一個,新進程在啟動時候,必定會出現端口占用的情況,但是,還有黑科技可以讓兩個SpringBoot進程真正的共用同一個端口,這是另一種解決辦法,我們下回分解。

    那么就會出現一個問題,如果此時有大量的用戶在訪問,但是你的代碼又必須要更新,這時候如果采用上面的做法,那么必定會導致一段時間內的用戶無法訪問,這段時間還取決于你的項目啟動速度,那么在單體應用下,如何解決這種事情?

    一種簡單辦法是,新代碼先用其他端口啟動,啟動完畢后,更改nginx的轉發地址,nginx重啟非常快,這樣就避免了大量的用戶訪問失敗,最后終止老進程就可以。

    但是還是比較麻煩,端口換來換去,即使你寫個腳本,也是比較麻煩,有沒有一種可能,新進程直接啟動,自動處理好這些事情?

    答案是有的。

    設計思路

    這里涉及到幾處源碼類的知識,如下。

    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<ServletContextInitializer> 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.classnewArgs);
            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<ServletContextInitializer> 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包,此時有兩個代碼,一個新的一個舊的。

    圖片
    image.png

    我們先啟動v1版本,并且使用IDEA中最好用的接口調試插件Cool Request測試,可以發現此時都正常。

    圖片
    image.png

    好的我們不用關閉v1的進程,直接啟動v2的jar包,并且啟動后,可以一直在Cool Request測試接口時間內的可用程度。

    稍等后,就會看到v2代碼已經生效,而在這個過程中,服務只有極短的時間不可用,不會超過1秒。

    圖片
    image.png

    妙不妙?

      本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發布,不代表本站觀點。請注意甄別內容中的聯系方式、誘導購買等信息,謹防詐騙。如發現有害或侵權內容,請點擊一鍵舉報。
      轉藏 分享 獻花(0

      0條評論

      發表

      請遵守用戶 評論公約

      類似文章 更多

      主站蜘蛛池模板: 亚洲日韩国产精品第一页一区 | 毛片无遮挡高清免费| 日韩大片高清播放器大全| 久久久久波多野结衣高潮| 日日碰狠狠添天天爽五月婷| 国产成人啪精品视频免费APP| 精品无码久久久久久久动漫| 午夜在线观看成人av| 无码人妻一区二区三区兔费| 99精品国产99久久久久久97| 亚洲午夜成人精品电影在线观看| 人妻少妇精品久久久久久| 偷拍精品一区二区三区| 狠狠色丁香婷婷综合潮喷| 亚洲欧洲精品日韩av| 国产高清自产拍AV在线| 亚洲一区二区精品另类| 无码AV动漫精品一区二区免费| 精品一区二区中文字幕| 中文字幕亚洲一区二区三区| 中文字幕日韩国产精品| 国产果冻豆传媒麻婆精东| 华人在线亚洲欧美精品| 国产精品V欧美精品V日韩精品| 国产成人高清亚洲综合| 欧美人与禽2O2O性论交| 日本成熟少妇激情视频免费看| 亚洲更新最快无码视频| 亚洲爆乳少妇无码激情| 2020国产欧洲精品网站| 中文字幕在线观看| 精品日韩亚洲AV无码| 鲁丝片一区二区三区免费| 99精品电影一区二区免费看| 国产精品丝袜亚洲熟女| 国产999精品2卡3卡4卡| 夜夜躁狠狠躁日日躁| 成人无码特黄特黄AV片在线| 国产不卡av一区二区| 午夜亚洲福利在线老司机| 亚洲日本高清一区二区三区|