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ī)切換:通過先啟動(dòng)備用服務(wù)、釋放主端口,再切換新服務(wù)到主端口,實(shí)現(xiàn)服務(wù)的無縫切換。
端口檢測(cè)和進(jìn)程終止:使用 ServerSocket 和系統(tǒng)命令來檢測(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 通過 ServletContextInitializer 集合完成 Servlet 注冊(cè)。
2.端口檢查和動(dòng)態(tài)切換:
通過 ServerSocket 判斷端口是否占用。
如果占用,則先用備用端口啟動(dòng)新服務(wù),再通過關(guān)閉老服務(wù)釋放主端口,最后切換新服務(wù)到主端口。
3.運(yùn)行時(shí)自動(dòng)處理:
利用 Runtime.exec 執(zhí)行系統(tǒng)命令,釋放端口并終止舊進(jìn)程。
在極短時(shí)間內(nèi)完成新舊服務(wù)切換,避免長時(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)用上下文,用于訪問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í)例,說明端口可用,返回false
return false;
} catch (IOException e) {
// 如果創(chuàng)建ServerSocket實(shí)例時(shí)拋出IOException,說明端口已被占用,返回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)類返回
// ServletContextInitializerBeans將會(huì)負(fù)責(zé)收集應(yīng)用上下文中所有ServletContextInitializer的實(shí)現(xiàn)
// 并在應(yīng)用啟動(dòng)時(shí)依次調(diào)用它們的onStartup方法來初始化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)后,訪問 http://localhost:8080/port/test
修改TestPortController 的返回值, 打個(gè)jar包, 啟動(dòng)新的jar包,
重新訪問 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每次新打開的項(xiàng)目窗口maven都要重新設(shè)置問題
這篇文章主要介紹了idea每次新打開的項(xiàng)目窗口maven都要重新設(shè)置問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-11-11
SpringBoot對(duì)接AWS?S3實(shí)現(xiàn)上傳和查詢
AWS?S3是亞馬遜提供的一種對(duì)象存儲(chǔ)服務(wù),旨在提供可擴(kuò)展、高可用性和安全的數(shù)據(jù)存儲(chǔ)解決方案,本文我們就來看看SpringBoot如何對(duì)接AWS?S3實(shí)現(xiàn)上傳和查詢吧2025-02-02
Spring AOP定義AfterReturning增加實(shí)例分析
這篇文章主要介紹了Spring AOP定義AfterReturning增加,結(jié)合實(shí)例形式分析了Spring面相切面AOP定義AfterReturning增加相關(guān)操作技巧與使用注意事項(xiàng),需要的朋友可以參考下2020-01-01
SpringBoot項(xiàng)目集成Swagger和swagger-bootstrap-ui及常用注解解讀
這篇文章主要介紹了SpringBoot項(xiàng)目集成Swagger和swagger-bootstrap-ui及常用注解解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
mybatis中一對(duì)一關(guān)系association標(biāo)簽的使用
這篇文章主要介紹了mybatis中一對(duì)一關(guān)系association標(biāo)簽的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
java.sql.SQLException:?connection?holder?is?null錯(cuò)誤解決辦法
這篇文章主要給大家介紹了關(guān)于java.sql.SQLException:?connection?holder?is?null錯(cuò)誤的解決辦法,這個(gè)錯(cuò)誤通常是由于連接對(duì)象為空或未正確初始化導(dǎo)致的,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-02-02

