1. 代码概述
我们实现了一个简单的 spring boot 应用程序,它可以自动检测端口是否被占用,并在必要时切换到备用端口,然后再将目标端口程序关闭再将备用端口切换为目标端口。具体功能包括:
- 检查默认端口(8080)是否被占用。
- 如果被占用,自动切换到备用端口(8086)。
- 在 linux 系统下,优雅地关闭占用该端口的进程。
- 修改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("启动参数: {}", arrays.tostring(newargs)); //去除newargs的null数据 newargs = arrays.stream(newargs).filter(objects::nonnull).toarray(string[]::new); configurableapplicationcontext context = springapplication.run(mybatisdemoapplication.class, newargs); //判断是否是linux系统,如果是linux系统,则尝试杀死占用8080端口的进程 system.out.println("是否需要修改端口: "+isneedchangeport); if (isneedchangeport && islinuxos()) { changeportandrestart(context); } } /** * 如果端口占用,则尝试杀死占用8080端口的进程,并修改端口并重启服务 * * @param context */ private static void changeportandrestart(configurableapplicationcontext context) { log.info("尝试杀死占用 8080 端口的进程."); 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("停止旧服务器."); webserver.stop(); } //((tomcatservletwebserverfactory) servletcontextinitializer).setport(default_port_8080); ((tomcatservletwebserverfactory) webserverfactory).setport(default_port_8080); webserver = webserverfactory.getwebserver(servletcontextinitializer); webserver.start(); log.info("新服务启动成功."); } /** * 杀死占用 8080 端口的进程 */ private static void killoldserviceinlinux() { try { // 查找占用 8080 端口的进程 string command = "lsof -t -i:" + default_port_8080; log.info("正在执行命令: {}", command); process process = runtime.getruntime().exec(command); bufferedreader reader = new bufferedreader(new inputstreamreader(process.getinputstream())); string pid; while ((pid = reader.readline()) != null) { // 发送 sigint 信号以优雅关闭 runtime.getruntime().exec("kill -2 " + pid); log.info("killed process: {}", pid); } } catch (ioexception e) { log.error("failed to stop old service", e); } } /** * 判断是否是linux系统 * * @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. 主要功能
检测端口状态
通过 portutil.isportinuse()
检查默认端口的使用状态。如果端口被占用,修改启动参数。
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; } } }
修改启动参数
当发现端口被占用时,我们动态调整启动参数,以便在启动时使用新的端口。
if (isneedchangeport) { log.info("端口 {} 正在使用中, 正在尝试端口切换到 {}.", default_port_8080, alternate_port_8086); newargs[newargs.length - 1] = "--server.port=" + alternate_port_8086; }
优雅关闭
在 linux 系统中,如果检测到端口被占用,调用 killoldserviceinlinux()
方法,优雅地关闭占用该端口的进程。这是通过发送 sigint
信号实现的,允许应用程序进行清理工作并优雅退出。
/** * 杀死占用 8080 端口的进程 */ private static void killoldserviceinlinux() { try { // 查找占用 8080 端口的进程 string command = "lsof -t -i:" + default_port_8080; log.info("正在执行命令: {}", command); process process = runtime.getruntime().exec(command); bufferedreader reader = new bufferedreader(new inputstreamreader(process.getinputstream())); string pid; while ((pid = reader.readline()) != null) { // 发送 sigint 信号以优雅关闭 runtime.getruntime().exec("kill -2 " + pid); log.info("killed process: {}", pid); } } catch (ioexception e) { log.error("failed to stop old service", e); } }
3. 代码实现
代码的核心逻辑在 changeportandrestart()
方法中实现,主要步骤包括停止当前 web 服务器并重启。
/** * 如果端口占用,则尝试杀死占用8080端口的进程,并修改端口并重启服务 * * @param context */ private static void changeportandrestart(configurableapplicationcontext context) { log.info("尝试杀死占用 8080 端口的进程."); 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("停止旧服务器."); webserver.stop(); } //((tomcatservletwebserverfactory) servletcontextinitializer).setport(default_port_8080); ((tomcatservletwebserverfactory) webserverfactory).setport(default_port_8080); webserver = webserverfactory.getwebserver(servletcontextinitializer); webserver.start(); log.info("新服务启动成功."); }
4. 配置优雅关闭
在 application.yml
中设置优雅关闭:
server: shutdown: graceful
这个配置允许 spring boot 在接收到关闭请求时,等待当前请求完成后再停止服务。 (因此代码使用的是kill -2命令)
5. 小结
通过以上实现,我们能够灵活应对端口占用问题,并提升开发效率。热部署功能不仅依赖于 spring boot 提供的丰富 api,还需要结合操作系统特性,以确保在生产环境中的稳定性和可用性。
附带window关闭端口程序代码
(window关闭程序后可能得需要sleep一下,不然还会显示端口占用)
private static void killoldserviceinwindows() { try { // 查找占用 8080 端口的进程 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]; // 杀死该进程 runtime.getruntime().exec("taskkill /f /pid " + pid); log.info("killed process: {}", pid); } } } catch (ioexception e) { log.error("failed to stop old service", e); } }
以上就是springboot项目请求不中断动态更新代码的实现的详细内容,更多关于springboot不中断更新代码的资料请关注代码网其它相关文章!
发表评论