You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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秒后再关闭,请问哪里操作出错了?


问题原因与解决方案

核心原因

  1. 主线程被阻塞:你的CommandLineRunner的run方法直接在Spring应用的主线程中执行,当收到停机信号时,JVM会中断主线程的sleep操作,抛出InterruptedException,导致run方法直接结束,应用随即退出,没有机会执行优雅等待逻辑。
  2. 配置的误解: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 01:52:38