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) {}}}@Overridepublic 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 表示进程可能存在,但检查失败}}
}