如何实现Spring CommandLineRunner的优雅停机?
如何实现Spring CommandLineRunner的优雅停机?
代码示例
package com.example.demo; import java.util.concurrent.TimeUnit; import javax.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication implements CommandLineRunner { private static Logger log = LoggerFactory .getLogger(DemoApplication.class); @Value("${server.shutdown}") private String serverShutdown; @Value("${spring.lifecycle.timeout-per-shutdown-phase}") private String timeout; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override public void run(String... args) throws InterruptedException { log.info("server.shutdown: " + this.serverShutdown); log.info("spring.lifecycle.timeout-per-shutdown-phase: " + this.timeout); log.info("Starting..."); int i = 1000; while (--i > 0) { log.info("Running..."); TimeUnit.SECONDS.sleep(3); } log.info("Done."); } @PreDestroy public void tearDown() { log.info("Shutting Down!"); } }
运行命令与日志
使用./mvnw spring-boot:run运行后按下Ctrl+C,输出日志:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.7.11) 2023-04-30 13:12:25.575 INFO 17558 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 11.0.17 on WS0 with PID 17558 (/home/asd/temp/java/GracefulShutdown/demo/target/classes started by asd in /home/asd/temp/java/GracefulShutdown/demo) 2023-04-30 13:12:25.577 INFO 17558 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default" 2023-04-30 13:12:25.934 INFO 17558 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.647 seconds (JVM running for 0.903) 2023-04-30 13:12:25.940 INFO 17558 --- [ main] com.example.demo.DemoApplication : server.shutdown: graceful 2023-04-30 13:12:25.941 INFO 17558 --- [ main] com.example.demo.DemoApplication : spring.lifecycle.timeout-per-shutdown-phase: 1m 2023-04-30 13:12:25.941 INFO 17558 --- [ main] com.example.demo.DemoApplication : Starting... 2023-04-30 13:12:25.941 INFO 17558 --- [ main] com.example.demo.DemoApplication : Running... 2023-04-30 13:12:28.942 INFO 17558 --- [ main] com.example.demo.DemoApplication : Running... ^C2023-04-30 13:12:30.597 INFO 17558 --- [ionShutdownHook] com.example.demo.DemoApplication : Shutting Down! [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 7.620 s [INFO] Finished at: 2023-04-30T13:12:30+03:00 [INFO] ------------------------------------------------------------------------
配置文件
server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=1m
问题描述
按下Ctrl+C或发送kill -15 <process id>信号后,应用立即停机,并未按照配置等待60秒后再关闭,请问哪里操作出错了?
问题原因与解决方案
核心原因
- 主线程被阻塞:你的
CommandLineRunner的run方法直接在Spring应用的主线程中执行,当收到停机信号时,JVM会中断主线程的sleep操作,抛出InterruptedException,导致run方法直接结束,应用随即退出,没有机会执行优雅等待逻辑。 - 配置的误解:
server.shutdown=graceful主要作用于Web服务器(如Tomcat)的HTTP请求优雅收尾,对非Web场景的自定义后台任务无效;spring.lifecycle.timeout-per-shutdown-phase是Spring关闭Bean销毁阶段的超时时间,并非让应用收到信号后等待指定时长再启动关闭流程。
修正方案
将后台任务移至独立线程执行,并通过标志位或中断机制实现优雅停止:
package com.example.demo; import java.util.concurrent.TimeUnit; import javax.annotation.PreDestroy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication implements CommandLineRunner { private static Logger log = LoggerFactory.getLogger(DemoApplication.class); @Value("${server.shutdown}") private String serverShutdown; @Value("${spring.lifecycle.timeout-per-shutdown-phase}") private String timeout; // 用volatile保证多线程可见性 private volatile boolean running = true; private Thread taskThread; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Override public void run(String... args) throws InterruptedException { log.info("server.shutdown: " + this.serverShutdown); log.info("spring.lifecycle.timeout-per-shutdown-phase: " + this.timeout); log.info("Starting..."); // 将任务放入独立线程执行,避免阻塞主线程 taskThread = new Thread(() -> { int i = 1000; while (running && --i > 0) { log.info("Running..."); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { // 重置中断状态,保证线程能正确退出 Thread.currentThread().interrupt(); log.info("Task thread interrupted"); break; } } log.info("Task done."); }); taskThread.start(); } @PreDestroy public void tearDown() { log.info("Shutting Down!"); // 标记任务停止 running = false; // 中断线程的sleep操作,加速退出 if (taskThread != null) { taskThread.interrupt(); } // 等待线程结束,配合spring.lifecycle.timeout-per-shutdown-phase的超时设置 try { taskThread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
说明
- 独立线程执行后台任务,避免阻塞主线程,让Spring的优雅停机流程能正常触发Bean的销毁逻辑。
volatile标志位保证多线程间的状态可见性,确保销毁方法能正确通知任务线程停止。- 中断线程的
sleep操作,避免任务线程长时间阻塞在休眠状态,加速优雅退出。 taskThread.join()等待任务线程完成收尾,spring.lifecycle.timeout-per-shutdown-phase会限制这个等待的最长时间,超时后Spring会强制关闭应用。
内容的提问来源于stack exchange,提问作者Nikolai Koudelia
相关产品推荐
相关产品推荐

