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)

相关推荐
a8a30215 分钟前
Laravel9.x新特性全解析
运维·spring boot·nginx
counting money44 分钟前
Spring框架基础(配置篇)
java·后端·spring
秋91 小时前
OceanBase与GreatSQL在Java应用中的性能调优方法有哪些?
java·开发语言·oceanbase
今天又在写代码1 小时前
并发问题解决
java·开发语言·数据库
老王以为2 小时前
前端视角下的 Java
java·javascript·程序员
看腻了那片水2 小时前
开源一个对业务代码零侵入的透明数据治理框架 —— 【sangsang】
java·mybatis
Nyarlathotep01132 小时前
JUC工具(3):StampedLock的基础和原理
java·后端
呱牛do it2 小时前
企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 7)
java·vue
NE_STOP2 小时前
Redis--SDS字符串与集合的底层实现原理
java