SpringBoot中3种优雅停机的实现方式

引言

应用的启停是一个常见操作。然而,突然终止一个正在运行的应用可能导致正在处理的请求失败、数据不一致等问题。优雅停机(Graceful Shutdown)是指应用在接收到停止信号后,能够妥善处理现有请求、释放资源,然后再退出的过程。本文将详细介绍SpringBoot中实现优雅停机的三种方式。

什么是优雅停机?

优雅停机指的是应用程序在收到停止信号后,不是立即终止,而是遵循以下步骤有序退出:

  1. 停止接收新的请求
  2. 等待正在处理的请求完成
  3. 关闭各种资源连接(数据库连接池、线程池、消息队列连接等)
  4. 完成必要的清理工作
  5. 最后退出应用

优雅停机的核心价值在于:

  • 提高用户体验,避免请求突然中断
  • 保障数据一致性,防止数据丢失

方式一:SpringBoot内置的优雅停机支持

原理与支持版本

从Spring Boot 2.3版本开始,框架原生支持优雅停机机制。这是最简单且官方推荐的实现方式。

当应用接收到停止信号(如SIGTERM)时,内嵌的Web服务器(如Tomcat、Jetty或Undertow)会执行以下步骤:

  1. 停止接收新的连接请求
  2. 设置现有连接的keepalive为false
  3. 等待所有活跃请求处理完成或超时
  4. 关闭应用上下文和相关资源

配置方法

application.propertiesapplication.yml中添加简单配置即可启用:

yaml 复制代码
server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

这里的timeout-per-shutdown-phase指定了等待活跃请求完成的最大时间,默认为30秒。

实现示例

下面是一个完整的SpringBoot应用示例,启用了优雅停机:

typescript 复制代码
@SpringBootApplication
public class GracefulShutdownApplication {

    private static final Logger logger = LoggerFactory.getLogger(GracefulShutdownApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(GracefulShutdownApplication.class, args);
        logger.info("Application started");
    }
    
    @RestController
    @RequestMapping("/api")
    static class SampleController {
        
        @GetMapping("/quick")
        public String quickRequest() {
            return "Quick response";
        }
        
        @GetMapping("/slow")
        public String slowRequest() throws InterruptedException {
            // 模拟长时间处理的请求
            logger.info("Start processing slow request");
            Thread.sleep(10000); // 10秒
            logger.info("Finished processing slow request");
            return "Slow response completed";
        }
    }
    
    @Bean
    public ApplicationListener<ContextClosedEvent> contextClosedEventListener() {
        return event -> logger.info("Received spring context closed event - shutting down");
    }
}

测试验证

  1. 启动应用
  2. 发起一个长时间运行的请求:curl http://localhost:8080/api/slow
  3. 在处理过程中,向应用发送SIGTERM信号:kill -15 <进程ID>,如果是IDEA开发环境,可以点击一次停止服务按钮
  4. 观察日志输出:应该能看到应用等待长请求处理完成后才关闭

优缺点

优点:

  • 配置简单,官方原生支持
  • 无需额外代码,维护成本低
  • 适用于大多数Web应用场景
  • 与Spring生命周期事件完美集成

缺点:

  • 仅支持Spring Boot 2.3+版本
  • 对于超出HTTP请求的场景(如长时间运行的作业)需要额外处理
  • 灵活性相对较低,无法精细控制停机流程
  • 只能设置统一的超时时间

适用场景

  • Spring Boot 2.3+版本的Web应用
  • 请求处理时间可预期,不会有超长时间运行的请求
  • 微服务架构中的标准服务

方式二:使用Actuator端点实现优雅停机

原理与实现

Spring Boot Actuator提供了丰富的运维端点,其中包括shutdown端点,可用于触发应用的优雅停机。这种方式的独特之处在于它允许通过HTTP请求触发停机过程,适合需要远程操作的场景。

配置步骤

  1. 添加Actuator依赖:
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 启用并暴露shutdown端点:
yaml 复制代码
management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: shutdown
      base-path: /management
  server:
    port: 8081  # 可选:为管理端点设置单独端口

使用方法

通过HTTP POST请求触发停机:

bash 复制代码
curl -X POST http://localhost:8081/management/shutdown

请求成功后,会返回类似如下响应:

json 复制代码
{
  "message": "Shutting down, bye..."
}

安全性考虑

由于shutdown是一个敏感操作,必须考虑安全性:

yaml 复制代码
spring:
  security:
    user:
      name: admin
      password: secure_password
      roles: ACTUATOR

management:
  endpoints:
    web:
      exposure:
        include: shutdown
  endpoint:
    shutdown:
      enabled: true

# 配置端点安全
management.endpoints.web.base-path: /management

使用安全配置后的访问方式:

perl 复制代码
curl -X POST http://admin:secure_password@localhost:8080/management/shutdown

完整实现示例

typescript 复制代码
@SpringBootApplication
@EnableWebSecurity
public class ActuatorShutdownApplication {

    private static final Logger logger = LoggerFactory.getLogger(ActuatorShutdownApplication.class);

    public static void main(String[] args) {
        SpringApplication.run(ActuatorShutdownApplication.class, args);
        logger.info("Application started with Actuator shutdown enabled");
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests()
            .requestMatchers("/management/**").hasRole("ACTUATOR")
            .anyRequest().permitAll()
            .and()
            .httpBasic();
        
        return http.build();
    }
    
    @RestController
    static class ApiController {
        
        @GetMapping("/api/hello")
        public String hello() {
            return "Hello, world!";
        }
    }
    
    @Bean
    public ApplicationListener<ContextClosedEvent> shutdownListener() {
        return event -> {
            logger.info("Received shutdown signal via Actuator");
            
            // 等待活跃请求完成
            logger.info("Waiting for active requests to complete...");
            try {
                Thread.sleep(5000); // 简化示例,实际应监控活跃请求
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            logger.info("All requests completed, shutting down");
        };
    }
}

优缺点

优点:

  • 可以通过HTTP请求远程触发停机
  • 适合管理工具和运维脚本集成
  • 可以与Spring Security集成实现访问控制
  • 支持所有Spring Boot版本(包括2.3之前的版本)

缺点:

  • 需要额外配置和依赖
  • 潜在的安全风险,需谨慎保护端点
  • 对于内部复杂资源的关闭需要额外编码

适用场景

  • 需要通过HTTP请求触发停机的场景
  • 使用运维自动化工具管理应用的部署
  • 集群环境中需要按特定顺序停止服务
  • 内部管理系统需要直接控制应用生命周期

方式三:自定义ShutdownHook实现优雅停机

原理与实现

JVM提供了ShutdownHook机制,允许在JVM关闭前执行自定义逻辑。通过注册自定义的ShutdownHook,我们可以实现更加精细和灵活的优雅停机控制。这种方式的优势在于可以精确控制资源释放顺序,适合有复杂资源管理需求的应用。

基本实现步骤

  1. 创建自定义的ShutdownHandler类
  2. 注册JVM ShutdownHook
  3. 在Hook中实现自定义的优雅停机逻辑

完整实现示例

以下是一个包含详细注释的完整示例:

scss 复制代码
@SpringBootApplication
public class CustomShutdownHookApplication {

    private static final Logger logger = LoggerFactory.getLogger(CustomShutdownHookApplication.class);

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(CustomShutdownHookApplication.class, args);
        
        // 注册自定义ShutdownHook
        registerShutdownHook(context);
        
        logger.info("Application started with custom shutdown hook");
    }
    
    private static void registerShutdownHook(ConfigurableApplicationContext context) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            logger.info("Executing custom shutdown hook");
            try {
                // 1. 停止接收新请求(如果是Web应用)
                if (context.containsBean("tomcatServletWebServerFactory")) {
                    TomcatServletWebServerFactory server = context.getBean(TomcatServletWebServerFactory.class);
                    logger.info("Stopping web server to reject new requests");
                    // 注意: 实际应用中需要找到正确方式停止特定Web服务器
                }
                
                // 2. 等待活跃请求处理完成
                logger.info("Waiting for active requests to complete");
                // 这里可以添加自定义等待逻辑,如检查活跃连接数或线程状态
                Thread.sleep(5000); // 简化示例
                
                // 3. 关闭自定义线程池
                shutdownThreadPools(context);
                
                // 4. 关闭消息队列连接
                closeMessageQueueConnections(context);
                
                // 5. 关闭数据库连接池
                closeDataSourceConnections(context);
                
                // 6. 执行其他自定义清理逻辑
                performCustomCleanup(context);
                
                // 7. 最后关闭Spring上下文
                logger.info("Closing Spring application context");
                context.close();
                
                logger.info("Graceful shutdown completed");
            } catch (Exception e) {
                logger.error("Error during graceful shutdown", e);
            }
        }, "GracefulShutdownHook"));
    }
    
    private static void shutdownThreadPools(ApplicationContext context) {
        logger.info("Shutting down thread pools");
        
        // 获取所有ExecutorService类型的Bean
        Map<String, ExecutorService> executors = context.getBeansOfType(ExecutorService.class);
        
        executors.forEach((name, executor) -> {
            logger.info("Shutting down executor: {}", name);
            executor.shutdown();
            try {
                // 等待任务完成
                if (!executor.awaitTermination(15, TimeUnit.SECONDS)) {
                    logger.warn("Executor did not terminate in time, forcing shutdown: {}", name);
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.warn("Interrupted while waiting for executor shutdown: {}", name);
                executor.shutdownNow();
            }
        });
    }
    
    private static void closeMessageQueueConnections(ApplicationContext context) {
        logger.info("Closing message queue connections");
        
        // 示例:关闭RabbitMQ连接
        if (context.containsBean("rabbitConnectionFactory")) {
            try {
                ConnectionFactory rabbitFactory = context.getBean(ConnectionFactory.class);
                // 适当地关闭连接
                logger.info("Closed RabbitMQ connections");
            } catch (Exception e) {
                logger.error("Error closing RabbitMQ connections", e);
            }
        }
        
        // 示例:关闭Kafka连接
        if (context.containsBean("kafkaConsumerFactory")) {
            try {
                // 关闭Kafka连接的代码
                logger.info("Closed Kafka connections");
            } catch (Exception e) {
                logger.error("Error closing Kafka connections", e);
            }
        }
    }
    
    private static void closeDataSourceConnections(ApplicationContext context) {
        logger.info("Closing datasource connections");
        
        // 获取所有DataSource类型的Bean
        Map<String, DataSource> dataSources = context.getBeansOfType(DataSource.class);
        
        dataSources.forEach((name, dataSource) -> {
            try {
                // 对于HikariCP连接池
                if (dataSource instanceof HikariDataSource) {
                    ((HikariDataSource) dataSource).close();
                    logger.info("Closed HikariCP datasource: {}", name);
                }
                // 可以添加其他类型连接池的关闭逻辑
                else {
                    // 尝试通过反射调用close方法
                    Method closeMethod = dataSource.getClass().getMethod("close");
                    closeMethod.invoke(dataSource);
                    logger.info("Closed datasource: {}", name);
                }
            } catch (Exception e) {
                logger.error("Error closing datasource: {}", name, e);
            }
        });
    }
    
    private static void performCustomCleanup(ApplicationContext context) {
        // 这里可以添加应用特有的清理逻辑
        logger.info("Performing custom cleanup tasks");
        
        // 例如:保存应用状态
        // 例如:释放本地资源
        // 例如:发送关闭通知给其他系统
    }
    
    @Bean
    public ExecutorService applicationTaskExecutor() {
        return Executors.newFixedThreadPool(10);
    }
    
    @RestController
    @RequestMapping("/api")
    static class ApiController {
        
        @Autowired
        private ExecutorService applicationTaskExecutor;
        
        @GetMapping("/task")
        public String submitTask() {
            applicationTaskExecutor.submit(() -> {
                try {
                    logger.info("Task started, will run for 30 seconds");
                    Thread.sleep(30000);
                    logger.info("Task completed");
                } catch (InterruptedException e) {
                    logger.info("Task interrupted");
                    Thread.currentThread().interrupt();
                }
            });
            return "Task submitted";
        }
    }
}

优缺点

优点:

  • 最大的灵活性和可定制性
  • 可以精确控制资源关闭顺序和方式
  • 适用于复杂应用场景和所有Spring Boot版本
  • 可以处理Spring框架无法管理的外部资源

缺点:

  • 实现复杂度高,需要详细了解应用资源
  • 维护成本较高
  • 容易出现资源关闭顺序错误导致的问题

适用场景

  • 具有复杂资源管理需求的应用
  • 需要特定顺序关闭资源的场景
  • 使用Spring Boot早期版本(不支持内置优雅停机)
  • 非Web应用或混合应用架构
  • 使用了Spring框架不直接管理的资源(如Native资源)

方案对比和选择指南

下面是三种方案的对比表格,帮助您选择最适合自己场景的实现方式:

特性/方案 SpringBoot内置 Actuator端点 自定义ShutdownHook
实现复杂度
灵活性
可定制性
框架依赖 Spring Boot 2.3+ 任何Spring Boot版本 任何Java应用
额外依赖 Actuator
触发方式 系统信号(SIGTERM) HTTP请求 系统信号或自定义
安全性考虑 高(需要保护端点)
维护成本
适用Web应用 最适合 适合 适合
适用非Web应用 部分适合 部分适合 最适合
  1. 选择SpringBoot内置方案,如果:

    • 使用Spring Boot 2.3+版本
    • 主要是标准Web应用
    • 没有特殊的资源管理需求
    • 希望最简单的配置
  2. 选择Actuator端点方案,如果:

    • 需要通过HTTP请求触发停机
    • 使用早期Spring Boot版本
    • 集成了运维自动化工具
    • 已经在使用Actuator进行监控
  3. 选择自定义ShutdownHook方案,如果:

    • 有复杂的资源管理需求
    • 需要精确控制停机流程
    • 使用了Spring框架不直接管理的资源
    • 混合架构应用(既有Web又有后台作业)

结论

优雅停机是保障应用可靠性和用户体验的重要实践。SpringBoot提供了多种实现方式,从简单的配置到复杂的自定义实现,可以满足不同应用场景的需求。

  • 对于简单应用:SpringBoot内置方案是最佳选择,配置简单,足够满足大多数Web应用需求
  • 对于需要远程触发的场景:Actuator端点提供了HTTP接口控制,便于集成运维系统
  • 对于复杂应用:自定义ShutdownHook提供了最大的灵活性,可以精确控制资源释放顺序和方式

无论选择哪种方式,优雅停机都应该成为微服务设计的标准实践。正确实现优雅停机,不仅能提升系统稳定性,还能改善用户体验,减少因应用重启或降级带来的业务中断。

相关推荐
㳺三才人子5 小时前
初探 Flask
后端·python·flask·html
星栈独行5 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Lei活在当下5 小时前
先用起来,再理解,关于协程Coroutine应该知道的事
android·java·jvm
Java爱好狂.5 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易6 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
tongluowan0076 小时前
以ReentrantLock为例解释AQS的工作流程
java·模板方法模式·aqs·reentrantlock
装不满的克莱因瓶6 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl7 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
身如柳絮随风扬7 小时前
Java 项目打包与部署完全指南:JAR vs WAR,从构建到运行
java·firefox·jar
云烟成雨TD7 小时前
Spring AI Alibaba 1.x 系列【62】时光旅行(Time-Travel)
java·人工智能·spring