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

