SpringBoot實現(xiàn)動態(tài)端口切換黑魔法
關(guān)鍵技術(shù)點
利用 Spring Boot 內(nèi)嵌 Servlet 容器 和 動態(tài)端口切換 的方式實現(xiàn)平滑更新的方案,關(guān)鍵技術(shù)點如下:
Servlet 容器重新綁定端口:Spring Boot 使用 ServletWebServerFactory 動態(tài)設(shè)置新端口。
零停機切換:通過先啟動備用服務(wù)、釋放主端口,再切換新服務(wù)到主端口,實現(xiàn)服務(wù)的無縫切換。
端口檢測和進程終止:使用 ServerSocket 和系統(tǒng)命令來檢測和操作端口。
這種設(shè)計允許服務(wù)在不完全停止的情況下切換到更新的版本,從而極大地縮短了不可用時間,實現(xiàn)了接近于零停機的效果。
核心原理
1.內(nèi)嵌 Tomcat 容器動態(tài)啟動:
使用 TomcatServletWebServerFactory 實現(xiàn)容器的動態(tài)創(chuàng)建和啟動。
動態(tài)綁定 DispatcherServlet 通過 ServletContextInitializer 集合完成 Servlet 注冊。
2.端口檢查和動態(tài)切換:
通過 ServerSocket 判斷端口是否占用。
如果占用,則先用備用端口啟動新服務(wù),再通過關(guān)閉老服務(wù)釋放主端口,最后切換新服務(wù)到主端口。
3.運行時自動處理:
利用 Runtime.exec 執(zhí)行系統(tǒng)命令,釋放端口并終止舊進程。
在極短時間內(nèi)完成新舊服務(wù)切換,避免長時間的停機。
Code
package com.artisan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.servlet.ServletContextInitializer; import org.springframework.boot.web.servlet.ServletContextInitializerBeans; import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.ConfigurableApplicationContext; import java.io.IOException; import java.net.ServerSocket; import java.util.Collections; @SpringBootApplication() public class BootMainApplication { public static void main(String[] args) { // 默認端口設(shè)置 int defaultPort = 8080; // 備選端口設(shè)置 int alternativePort = 9090; // 檢查默認端口是否已被占用 boolean isPortOccupied = isPortInUse(defaultPort); // 動態(tài)端口分配 int portToUse = isPortOccupied ? alternativePort : defaultPort; // 創(chuàng)建Spring Boot應(yīng)用實例 SpringApplication app = new SpringApplication(WebMainApplication2.class); // 設(shè)置端口配置 app.setDefaultProperties(Collections.singletonMap("server.port", portToUse)); // 運行應(yīng)用并獲取上下文 ConfigurableApplicationContext context = app.run(args); // 如果默認端口被占用,則嘗試切換回默認端口 if (isPortOccupied) { switchToDefaultPort(context, defaultPort, portToUse); } } /** * 切換到默認端口 * * 當(dāng)默認端口被其他進程占用時,此方法嘗試釋放該端口,并啟動一個新的Web服務(wù)器實例綁定到默認端口 * 同時,它會停止當(dāng)前的Web服務(wù)器實例 * * @param context 當(dāng)前應(yīng)用上下文,用于訪問Web服務(wù)器工廠和停止當(dāng)前Web服務(wù)器 * @param defaultPort 默認端口號,希望切換到的目標(biāo)端口 * @param currentPort 當(dāng)前Web服務(wù)器正在使用的端口號 */ private static void switchToDefaultPort(ConfigurableApplicationContext context, int defaultPort, int currentPort) { try { // 釋放默認端口 terminateProcessUsingPort(defaultPort); // 等待端口釋放 while (isPortInUse(defaultPort)) { Thread.sleep(100); } // 啟動新容器綁定默認端口 ServletWebServerFactory webServerFactory = getWebServerFactory(context); ((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort); WebServer newServer = webServerFactory.getWebServer(getServletContextInitializers(context)); newServer.start(); // 停止當(dāng)前容器 ((ServletWebServerApplicationContext) context).getWebServer().stop(); } catch (Exception e) { e.printStackTrace(); } } /** * 檢查指定的端口是否正在使用 * * @param port 要檢查的端口號 * @return 如果端口正在使用,則返回true;否則返回false */ private static boolean isPortInUse(int port) { try (ServerSocket serverSocket = new ServerSocket(port)) { // 如果能夠成功創(chuàng)建ServerSocket實例,說明端口可用,返回false return false; } catch (IOException e) { // 如果創(chuàng)建ServerSocket實例時拋出IOException,說明端口已被占用,返回true return true; } } /** * 終止使用指定端口的進程 * * @param port 需要釋放的端口號 * @throws IOException 如果執(zhí)行命令發(fā)生錯誤 * @throws InterruptedException 如果線程被中斷 */ private static void terminateProcessUsingPort(int port) throws IOException, InterruptedException { // 構(gòu)建終止使用指定端口的進程的命令 String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", port); // 執(zhí)行命令并等待命令執(zhí)行完成 Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor(); } /** * 獲取ServletContextInitializer實例 * 該方法用于將Spring應(yīng)用上下文中的所有ServletContextInitializerBeans實例 * 轉(zhuǎn)換為ServletContextInitializer接口的實現(xiàn),以便在應(yīng)用啟動時初始化ServletContext * * @param context Spring的應(yīng)用上下文,用于獲取BeanFactory * @return 返回一個實現(xiàn)了ServletContextInitializer接口的實例 */ private static ServletContextInitializer getServletContextInitializers(ConfigurableApplicationContext context) { // 使用ApplicationContext中的BeanFactory創(chuàng)建ServletContextInitializerBeans實例 // 這里將ServletContextInitializerBeans作為ServletContextInitializer的實現(xiàn)類返回 // ServletContextInitializerBeans將會負責(zé)收集應(yīng)用上下文中所有ServletContextInitializer的實現(xiàn) // 并在應(yīng)用啟動時依次調(diào)用它們的onStartup方法來初始化ServletContext return (ServletContextInitializer) new ServletContextInitializerBeans(context.getBeanFactory()); } /** * 獲取Servlet Web服務(wù)器工廠 * * @param context 可配置的應(yīng)用上下文,用于獲取Bean工廠 * @return ServletWebServerFactory實例,用于配置和創(chuàng)建Web服務(wù)器 */ private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) { // 從應(yīng)用上下文中獲取Bean工廠,并從中獲取ServletWebServerFactory實例 return context.getBeanFactory().getBean(ServletWebServerFactory.class); } }
測試
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController() @RequestMapping("port/") public class TestPortController { @GetMapping("test") public String test() { return "artisan-old"; } }
啟動后,訪問 http://localhost:8080/port/test
修改TestPortController 的返回值, 打個jar包, 啟動新的jar包,
重新訪問 http://localhost:8080/port/test ,觀察返回結(jié)果是否是修改后的返回值
到此這篇關(guān)于SpringBoot實現(xiàn)動態(tài)端口切換黑魔法的文章就介紹到這了,更多相關(guān)SpringBoot動態(tài)端口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
idea每次新打開的項目窗口maven都要重新設(shè)置問題
這篇文章主要介紹了idea每次新打開的項目窗口maven都要重新設(shè)置問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11SpringBoot對接AWS?S3實現(xiàn)上傳和查詢
AWS?S3是亞馬遜提供的一種對象存儲服務(wù),旨在提供可擴展、高可用性和安全的數(shù)據(jù)存儲解決方案,本文我們就來看看SpringBoot如何對接AWS?S3實現(xiàn)上傳和查詢吧2025-02-02Spring AOP定義AfterReturning增加實例分析
這篇文章主要介紹了Spring AOP定義AfterReturning增加,結(jié)合實例形式分析了Spring面相切面AOP定義AfterReturning增加相關(guān)操作技巧與使用注意事項,需要的朋友可以參考下2020-01-01SpringBoot項目集成Swagger和swagger-bootstrap-ui及常用注解解讀
這篇文章主要介紹了SpringBoot項目集成Swagger和swagger-bootstrap-ui及常用注解解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03mybatis中一對一關(guān)系association標(biāo)簽的使用
這篇文章主要介紹了mybatis中一對一關(guān)系association標(biāo)簽的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03java.sql.SQLException:?connection?holder?is?null錯誤解決辦法
這篇文章主要給大家介紹了關(guān)于java.sql.SQLException:?connection?holder?is?null錯誤的解決辦法,這個錯誤通常是由于連接對象為空或未正確初始化導(dǎo)致的,文中通過代碼介紹的非常詳細,需要的朋友可以參考下2024-02-02