1.前言
在个人或者企业服务器上,总归有要更新代码的时候,普通的做法必须先终止原来进程,因为新进程和老进程端口是一个,新进程在启动时候,必定会出现端口占用的情况。
2. 痛点
如果此时有大量的用户在访问,但是你的代码又必须要更新,这时如果采用上面的做法(先终止原来进程,重新启动新进程),那么必定会导致一段时间内的用户无法访问,这段时间取决于项目启动速度,在单体应用下,如何解决这一痛点?
3.解决方案
3.1 通过更改nginx的转发地址来实现
一种简单办法是,新代码先用其他端口启动,启动完毕后,更改nginx的转发地址,nginx重启非常快,这样就避免了大量的用户访问失败,最后终止老进程就可以。
但是还是比较麻烦,端口换来换去,即使你写个脚本,也是比较麻烦。
3.2 再springboot项目中通过代码实现
在spring项目中可以使用获取ServletWebServerFactory(webServer的工厂类),可以使用该工厂类的getWebServer()
获取一个Web服务,它有start、stop方法启动和关闭Web服务。
@Slf4j
@SpringBootApplication
public class TargetinfoApplication implements CommandLineRunner {
@Value("${server.port}")
private String serverPort;
public static void main(String[] args) {
//SpringApplication.run(TargetinfoApplication.class, args);
String[] newArgs = args.clone();
int defaultPort = 9088;
boolean needChangePort = false;
if (isPortInUse(defaultPort)) {
newArgs = new String[args.length + 1];
System.arraycopy(args, 0, newArgs, 0, args.length);
newArgs[newArgs.length - 1] = "--server.port=9090";
needChangePort = true;
}
ConfigurableApplicationContext run = SpringApplication.run(TargetinfoApplication.class, newArgs);
if (needChangePort) {
String command = String.format("lsof -i :%d | grep LISTEN | awk '{print $2}' | xargs kill -9", defaultPort);
try {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
processWinCommand(defaultPort);
}
if (os.contains("nix") || os.contains("nux") || os.contains("mac")) {
Runtime.getRuntime().exec(new String[]{"sh", "-c", command}).waitFor();
}
// 等待端口释放
while (isPortInUse(defaultPort)) {
}
// 更改 Web 服务器监听的端口 (先获取webServer工厂,然后设置端口号,最后创建webServer运行程序)
ServletWebServerFactory webServerFactory = getWebServerFactory(run);
//设置Web服务器监听的端口:还是监听原来的端口
((TomcatServletWebServerFactory) webServerFactory).setPort(defaultPort);
//创建并启动新的Web服务器
WebServer webServer = webServerFactory.getWebServer(invokeSelfInitialize(((ServletWebServerApplicationContext) run)));
webServer.start();
// 停止原先的 Web 服务器
((ServletWebServerApplicationContext) run).getWebServer().stop();
} catch (IOException | InterruptedException ignored) {
}
}
}
@Override
public void run(String... args) throws Exception {
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
String hostName = address.getHostName();
log.info("road server Started! [{}<{}:{}>].", hostName, ip, serverPort);
log.info("接口文档访问地址:http://{}:{}/doc.html", ip, serverPort);
}
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;
}
}
private static ServletWebServerFactory getWebServerFactory(ConfigurableApplicationContext context) {
String[] beanNames = context.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
return context.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
/**
* 执行windows命令
* @param port
*/
public static void processWinCommand(int port) {
List<Integer> pids = getListeningPids(port);
terminateProcesses(pids);
}
/**
* 获取占用指定端口的进程的 PID 列表
* @param port
* @return
*/
private static List<Integer> getListeningPids(int port) {
List<Integer> pids = new ArrayList<>();
try {
Process process = new ProcessBuilder("cmd", "/c", "netstat -ano").start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (!line.trim().isEmpty() && line.contains(String.format(":%d", port)) && line.contains("LISTENING")) {
String[] parts = line.trim().split("\\s+");
System.out.println(line);
String pid = parts[4];
System.out.println(pid);
pids.add(Integer.parseInt(pid));
}
}
reader.close();
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return pids;
}
/**
* 终止指定 PID 的进程
* @param pids
*/
private static void terminateProcesses(List<Integer> pids) {
for (Integer pid : pids) {
try {
System.out.println("Terminating process with PID: " + pid);
ProcessBuilder builder = new ProcessBuilder("cmd", "/c", "taskkill", "/F", "/PID", pid.toString());
Process process = builder.start();
process.waitFor();
// 检查进程是否已经终止
System.out.println(checkIfProcessIsRunning(pid));
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 检查指定的进程是否运行 true是 false否
* @param pid
* @return
*/
public static boolean checkIfProcessIsRunning(int pid) {
try {
ProcessBuilder builder = new ProcessBuilder("cmd", "/c", "tasklist /FI \"PID eq " + pid + "\"");
Process process = builder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
boolean found = false;
while ((line = reader.readLine()) != null) {
if (line.contains(Integer.toString(pid))) {
found = true;
break;
}
}
reader.close();
process.waitFor();
return found;
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return true; // 返回 true 表示进程可能存在,但检查失败
}
}
}