SpringBoot优雅关机
1. 启用优雅关机
在 application.yml
中启用优雅关机,并设置最大等待时间:
server:shutdown: graceful # 启用优雅关机
spring:lifecycle:timeout-per-shutdown-phase: 30s # 设置优雅关机的最大等待时间
2. 创建一个长时间运行的任务
编写一个简单的服务类,模拟一个阻塞的任务:
import org.springframework.stereotype.Service;@Service
public class LongRunningTaskService {public void executeLongRunningTask() {System.out.println("Long running task started at: " + System.currentTimeMillis());try {// 模拟任务执行 10 秒Thread.sleep(10000);} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println("Long running task was interrupted.");}System.out.println("Long running task finished at: " + System.currentTimeMillis());}
}
3. 创建控制器触发任务
编写一个控制器,用于手动触发上述任务:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class TaskController {private final LongRunningTaskService longRunningTaskService;public TaskController(LongRunningTaskService longRunningTaskService) {this.longRunningTaskService = longRunningTaskService;}@GetMapping("/start-task")public String startTask() {new Thread(longRunningTaskService::executeLongRunningTask).start();return "Task started successfully.";}
}
一、通过监听 ContextClosedEvent
import org.quartz.Scheduler;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;@Component
public class QuartzGracefulShutdown implements ApplicationListener<ContextClosedEvent> {private final Scheduler scheduler;public QuartzGracefulShutdown(Scheduler scheduler) {this.scheduler = scheduler;}@Overridepublic void onApplicationEvent(ContextClosedEvent event) {System.out.println("Starting graceful shutdown for Quartz...");try {// 关闭 Quartz 定时任务scheduler.shutdown(true); // true 表示等待正在运行的任务完成// 检查 Quartz 是否已关闭while (!scheduler.isShutdown()) {System.out.println("Waiting for Quartz tasks to shut down...");Thread.sleep(1000); // 每秒检查一次}System.out.println("All Quartz tasks have been shut down.");} catch (Exception e) {System.err.println("Failed to gracefully shut down Quartz: " + e.getMessage());}}
}
二、
@PreDestroy
@PreDestroy
是 Java 的标准注解,用于标记一个方法在 Bean 被销毁之前执行。它通常用于释放资源或执行清理逻辑。然而,在 Spring Boot 的优雅关机场景中,使用 @PreDestroy
是否合适需要具体分析。
以下是关键点:
-
Spring 生命周期与
@PreDestroy
:- 当 Spring 容器关闭时,所有被管理的 Bean 都会被销毁,
@PreDestroy
注解的方法会在销毁前调用。 - 但是,
@PreDestroy
方法的执行时机是在容器关闭过程中,并不保证与 Spring Boot 的优雅关机机制(如 HTTP 请求的优雅处理或线程池的等待)完全同步。
- 当 Spring 容器关闭时,所有被管理的 Bean 都会被销毁,
-
适用场景:
- 如果您的目标是关闭一些资源(如定时任务、数据库连接等),
@PreDestroy
是可以实现的。 - 但如果需要更复杂的逻辑(如阻塞等待任务完成),
@PreDestroy
可能不够灵活。
- 如果您的目标是关闭一些资源(如定时任务、数据库连接等),
使用 @PreDestroy
实现优雅关机
1. 基本实现
我们可以通过 @PreDestroy
注解的方法来关闭 Quartz 定时任务,并检查是否关闭成功。
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;@Component
public class QuartzShutdownHandler {@PreDestroypublic void shutdown() {System.out.println("Starting graceful shutdown via @PreDestroy...");}
}
在 Spring Boot 中,ContextClosedEvent
和 @PreDestroy
都可以用于在应用程序关闭时执行清理逻辑。然而,它们的作用范围、触发时机和使用场景有所不同。以下是两者的详细对比和适用场景分析。
1. ContextClosedEvent
定义
ContextClosedEvent
是 Spring 的生命周期事件之一,当 Spring 容器关闭时触发。- 它是 Spring 的事件机制的一部分,可以通过实现
ApplicationListener<ContextClosedEvent>
或使用@EventListener
注解来监听。
触发时机
- 在 Spring 容器关闭的过程中触发,具体是在
ApplicationContext
调用close()
方法后。 - 触发顺序早于
@PreDestroy
,因为它是容器级别的事件。
特点
- 全局性:适用于需要在整个应用程序范围内执行清理逻辑的场景。
- 灵活性:可以通过监听多个事件(如
ContextRefreshedEvent
、ContextStartedEvent
等)实现更复杂的逻辑。 - 可扩展性:可以结合其他 Spring 组件(如线程池、定时任务等)实现优雅关机。
示例代码
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;@Component
public class ShutdownListener implements ApplicationListener<ContextClosedEvent> {@Overridepublic void onApplicationEvent(ContextClosedEvent event) {System.out.println("ContextClosedEvent triggered: Shutting down...");// 执行清理逻辑,例如关闭线程池或定时任务}
}
2. @PreDestroy
定义
@PreDestroy
是 Java 标准注解,用于标记一个方法在 Bean 被销毁之前执行。- 它是 JSR-250 规范的一部分,Spring 支持该注解。
触发时机
- 在 Spring 容器关闭时,针对每个被管理的 Bean,调用其标注了
@PreDestroy
的方法。 - 触发顺序晚于
ContextClosedEvent
,因为它是 Bean 级别的销毁逻辑。
特点
- 局部性:适用于单个 Bean 的清理逻辑,例如释放资源(数据库连接、文件句柄等)。
- 简单性:无需额外配置,直接在 Bean 的方法上添加注解即可。
- 依赖性:只能用于 Spring 管理的 Bean,无法处理非 Spring 管理的组件。
示例代码
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;@Component
public class ResourceCleanup {@PreDestroypublic void cleanup() {System.out.println("@PreDestroy triggered: Cleaning up resources...");// 执行清理逻辑,例如关闭文件或释放内存}
}
3. 对比总结
特性 | ContextClosedEvent | @PreDestroy |
---|---|---|
触发时机 | Spring 容器关闭时,早于 @PreDestroy | Bean 销毁时,晚于 ContextClosedEvent |
作用范围 | 全局(整个应用程序上下文) | 局部(单个 Bean) |
灵活性 | 高,可以监听多个事件并执行复杂逻辑 | 低,仅限于单个 Bean 的清理逻辑 |
适用场景 | 优雅关机、关闭线程池、等待任务完成等全局操作 | 关闭资源、释放文件句柄等局部操作 |
依赖性 | 不依赖特定 Bean,适用于全局逻辑 | 必须是 Spring 管理的 Bean |
扩展性 | 可以结合其他 Spring 功能(如事件机制) | 仅限于简单的销毁逻辑 |
4. 使用场景推荐
适合使用 ContextClosedEvent
的场景
-
优雅关机:
- 需要等待线程池中的任务完成。
- 需要关闭 Quartz 定时任务或其他后台线程。
- 示例:等待所有 HTTP 请求完成后再关闭服务。
-
全局资源清理:
- 需要在整个应用程序范围内执行清理逻辑。
- 示例:关闭数据库连接池、释放共享资源。
-
多组件协调:
- 需要协调多个 Bean 的关闭顺序。
- 示例:先关闭 A 组件,再关闭 B 组件。
适合使用 @PreDestroy
的场景
-
单个 Bean 的资源释放:
- 需要释放某个 Bean 的独占资源。
- 示例:关闭文件句柄、释放网络连接。
-
轻量级清理逻辑:
- 清理逻辑简单且不需要与其他组件交互。
- 示例:清除缓存、重置状态。
5. 实际应用中的选择
在实际开发中,可以根据需求选择合适的机制:
- 如果需要实现优雅关机或全局清理逻辑,优先使用
ContextClosedEvent
。 - 如果只需要为单个 Bean 执行简单的清理逻辑,使用
@PreDestroy
更加方便。
注:K8S配置:terminationGracePeriodSeconds: 120