更新代码方式为 kill 老进程 -> 启动新进程;
主要为其他端口启动后,kill 了 8080 端口,然后在创建一个新容器监听 8080,再关闭 9090 端口的容器。
mermaid
flowchart TD
Start --> 是否占用
是否占用 --> |未占用| 直接启动8080 --> stopno(stop)
是否占用 --> |占用| 其他端口9090启动 --> |Spring Boot,启动!| 等待启动完毕 --> |通过sh/cmd命令| 等待8080所在进程的终止 --> |获取TomcatServletWebServerFactory实例| 创建新的容器实例 --> |设置8080端口并通过反射拿到 ServletWebServerApplicationContext 也就是端口 8080 的 run| 获取8080的WebServer调用start方法启动 --> 获取端口9090run的WebServer方法调用stop方法停止 --> stop
整个过程就是给容器换了个端口,不影响程序的正常访问,并且杀程序到替换端口也就不到 1s 的时间,基本来说不影响用户使用!
java
package com.likebocai.notStopUpdate;
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.context.ServletWebServerApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
@SpringBootApplication
public class NotStopUpdate {
public static void main(String[] args) {
int port = 8080;
boolean needChangePort = false;
args = new String[args.length + 1];
args[args.length - 1] = "--server.port=" + port;
if (isPortInUse(port)) {
args[args.length - 1] = "--server.port=9090";
needChangePort = true;
}
ConfigurableApplicationContext run = SpringApplication.run(NotStopUpdate.class, args);
if (needChangePort) {
// linux 系统杀死老端口的命令
// String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort);
// win 系统下杀死老端口的命令
String command = String.format("for /f "tokens=5" %%i in ('netstat -aon ^| findstr :%d') do @taskkill /F /PID %%i", port);
System.out.println("Running command: " + command);
try {
// 注意区分
// linux 系统杀死老端口的命令
// int i = Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor();
// win 系统下杀死老端口的命令
int i = Runtime.getRuntime().exec(new String[]{"cmd", "/c", command}).waitFor();
// 获取TomcatServletWebServerFactory实例
TomcatServletWebServerFactory tomcatWebServerFactory = run.getBeanFactory().getBean("tomcatServletWebServerFactory", TomcatServletWebServerFactory.class);
// 设置端口8080
tomcatWebServerFactory.setPort(port);
// 获取 getWebServer(ServletContextInitializer servletContextInitializer);源码如下private ServletContextInitializer getSelfInitializer() {return this::selfInitializer;}可以看到spring并没有中将ServletContextInitializer暴露出来,这里通过上下文然后用反射拿到,我封装一个方法到下面
WebServer webServer = tomcatWebServerFactory.getWebServer(invokeSelfInitialize(((ServletWebServerApplicationContext) run)));
// 开启 8080 端口
webServer.start();
// 关闭 9090 端口的监听,防止下一个jar运行时8080占用,9090也占用的问题。
((ServletWebServerApplicationContext) run).getWebServer().stop();
} catch (IOException | InterruptedException ignored) {
System.out.println("IOException | InterruptedException ignored");
}
}
}
private static ServletContextInitializer invokeSelfInitialize(ServletWebServerApplicationContext context) {
try {
Method method = ServletWebServerApplicationContext.class.getDeclaredMethod("getSelfInitializer");
method.setAccessible(true);
return (ServletContextInitializer) method.invoke(context);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private static boolean isPortInUse(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
return false;
} catch (IOException e) {
return true;
}
}
}