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)

相关推荐
岁岁岁平安1 天前
Java+SpringBoot+Dubbo+Nacos快速入门
java·spring boot·nacos·rpc·dubbo
学习编程的Kitty1 天前
算法——位运算
java·前端·算法
用户904706683571 天前
如何使用 Spring MVC 实现 RESTful API 接口
java·后端
刘某某.1 天前
数组和小于等于k的最长子数组长度b
java·数据结构·算法
程序员飞哥1 天前
真正使用的超时关单策略是什么?
java·后端·面试
用户904706683571 天前
SpringBoot 多环境配置与启动 banner 修改
java·后端
小old弟1 天前
后端三层架构
java·后端
花花鱼1 天前
spring boot 2.x 与 spring boot 3.x 及对应Tomcat、Jetty、Undertow版本的选择(理论)
java·后端
温柔一只鬼.1 天前
Docker快速入门——第二章Docker基本概念
java·docker·容器
要争气1 天前
5 二分查找算法应用
java·数据结构·算法