一、前言

优雅停机就是向应用进程发出停止指令之后,能保证正在执行的业务操作不受影响,直到操作运行完毕之后再停止服务。应用程序接收到停止指令之后,会进行如下操作:

  • 1.停止接收新的访问请求
  • 2.正在处理的请求,等待请求处理完毕;对于内部正在执行的其他任务,比如定时任务、mq 消费等等,也要等当前正在执行的任务执行完毕,并且不再启动新的任务
  • 3.当应用准备关闭的时候,按需向外发出信号,告知其他应用服务准备接手,以保证服务高可用

如果暴力的关闭应用程序,比如通过kill -9 <pid>命令强制直接关闭应用程序进程,可能会导致正在执行的任务数据丢失或者错乱,也可能会导致任务所持有的全局资源等不到释放,比如当前任务持有 redis 的锁,并且没有设置过期时间,当任务突然被终止并且没有主动释放锁,会导致其他进程因无法获取锁而不能处理业务。

二、方案研究

SpringBoot 官方文档上,已经告诉开发者只需要实现特定接口即可监听到项目启动成功与关闭时的事件,相关接口如下:

  • CommandLineRunner接口:当应用启动成功后但在开始接受流量之前,会回调此接口的实现类,也可以实现ApplicationRunner接口,工作的方式与CommandLineRunner与之类似
  • DisposableBean接口:当应用正要被销毁前,会回调此接口的实现类,也可以使用@PreDestroy注解,被标记的方法也会被调用

基于此流程,我们可以创建一个服务监听类,用于监听到项目启动成功与关闭时的回调服务,示例代码如下:

package com.example.dataproject.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
 * @author qx
 * @date 2024/8/9
 * @des
 */
@Component
@Slf4j
public class ApplicationListener implements CommandLineRunner, DisposableBean {
    @Override
    public void run(String... args) throws Exception {
        log.info("应用启动成功,预加载相关数据");
    }

    @Override
    public void destroy() throws Exception {
        log.info("应用正在关闭,清理相关资源");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
三、使用ApplicationContext的close方法关闭服务

我们可以使用ApplicationContext的close方法来关停服务,他会自动销毁bean对象并关停服务。

只需要在应用启用的时候,获取ApplicationContext对象,然后在相关的位置调用close方法,就可以关闭服务。

package com.example.dataproject;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class DataProjectApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(DataProjectApplication.class, args);

        try {
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //启动15秒后 自动关闭应用
        context.close();
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.

我们启动程序后,在控制台发现15秒后自动停止了应用。

在SpringBoot中实现优雅停机_SpringBoot

我们也可以自己创建一个控制层,通过请求的方式关闭应用服务。示例代码如下:

package com.example.dataproject.controller;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author qx
 * @date 2024/8/9
 * @des
 */
@RestController
public class DemoController implements ApplicationContextAware {

    private ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
    }

    /**
     * 关闭服务
     */
    @GetMapping("/close")
    public String closeApp() {
        ((ConfigurableApplicationContext) context).close();
        return "关闭服务";
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

我们启动程序调用关闭服务的请求。

在SpringBoot中实现优雅停机_ApplicationContext_02

我们在控制台上同样看到了关闭应用服务的日志。

在SpringBoot中实现优雅停机_ApplicationContext_03

四、使用SpringApplication的exit方法关闭服务

通过调用一个SpringApplication.exit()方法也可以安全的退出程序,同时会返回一个退出码,这个退出码可以传递给所有的context,最后通过调用System.exit()可以将这个错误码也传给JVM。

示例代码如下:

package com.example.dataproject;

import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class DataProjectApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(DataProjectApplication.class, args);
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //10秒后 关闭服务
        exitApp(context);
    }

    public static void exitApp(ConfigurableApplicationContext context) {
        //获取退出码
        int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0);
        //退出码传递给jvm,安全退出程序
        System.exit(exitCode);
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

我们同样启动程序,10秒钟后在控制台上打印出了服务停止的日志信息。

在SpringBoot中实现优雅停机_优雅停机_04