<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          代碼更新不停機(jī):SpringBoot應(yīng)用實(shí)現(xiàn)零停機(jī)更新的新質(zhì)生產(chǎn)力

          共 11768字,需瀏覽 24分鐘

           ·

          2024-07-31 07:25

          程序員的成長(zhǎng)之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享 
          關(guān)注


          閱讀本文大概需要 5 分鐘。

          來自:網(wǎng)絡(luò),侵刪

          推薦一個(gè)程序員編程資料站:
          http://cxyroad.com

          tips:
          2024年IDEA最新激活方法教程,后臺(tái)回復(fù):激活碼

          CSDN免登錄復(fù)制代碼插件下載:CSDN復(fù)制插件

          以下是正文。




          在個(gè)人或者企業(yè)服務(wù)器上,總歸有要更新代碼的時(shí)候,普通的做法必須先終止原來進(jìn)程,因?yàn)樾逻M(jìn)程和老進(jìn)程端口是一個(gè),新進(jìn)程在啟動(dòng)時(shí)候,必定會(huì)出現(xiàn)端口占用的情況,但是,還有黑科技可以讓兩個(gè)SpringBoot進(jìn)程真正的共用同一個(gè)端口,這是另一種解決辦法,我們下回分解。
          那么就會(huì)出現(xiàn)一個(gè)問題,如果此時(shí)有大量的用戶在訪問,但是你的代碼又必須要更新,這時(shí)候如果采用上面的做法,那么必定會(huì)導(dǎo)致一段時(shí)間內(nèi)的用戶無法訪問,這段時(shí)間還取決于你的項(xiàng)目啟動(dòng)速度,那么在單體應(yīng)用下,如何解決這種事情?
          一種簡(jiǎn)單辦法是,新代碼先用其他端口啟動(dòng),啟動(dòng)完畢后,更改nginx的轉(zhuǎn)發(fā)地址,nginx重啟非常快,這樣就避免了大量的用戶訪問失敗,最后終止老進(jìn)程就可以。
          但是還是比較麻煩,端口換來換去,即使你寫個(gè)腳本,也是比較麻煩,有沒有一種可能,新進(jìn)程直接啟動(dòng),自動(dòng)處理好這些事情?
          答案是有的。

          設(shè)計(jì)思路

          這里涉及到幾處源碼類的知識(shí),如下。
          1. SpringBoot內(nèi)嵌Servlet容器的原理是什么
          2. DispatcherServlet是如何傳遞給Servlet容器的
          先看第一個(gè)問題,用Tomcat來說,這個(gè)首先得Tomcat本身支持,如果Tomcat不支持內(nèi)嵌,SpringBoot估計(jì)也沒辦法,或者可能會(huì)另找出路。
          Tomcat本身有一個(gè)Tomcat類,沒錯(cuò)就叫Tomcat,全路徑是org.apache.catalina.startup.Tomcat,我們想啟動(dòng)一個(gè)Tomcat,直接new Tomcat(),之后調(diào)用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源碼中,根據(jù)你引入的Servlet容器依賴,通過下面代碼可以獲取創(chuàng)建對(duì)應(yīng)容器的工廠,拿Tomcat來說,創(chuàng)建Tomcat容器的工廠類是TomcatServletWebServerFactory

          private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
              String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);

              return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
          }

          調(diào)用ServletWebServerFactory.getWebServer就可以獲取一個(gè)Web服務(wù),他有start、stop方法啟動(dòng)、關(guān)閉Web服務(wù)。
          而getWebServer方法的參數(shù)很關(guān)鍵,也是第二個(gè)問題,DispatcherServlet是如何傳遞給Servlet容器的。
          SpringBoot并不像上面Tomcat的例子一樣簡(jiǎn)單的通過tomcat.addServlet把DispatcherServlet傳遞給Tomcat,而是通過個(gè)Tomcat主動(dòng)回調(diào)來完成的,具體的回調(diào)通過ServletContainerInitializer接口協(xié)議,它允許我們動(dòng)態(tài)地配置Servlet、過濾器。
          SpringBoot在創(chuàng)建Tomcat后,會(huì)向Tomcat添加一個(gè)此接口的實(shí)現(xiàn),類名是TomcatStarter,但是TomcatStarter也只是一堆SpringBoot內(nèi)部ServletContextInitializer的集合,簡(jiǎn)單的封裝了一下,這些集合中有一個(gè)類會(huì)向Tomcat添加DispatcherServlet
          在Tomcat內(nèi)部啟動(dòng)后,會(huì)通過此接口回調(diào)到SpringBoot內(nèi)部,SpringBoot在內(nèi)部會(huì)調(diào)用所有ServletContextInitializer集合來初始化,
          而getWebServer的參數(shù)正好就是一堆ServletContextInitializer集合。
          那么這時(shí)候還有一個(gè)問題,怎么獲取ServletContextInitializer集合?
          非常簡(jiǎn)單,注意,ServletContextInitializerBeans是實(shí)現(xiàn)Collection的。

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

          到這里所有用到的都準(zhǔn)備完畢了,思路也很簡(jiǎn)單。
          1. 判斷端口是否占用
          2. 占用則先通過其他端口啟動(dòng)
          3. 等待啟動(dòng)完畢后終止老進(jìn)程
          4. 重新創(chuàng)建容器實(shí)例并且關(guān)聯(lián)DispatcherServlet
          在第三步和第四步之間,速度很快的,這樣就達(dá)到了無縫更新代碼的目的。

          實(shí)現(xiàn)代碼

          @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<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);
              }

          }

          測(cè)試

          我們先寫一個(gè)小demo。

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

          并且打包成jar,然后更改返回值為2,并打包成v2版本的jar包,此時(shí)有兩個(gè)代碼,一個(gè)新的一個(gè)舊的。
          圖片
          我們先啟動(dòng)v1版本,并且使用IDEA中最好用的接口調(diào)試插件Cool Request測(cè)試,可以發(fā)現(xiàn)此時(shí)都正常。
          圖片
          好的我們不用關(guān)閉v1的進(jìn)程,直接啟動(dòng)v2的jar包,并且啟動(dòng)后,可以一直在Cool Request測(cè)試接口時(shí)間內(nèi)的可用程度。
          稍等后,就會(huì)看到v2代碼已經(jīng)生效,而在這個(gè)過程中,服務(wù)只有極短的時(shí)間不可用,不會(huì)超過1秒。
          圖片
          妙不妙?
          <END>

          推薦閱讀:

          不引入ES,如何利用 MySQL 實(shí)現(xiàn)模糊匹配

          Lombok 同時(shí)使用 @Data 和 @Builder 的巨坑,千萬別亂用!

              
          程序員在線工具站:cxytools.com

          推薦一個(gè)我自己寫的工具站:http://cxytools.com,專為程序員設(shè)計(jì),包括時(shí)間日期、JSON處理、SQL格式化、隨機(jī)字符串生成、UUID生成、隨機(jī)數(shù)生成、文本Hash...等功能,提升開發(fā)效率。

          ?戳閱讀原文直達(dá)!                                  朕已閱 

          瀏覽 84
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  大肉大捧一进一出两腿 | 无码视频在线播放 | 国产精品V无码A片在线看吃奶 | 一级录像在线免费播放 | 中文字幕黄色电影 |