Spring Boot 应用优雅关闭

写这篇文章是因为看到 "线程池在使用结束后应该正确关闭." 那么如果我们的 Spring 应用都无法正确关闭, 那么线程池肯定也无从保障

1. 优雅关闭

  • kill with pid, without -9

大多数情况下无须在意这个问题, 正确使用 kill 命令关闭就行 (注意不能使用 kill -9)

java 复制代码
kill $(cat ./application.pid)

默认发送的信号是 SIGTERM,其信号编号为 15. 在无法正常终止进程时使用 -9 (SIGKILL): 强制立即终止进程。这个信号无法被捕获或忽略,进程会被操作系统强制杀死

  • 在启动的时候, 将 pid 和 端口号 记录到文件中 (这些文件在程序关闭后会自动删除)
java 复制代码
public static void main(String[] args) {
    try {
        long start = System.currentTimeMillis();
        SpringApplicationBuilder builder = new SpringApplicationBuilder(App.class);
        builder.beanNameGenerator(FullyQualifiedAnnotationBeanNameGenerator.INSTANCE);
        // startup with pid file and port file
        builder.listeners(new ApplicationPidFileWriter()); // pid file
        // 上面的类可以传入文件路径, 方便使用 kill 命令
        // builder.listeners(new ApplicationPidFileWriter("./bin/shutdown.pid")); // kill $(cat ./bin/shutdown.pid)
        builder.listeners(new WebServerPortFileWriter()); // port file
        builder.run(args);
        long costs = (System.currentTimeMillis() - start) / 1000;
        log.info("### APP STARTED ### It take {} seconds.", costs);
    } catch (Exception e) { log.error(" STARTUP FAILED ", e); }
}

2. 增加 URL 接口来关闭程序

  1. 使用 ConfigurableApplicationContext#close
  2. 使用 int exit = SpringApplication.exit(context);
  3. 关闭应用时执行某段逻辑
    1. 注解 @PreDestroy
    2. 事件 ContextClosedEvent
java 复制代码
@Slf4j
@RestController
public class ShutDownController {
    @Autowired
    private ConfigurableApplicationContext applicationContext;
    @Autowired
    private ApplicationContext context; // 两个 context 都行

    @GetMapping("/shutdown")
    public String shutdown() {
        log.error("1. shutdown begin..."); // 前面的编号是执行顺序
        // 清理资源并关闭
        ((ConfigurableApplicationContext) context).close();
        // applicationContext.close();
        log.error("3. spring application closed.");
        // 在嵌入式应用或在某些情况下,关闭上下文后,JVM 可能不会自动退出。为了确保应用完全终止,使用 System.exit(0)
        System.exit(0); // not necessary
        // log.error("system closed."); // 这里日志对象 log 已经失效, 无法打印, 换用 println
        System.err.println("system closed.");
        return "shutdown";
    }

    @GetMapping("/exit")
    public String exit() {
        log.error("1. shutdown begin...");
        // 负责清理资源
        int exit = SpringApplication.exit(context);
        log.error("3. Spring app closed.");
        System.exit(exit); // not necessary
        // log.error("system closed."); // It will fail over
        System.err.println("system closed.");
        return "shutdown";
    }

    @PreDestroy // @PreDestroy 注解: 在 Spring 应用销毁前, 执行某段逻辑
    public void onDestroy() throws Exception {
        log.error("3. Spring Container is destroyed!");
    }

    @Slf4j
    @Component // ContextClosedEvent 事件: 在 Spring 上下文关闭时机, 执行某段逻辑
    public static class ApplicationShutdown implements ApplicationListener<ContextClosedEvent> {
        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            log.error("2. graceful shutdown (ContextClosedEvent)");
        }
    }
}

3. 优雅启动

  1. 显示配置参数
java 复制代码
    /**
     * 显示配置参数
     */
    public static void printEnvironment(Environment environment) {
        try {
            log.info("******************JVM参数**********************");
            String[] properties = 
                    {"java.home", "java.version", "java.vm.name", "java.vm.vendor", "os.name", "user.dir"};
            for (String property : properties) {
                log.info("* {} = {}", property, System.getProperty(property));
            }
            MemoryMXBean bean = ManagementFactory.getMemoryMXBean();
            MemoryUsage heapUsage = bean.getHeapMemoryUsage();
            log.info("* 初始内存 = " + heapUsage.getInit() / 1024 / 1024 + "M");
            log.info("* 已使用内存 = " + heapUsage.getUsed() / 1024 / 1024 + "M");
            log.info("* 已提交内存 = " + heapUsage.getCommitted() / 1024 / 1024 + "M");
            log.info("* 最大内存 = " + heapUsage.getMax() / 1024 / 1024 + "M");
        } catch (Exception e) {
            log.error("异常: {}", e.getMessage());
        }
    }
  1. 启动时调用: printEnvironment(builder.context().getEnvironment());

4. 线程池的关闭

  1. 由 Spring 管理的线程池, 关闭 Spring 应用上下文时会自动关闭, 包括 @Async 或 通过 Spring 配置的线程池

  2. 自定义线程池 (未通过 Spring 管理), 在关闭应用时, 需要显式地调用 shutdown() 来确保任务能够正常完成, 避免丢失. 可以利用上面提到的, 关闭应用时执行某段逻辑的方式执行

    1. 注解 @PreDestroy
    2. 事件 ContextClosedEvent

(END)

相关推荐
古法老登瞎折腾5 小时前
大数据智能运维实战(十一):运维 MCP 的工程化——权限、限流、审计与错误处理
java·大数据·运维·spring ai·mcp
智能运维指南6 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
yz_aiks7 小时前
Spring AI 2.1 ChatMemory 已过时用什么替代:迁移到 Session Memory 实战(JDBC 存储与会话压缩)
java·spring boot·大模型·ai agent·spring ai
金銀銅鐵7 小时前
[Java] 用 GUI 展示 EnumMap 增删键值对时的内部变化
java·后端·ai编程
学长毕设7 小时前
基于SpringBoot的上门洗车服务系统设计与实现
java·spring boot·后端
jason成都7 小时前
RTKLIB-Java 多基线联合RTK:FIX率从0%到91.5%的三层根因修复
java·gnss·rtklib·rtklib_java
鱼宵8 小时前
Stream 终端操作一次讲透:collect、count、reduce 怎么选
java·stream·终端操作
白山编程大哥8 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list
Linux运维技术栈8 小时前
CVE-2026-91403 高危漏洞修复:Rocky Linux 9 多版本 JDK 环境清理实操
java·linux·运维
On The Road :8 小时前
Java 多线程详解:从入门到线程安全
java·python·安全