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