SpringBoot項目請求不中斷動態(tài)更新代碼的實現(xiàn)
1. 代碼概述
我們實現(xiàn)了一個簡單的 Spring Boot 應(yīng)用程序,它可以自動檢測端口是否被占用,并在必要時切換到備用端口,然后再將目標(biāo)端口程序關(guān)閉再將備用端口切換為目標(biāo)端口。具體功能包括:
- 檢查默認(rèn)端口(8080)是否被占用。
- 如果被占用,自動切換到備用端口(8086)。
- 在 Linux 系統(tǒng)下,優(yōu)雅地關(guān)閉占用該端口的進(jìn)程。
- 修改Tomcat端口并重啟容器。
完整代碼
import com.lps.utils.PortUtil;
import lombok.extern.slf4j.Slf4j;
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.server.ServletWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
/**
* @author 阿水
*/
@SpringBootApplication
@Slf4j
public class MybatisDemoApplication {
private static final int DEFAULT_PORT_8080 = 8080;
private static final int ALTERNATE_PORT_8086 = 8086;
public static void main(String[] args) {
boolean isNeedChangePort = PortUtil.isPortInUse(DEFAULT_PORT_8080);
String[] newArgs = Arrays.copyOf(args, args.length + 1);
if (isNeedChangePort) {
log.info("端口 {} 正在使用中, 正在嘗試端口切換到 {}.", DEFAULT_PORT_8080, ALTERNATE_PORT_8086);
newArgs[newArgs.length - 1] = "--server.port=" + ALTERNATE_PORT_8086;
}
log.info("啟動參數(shù): {}", Arrays.toString(newArgs));
//去除newArgs的null數(shù)據(jù)
newArgs = Arrays.stream(newArgs).filter(Objects::nonNull).toArray(String[]::new);
ConfigurableApplicationContext context = SpringApplication.run(MybatisDemoApplication.class, newArgs);
//判斷是否是linux系統(tǒng),如果是linux系統(tǒng),則嘗試殺死占用8080端口的進(jìn)程
System.out.println("是否需要修改端口: "+isNeedChangePort);
if (isNeedChangePort && isLinuxOS()) {
changePortAndRestart(context);
}
}
/**
* 如果端口占用,則嘗試殺死占用8080端口的進(jìn)程,并修改端口并重啟服務(wù)
*
* @param context
*/
private static void changePortAndRestart(ConfigurableApplicationContext context) {
log.info("嘗試殺死占用 8080 端口的進(jìn)程.");
killOldServiceInLinux();
log.info("正在修改端口更改為 {}.", DEFAULT_PORT_8080);
ServletWebServerFactory webServerFactory = context.getBean(ServletWebServerFactory.class);
ServletContextInitializer servletContextInitializer = context.getBean(ServletContextInitializer.class);
WebServer webServer = webServerFactory.getWebServer(servletContextInitializer);
if (webServer != null) {
log.info("停止舊服務(wù)器.");
webServer.stop();
}
//((TomcatServletWebServerFactory) servletContextInitializer).setPort(DEFAULT_PORT_8080);
((TomcatServletWebServerFactory) webServerFactory).setPort(DEFAULT_PORT_8080);
webServer = webServerFactory.getWebServer(servletContextInitializer);
webServer.start();
log.info("新服務(wù)啟動成功.");
}
/**
* 殺死占用 8080 端口的進(jìn)程
*/
private static void killOldServiceInLinux() {
try {
// 查找占用 8080 端口的進(jìn)程
String command = "lsof -t -i:" + DEFAULT_PORT_8080;
log.info("正在執(zhí)行命令: {}", command);
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String pid;
while ((pid = reader.readLine()) != null) {
// 發(fā)送 SIGINT 信號以優(yōu)雅關(guān)閉
Runtime.getRuntime().exec("kill -2 " + pid);
log.info("Killed process: {}", pid);
}
} catch (IOException e) {
log.error("Failed to stop old service", e);
}
}
/**
* 判斷是否是linux系統(tǒng)
*
* @return
*/
private static boolean isLinuxOS() {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
}工具類
import java.io.IOException;
import java.net.ServerSocket;
/**
* @author 阿水
*/
public class PortUtil {
public static boolean isPortInUse(int port) {
try (ServerSocket ignored = new ServerSocket(port)) {
// 端口未被占用
return false;
} catch (IOException e) {
// 端口已被占用
return true;
}
}
}
測試效果


2. 主要功能
檢測端口狀態(tài)
通過 PortUtil.isPortInUse() 檢查默認(rèn)端口的使用狀態(tài)。如果端口被占用,修改啟動參數(shù)。
import java.io.IOException;
import java.net.ServerSocket;
/**
* @author 阿水
*/
public class PortUtil {
public static boolean isPortInUse(int port) {
try (ServerSocket ignored = new ServerSocket(port)) {
// 端口未被占用
return false;
} catch (IOException e) {
// 端口已被占用
return true;
}
}
}修改啟動參數(shù)
當(dāng)發(fā)現(xiàn)端口被占用時,我們動態(tài)調(diào)整啟動參數(shù),以便在啟動時使用新的端口。
if (isNeedChangePort) {
log.info("端口 {} 正在使用中, 正在嘗試端口切換到 {}.", DEFAULT_PORT_8080, ALTERNATE_PORT_8086);
newArgs[newArgs.length - 1] = "--server.port=" + ALTERNATE_PORT_8086;
}優(yōu)雅關(guān)閉
在 Linux 系統(tǒng)中,如果檢測到端口被占用,調(diào)用 killOldServiceInLinux() 方法,優(yōu)雅地關(guān)閉占用該端口的進(jìn)程。這是通過發(fā)送 SIGINT 信號實現(xiàn)的,允許應(yīng)用程序進(jìn)行清理工作并優(yōu)雅退出。
/**
* 殺死占用 8080 端口的進(jìn)程
*/
private static void killOldServiceInLinux() {
try {
// 查找占用 8080 端口的進(jìn)程
String command = "lsof -t -i:" + DEFAULT_PORT_8080;
log.info("正在執(zhí)行命令: {}", command);
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String pid;
while ((pid = reader.readLine()) != null) {
// 發(fā)送 SIGINT 信號以優(yōu)雅關(guān)閉
Runtime.getRuntime().exec("kill -2 " + pid);
log.info("Killed process: {}", pid);
}
} catch (IOException e) {
log.error("Failed to stop old service", e);
}
}3. 代碼實現(xiàn)
代碼的核心邏輯在 changePortAndRestart() 方法中實現(xiàn),主要步驟包括停止當(dāng)前 Web 服務(wù)器并重啟。
/**
* 如果端口占用,則嘗試殺死占用8080端口的進(jìn)程,并修改端口并重啟服務(wù)
*
* @param context
*/
private static void changePortAndRestart(ConfigurableApplicationContext context) {
log.info("嘗試殺死占用 8080 端口的進(jìn)程.");
killOldServiceInLinux();
log.info("正在修改端口更改為 {}.", DEFAULT_PORT_8080);
ServletWebServerFactory webServerFactory = context.getBean(ServletWebServerFactory.class);
ServletContextInitializer servletContextInitializer = context.getBean(ServletContextInitializer.class);
WebServer webServer = webServerFactory.getWebServer(servletContextInitializer);
if (webServer != null) {
log.info("停止舊服務(wù)器.");
webServer.stop();
}
//((TomcatServletWebServerFactory) servletContextInitializer).setPort(DEFAULT_PORT_8080);
((TomcatServletWebServerFactory) webServerFactory).setPort(DEFAULT_PORT_8080);
webServer = webServerFactory.getWebServer(servletContextInitializer);
webServer.start();
log.info("新服務(wù)啟動成功.");
}

4. 配置優(yōu)雅關(guān)閉
在 application.yml 中設(shè)置優(yōu)雅關(guān)閉:
server: shutdown: graceful
這個配置允許 Spring Boot 在接收到關(guān)閉請求時,等待當(dāng)前請求完成后再停止服務(wù)。 (因此代碼使用的是kill -2命令)

5. 小結(jié)
通過以上實現(xiàn),我們能夠靈活應(yīng)對端口占用問題,并提升開發(fā)效率。熱部署功能不僅依賴于 Spring Boot 提供的豐富 API,還需要結(jié)合操作系統(tǒng)特性,以確保在生產(chǎn)環(huán)境中的穩(wěn)定性和可用性。
附帶Window關(guān)閉端口程序代碼
(Window關(guān)閉程序后可能得需要sleep一下,不然還會顯示端口占用)
private static void killOldServiceInWindows() {
try {
// 查找占用 8080 端口的進(jìn)程 ID
ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/c", "netstat -ano | findstr :8080");
Process process = builder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.trim().split("\\s+");
if (parts.length > 4) {
String pid = parts[parts.length - 1];
// 殺死該進(jìn)程
Runtime.getRuntime().exec("taskkill /F /PID " + pid);
log.info("Killed process: {}", pid);
}
}
} catch (IOException e) {
log.error("Failed to stop old service", e);
}
}以上就是SpringBoot項目請求不中斷動態(tài)更新代碼的實現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot不中斷更新代碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java8的DateTimeFormatter與SimpleDateFormat的區(qū)別詳解
這篇文章主要介紹了Java8的DateTimeFormatter與SimpleDateFormat的區(qū)別詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Java關(guān)鍵字this使用方法詳細(xì)講解(通俗易懂)
這篇文章主要介紹了Java關(guān)鍵字this使用方法的相關(guān)資料,Java關(guān)鍵字this主要用于在方法體內(nèi)引用當(dāng)前對象,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-01-01
Java工程的Resources目錄從基礎(chǔ)到高級應(yīng)用深入探索
這篇文章主要介紹了Java工程中的resources目錄,從基礎(chǔ)概念到高級應(yīng)用,涵蓋了如何創(chuàng)建、使用以及資源文件的加載方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-01-01
MyBatis基礎(chǔ)支持DataSource實現(xiàn)源碼解析
這篇文章主要為大家介紹了MyBatis基礎(chǔ)支持DataSource實現(xiàn)源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02

