SpringBoot 优雅停机

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot 优雅停机相关的知识,希望对你有一定的参考价值。

参考技术A 对应用进程发送停止指令之后能够保证正在执行的业务操作不受影响。

如何保障在停机请求之后,当前请求处理不影响,但是无法接受新的请求,等到全部的请求都处理完成之后,再进行停机。

查找 PID 命令:ps -ef | grep -a java | grep 'TomatoStudySpringApplication'

kill -2 和 kill -15(kill)会执行处理逻辑,而 kill -9 什么都不输出。

application.yml

测试代码:

使用 kill -2 模拟关闭过程(不能使用kill -9,使用kill -9会立刻杀死进程,优雅停机不会起作用)。

POM依赖:

application.yml

POST 请求 /actuator/shutdown 端点即可关闭应用,作用和 kill -2 相同,也可以实现优雅停机。

Java 问题记录

优雅停机

优雅停机,就是在关闭服务之前,不是立马全部关停,而是做好一些善后工作,例如:关闭线程、释放连接资源等

Java
// 注册一个回调钩子函数
Runtime.getRuntime().addShutdownHook()
SpringBoot2.3之上
server:
  # 开启优雅停机,默认IMMEDIATE是立即关机
  # 当使用server.shutdown=graceful启用时,在 web 容器关闭时,web 服务器将不再接收新请求,并将等待活动请求完成的缓冲期。
  shutdown: graceful
spring:
  lifecycle:
    # 设置缓冲时间 默认30s
    timeout-pre-shutdown-phase: 20s
SpringBoot2.3之下

添加actuator的功能

@Component
public class TerminateBean {
    @PreDestroy
    public void preDestroy(){
        System.out.println("TerminalBean is destroyed");
    }
}
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        # include: * 或者 include: shutdown
        include: shutdown
curl -X POST http://localhost:8080/actuator/shutdown

小结

  • 通过应用暴露shutdown
  1. actutor的/actutor/shutdown方法(需要配置) 此时控制台会执行preDestroy方法
  2. 利用Application的close()方法(需要编码暴露出一个访问的方法) 此时控制台会执行preDestroy方法
  3. 利用SpringApplication的exit方法(需要暴露出一个访问的方法) 此时控制台会执行preDestroy方法
  • kill
  1. kill -9 pid 不需要编码,也不需要配置,利用操作系统的强制关闭指令,此时控制台不会执行preDestroy方法
  2. kill -15 pid 不需要编码,也不需要配置,利用操作系统的强制关闭指令,此时控制台会执行preDestroy方法
  3. kill | xargs kill 需要编码,然后利用操作系统指令cat/data/tmp/app.id | xargs kill,此时控制会打印preDestroy方法
public class SpringBootShutdownDemoApplication {
    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(SpringBootShutdownDemoApplication.class);

        // 指定一个文件,写入pid号
        application.addListeners(new ApplicationPidFileWriter("/data/tmp/app.pid"));
        application.run(args);
    }
}

生产环境

编写一个sh脚本,通过grep查找到我们项目的pid,然后先使用kill -15 pid,然后sleep一下,然后再找pid,如果没找到,说明已经关闭,如果找到,说明关闭失败了,那么就使用kill -9 pid强制关闭进程

响应式编程

https://blog.csdn.net/weixin_35725138/article/details/114187171

JVM相关

1、JVM主要组成

  • 类加载器(ClassLoader)

  • 运行时数据区(Runtime Data Area)

  • 执行引擎(Execution Engine)

  • 本地库接口(Native Interface)

2、JVM如何工作的

首先程序在执行之前先要把Java代码(.java)转换成字节码(.class),JVM通过类加载(ClassLoader)把字节码加载到内存中,但字节码文件是JVM的一套指令集规则,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine)将字节码翻译成底层机器码,再交由CPU去执行,CPU在执行过程中需要调用本地库接口(Native Interface)

3、JVM内存布局

  • 程序计数器(Program Counter Register)

    字节码行指示器,每个线程都有独立的程序计数器

  • Java虚拟机栈(Java Virtual Machine Stacks)

    用于存储局部变量表、操作数栈、动态链接、方法出口等。调用Java方法

    线程请求深度大于虚拟机所允许的栈深度就会抛出StackOverflowError异常

    无法申请到足够内存就会抛出OutOfMemoryError异常

  • 本地方法栈(Native Method Stack)

    与虚拟机栈作用一样,但是调用的是Native方法

  • Java堆(Java Heap)

    JVM中内存最大的一块,被所有线程共享,唯一目的存放对象实例。Java堆可以储在物理上不连续的内存空间中,只要逻辑连续即可

    -Xmx、-Xms

  • 方法区【永久代】(Methed Area)

    用于储存已被虚拟机加载的类信息、产量、静态变量、即时编译后代码等数据

JVM中GC

1、什么时候会被回收

JVM根据可达性分析算法判断这个是否需要回收

可达性分析算法是根据gc roots关联的,能关联到,表明对象还在使用,否则认为应该回收

2、那些可以作为gc roots

  • 虚拟机栈引用对象
  • 本地方法栈jni引用的对象
  • 方法区的静态属性引用的对象
  • 方法区的产量引用的对象

3、回收算法

  • 标记-清除:标记那些要删除,然后再一起清除。耗时并且造成的碎片多
  • 复制:将可用内存按大小分成两部分,每次用一般,满了就把目前用的区域内还活着的复制一份到另一边,然后再清除(新生代使用该算法
  • 标记-整理:标记清除的升级表,标记完所有活着的对象同一边移动得出一个活着对象的区域,然后把其他的删除了(老年代使用)

JVM如何实现热加载

public class Hotswap {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, InterruptedException {
        loadUser();

        System.gc();

        // 等待资源回收
        Thread.sleep(1000);
        // 需要被热部署的class文件
        File file1 = new File("C:\\\\StudyFile\\\\Java\\\\zp_project01\\\\file\\\\User.class");

        // 之前编译好的class文件
        File file2 = new File("C:\\\\StudyFile\\\\Java\\\\zp_project01\\\\build\\\\classes\\\\java\\\\main\\\\hotloader\\\\User.class");

        // 删除旧版本的class文件
        boolean isDelete = file2.delete();
        if (!isDelete) {
            System.out.println("热部署失败");
            return;
        }

        boolean b = file1.renameTo(file2);
        if (!b) {
            System.out.println("热部署 替换文件失败! ");
        }
        System.out.println("update success!");
        loadUser();
    }

    public static void loadUser() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        MyClassLoader myLoader = new MyClassLoader();

        Class<?> class1 = myLoader.findClass("hotloader.User");

        Object obj1 = class1.getDeclaredConstructor().newInstance();
        Method method = class1.getMethod("add");
        method.invoke(obj1);
        System.out.println(obj1.getClass());
        System.out.println(obj1.getClass().getClassLoader());
    }
}

https://www.jianshu.com/p/f82dd30c620b

类从加载虚拟机内存中开始到卸载出内存为止,生命周期:加载、验证、准备、解析、初始化、使用、卸载

image-20210512104129524

1、加载

通过类的全限定名来获取定义此类的二进制字节流(没有指明二进制字节流要从一个Class文件中获取,可以从ZIP包中读取,从网络中获取,隐形时计算生成等等)将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口完成后,虚拟机外部的二进制字节流就按照虚拟机所需格式存储在方法区中。对象是实例化的类。类的信息时存储在方法区中的,对象时存储在Java堆中的。类是对象的模板,对象是类的实例

2、验证

加载阶段未完成,连接阶段已经开始。两者之间会交叉运行。因为Class文件可以用任何途径产生,字节流不符合Class文件格式的约束,虚拟机会抛出java.lang.VerifyError异常,所以为了保护虚拟机,JVM会有一下四个方面的验证

  • 文件格式验证:即验证类文件结构
  • 元数据验证:这个是否有父类,父类是否继承了不允许被继承的类等等
  • 字节码验证:对类的方法体进行校验
  • 符号引用验证:全限定名是否能找到对应的类,在指定类中是否存在符合方法的字段描述以及简单名称描述的方法,字段。访问性是否正确。验证不成功会抛出java.lang.incompatibleClassChangeError异常的子类

3、准备

证是为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。这个时候进行内存分配的只包括类变量(被static修饰的变量),并不包括实例变量,实例变量是在对象实例化时随对象一起分配在java堆中。这个时候分配的初始值为零值。

假设一个变量定义为 public static int value = 123;则设置变量的初始值应该为0,而不是123。把value赋值为123的putstatic指令是在程序被编译后,存放于类构造器<client>()方法之中,所以把value赋值为123的动作将在【初始化阶段才会执行】。如果字段属性表中存在ConstantValue属性,那么在准备阶段会将value赋值为ConstantValue属性所指定的值。例如: public static final int value = 123 那么在准备节点,则会将value赋值为123

4、解析

将符号引用转换为直接应用的过程。符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。而直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在

解析动作主要针对:类或接口、字段(类成员变量)、类方法、接口方法等引用进行

类或接口的解析:判断所要转化的直接引用是对数组类型,还是对普通的对象类型的引用,从而进行不同的解析

字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束

类方法解析:对类方法的解析与对字段解析的搜索不着类似,只是多判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,在搜索接口

接口方法解析:与类方法解析步骤类型,由于接口不会有父类,因此,只递归向上搜索父类接口就行

5、初始化

类加载的最后异步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主管计划区初始化类变量和其他资源。到初始化阶段,才真正开始执行类中的Java程序代码。即初始化阶段是执行类构造器()方法的过程

方法解析过程:方法是有编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器的顺序是由语句在源文件中出现的顺序决定的,所以静态语句块中只能访问到定义在静态语句块之前的变量,定义在它他之后的变量,在前面的静态语句块可以赋值,但是不能访问。虚拟机会保证在子类的()方法执行前,父类的()方法已经执行完毕,因此在虚拟机中的一个被执行的()方法的类肯定是java.lang.Object。()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有变量的赋值操作,那么编译器可以不为这个类生成()方法。所以这个()方法主要是给静态变量赋值和执行静态语句块。接口中不能使用静态语句块,单仍然由变量初始化的赋值操作,因此接口与类一样都会生成()方法,但接口与类不同的是,执行接口的()方法不需要先执行父类的接口的()方法。只有当父类接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也不一样不会执行接口方法

虚拟机会保证一个类()方法在多线程环境中被正确加锁、同步,如果多个线程同时区初始化一个类,那么只会有一个线程去执行这个类()方法,其它线程都需要阻塞等待,直到活动线程执行方法完毕。如果在一个类()方法中有耗时很长的操作,就可能会造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的

Java中类加载器与双亲委派机制

实现"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作的代码模块称为类加载器java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制。JVM用来完成上述功能的具体实现就是类加载器。类加载器读取class字节码文件将其转换为java.lang.Class类的一个实例。每个实例用来表示一个java类。通过该实例的newInstance()方法可以创建出一个该类的对象

1、引导类加载器(Bootstrap ClassLoader)

这个类加载器负责将<JAVA_HOME>\\lib目录下的类库加载到虚拟机内存中,用来加载java的核心库,此类加载器并不继承java.lang.ClassLoader,不能被java程序直接调用,代码是C++编写的。是虚拟机自身的一部分

2、扩展类加载器(Extendsion ClassLoader)

这个类加载器负责加载<JAVA_HONE>\\lib\\ext目录下的类库,用来加载java的扩展库,开发者可以直接使用这个类加载器

3、应用程序类加载器(Application ClassLoader)

这个类加载器负责加载用户类路径(CLASSPATH)下的类库,一般我们编写的java类都是由这个类加载器加载,这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器,一般情况下这就是系统默认的类加载器

4、自定义的类加载器(ClassLoader)

这个加载器可以满足我们加载类的特殊需求,需要继承java.lang.ClassLoader类并且覆盖其中的findClass()方法和defineClass()方法

5、双亲委派机制

上面的4个加载器并不是并行加载,JVM中是通过双亲委派机制来加载的

image-20210513161232596

双亲委派模型是一种组织类加载器之间关系的一种规范,它的工作原理是:如果一个类加载器收到了类加载的请求,他不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(他的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载。这样的好处是:java类随着它的类加载器一起具备了带有优先级的层次关系。这是十分必要的,比如java.lang.Object,他存放在\\jre\\lib\\rt.jar中,他是所有java类的父类,因此无论那个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中。Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会乱了

必须对类进行初始化的5中情况

1、遇到new、getstatic、putstatic、invokestatic字节码指令。最常见的Java代码场景是:使用new实例化对象、读取或设置一个类的静态字段(被final修饰或已在编译器把结果放入常量池的静态字段除外)、调用一个类的静态方法

2、使用java.lang.redlect包的方法对类进行反射调用

3、类继承了父类,父类要先初始化

4、虚拟机启动,初始化主类

5、当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化

Java创建对象的几种方式

1、使用new关键字

2、使用反射机制

Java.lang.Class、Java.lang.reflect.Constructor类的newInstance()实例方法

// 通过Class创建
// 第一种在9已不推荐使用
IUserManager userManager = (IUserManager) Class.forName("proxy.IUserManager").newInstance();
IUserManager userManager = (IUserManager) Class.forName("proxy.IUserManager").getDeclaredConstructor().newInstance();
// 通过Constructor创建
IUserManager userManager = IUserManager.class.getConstructor().newInstance();

3、使用clone方法

无论合适我们调用一个对象的clone方法,jvm就会创建一个新的对象,将前面的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数

使用clone方法m需要先实现Cloneable接口并实现其定义的clone方法

IUserManagerImpl clone = (IUserManagerImpl) userManager.clone();

4、使用反序列化

当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象。在反序列化时,JVM创建对象并不会调用任何构造函数

对象需要实现序列化接口Serializable接口

JDK和CGLIB动态代理

原理

1、JDK动态代理原理(必须 implement 可以代理final修饰的类)

利用拦截器(拦截器必须实现InvocationHandler()加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理

2、CGLIB动态代理(都可以 fianl修饰不能被代理)

利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理

3、如何选择JDK还是CGLIB

  • 如果目标对象实现了接口,默认情况下会采用JDK动态代理实现
  • 如果目标对象实现了接口,可以强制使用CGLIB实现APO
  • 如果目标对象没有实现接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换

4、强制使用CGLIB实现AOP

1、添加相关依赖 添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar)
2、在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>

5、JDK动态代理和CGLIB字节码生成的区别

1、JDK动态代理只能对是西安了接口的类生成代理,而不能针对类
2、CGLIB是正对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法实现增强,但是应为采用的是继承,所以该类或方法最好不要声明成final【对于final类或方法,是无法继承的】

6、CGLIB和JDK效率

1、使用CGLIB实现动态代理,CGLIB底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK6之前比使用Java反射效率高。需要注意的是CGLIB不能对final的方法进行代理,因为CGLIB原理是动态生成被代理类的子类
2、在JDK6、7、8逐步对JDK动态代理优化后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,JDK6,7比CGLIB代理效率低一点,但是到JSK8的时候,JDK代理效率高域CGLIB代理,每一次版本升级jdk代理效率都得到提升,而CGLIB代理消息确有点更不上步伐

7、Spring如何选择JDK和CGLIB

1、当Bean实现接口时,Spring就会用JDK的动态代理
2、当Bean没有实现接口时,Spring使用CGLIB实现

总结

  • JDK代理不需要太第三方库支持,只需要JDK环境就可以进行代理
1、实现InvocationHandler
2、使用Proxy.newProxyInstance产生代理对象
3、被代理的对象必须要实现接口
CGLIB必须依赖cglib的类库,安安是他需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是正对接口编程的环境下推荐使用JDK代理

数据库千万级大表新增字段如何进行平滑升级

https://www.percona.com/downloads/percona-toolkit/LATEST/

1、先创建一个扩充字段后的新表

2、在原表user上创建三个触发器,对原表user进行的所有insert/update/delete操作,都会对信标user_new进行相同的操作

3、分批将原表user中的数据insert到新表user_new,直至数据迁移完成

4、删除触发器,把原表移走(默认drop)

5、把新表user_new重命名(rename)成原表user

数据库分表的设计?订单系统,日订单量500W如何设计

  • 热数据:3个月内的订单数据,查询实时性较高;
  • 冷数据A:3个月 ~ 12个月前的订单数据,查询频率不高;
  • 冷数据B:1年前的订单数据,几乎不会查询,只有偶尔的查询需求;

Guava

基本工具【Basic utilities】

  • 使用和避免null

image-20210517114718965

  • 前置条件
Preconditions

https://ifeve.com/google-guava/

Spring

1、Spring中,有集中配置Bean的方式

  • 基于XML的配置
  • 基于注解的配置
  • 基于Java的配置

2、Spring中的生命周期

实例化 -> 属性赋值 -> 初始化 -> 销毁

Spring Bean Factory负责管理再Spring容器中被创建的bean的生命周期。Bean生命周期由两组回调(call back)方法组成

  • 初始化之后调用的回调方法
  • 销毁之前调用的回调方法

Spring提供四种方式来管理生命周期事件

  • InitializingBeanDisposableBean回调接口
  • 针对特殊行为的其他Aware接口
  • Bean配置文件中Custom init()方法和destroy()方法
  • @PostConstruct和@PreDestroy注解方式

3、Spring Bean有哪些作用域(5种)【存活周期与作用域相关】

  • singleton默认是单列不管接受多少个请求,每个容器中只有一个bean的实例,单例的模式由bean factory自身维护
  • prototype:和singleton相反,为每一个bean请求提供一个实例
  • request:载请求bean范围内会为每一个来自客户端网络请求创建一个实例,在请求完成后bean会失效并被垃圾回收器回收
  • Session
  • global-session:global-session 和 Portlet 应用相关 。 当你的应用部署在 Portlet 容器中工作时,它包含很多 portlet。 如果你想要声明让所有的 portlet 共用全局的存储变量的话,那么这全局变量需要存储在 global-session 中

4、Spring框架种的单例Beans是线程安全的么

在对外提供的服务中没有全局变量是线程安全的,【如果是多线程环境】

5、Spring框架有哪些装配模式

  • no:默认方式,在该设置下自动装配是关闭,开发者需要自行在bean定义中用标签明确的设置依赖关系
  • byName:根据bean名称设置依赖关系
  • byType:根据bean类型设置依赖
  • constructor:构造器自动装配
  • autodetect:自动探测使用构造器自动装配

6、Spring中主要使用了那是设计模式

  • 代理模式:在AOP和Remoting中
  • 单例模式:Spring配置文件中定义的bean默认是单例
  • 模板方法:用来解决重复代码 RedisTemplate等
  • 依赖注入:贯穿BeanFactory/ApplicationContext接口的核心理念
  • 工厂模式:用BeanFactory来创建对象实例

集合

1、集合与数组的区别

集合是集合,数组是数组

1.1、集合分类

单列集合:Collection(List、Set)

双列集合:Map

相同点

都是容器,可以存储多个数据

不同点

  • 数组的长度是不可变的
  • 集合的长度是可变的
  • 数组可以存基本数据类型和引用数据类型
  • 集合只能存引用数据类型,如果要存基本数据类型,需要存对应的包装类

2、ArrayList的扩容机制 (默认是10)

  • 扩容原理:ArrayList的add方法,在真正add之前,会检查是否需要扩容,扩容的关键就是新的数组长度,java源码中是按照原始长度的1.5倍来确定新长度。具体的拷贝过程,就是简历一个新的长度数组,把原始数组的数据拷贝到新数组中
  • 在已知需要加入list中的元素个数时,可以调用ensureCapacity方法来提前扩容到指定的大小,避免添加的过程中不断扩容,提高性能

3、HashMap的底层结构

  • HashMap底层结构是数组+连表+红黑树(8引进),当链表达式默认阈值8时,会转变为红黑树
3.1、HashMap的扩容机制(默认是16)
  • ArrayList类似,楚氏都是一个空的数组,当实际添加数据时,会初始化一个默认产犊为16的数组
  • 负载因子(0.75):HashMap的扩容依据是当前容器中的键值对个数大于数组长度*负载因子=负载个数时,进行扩容,扩容为之前长度2
3.2、HashMap的长度为什么时2的幂次方
  • hash值时int类型,默认有很大的方位,但是数组又不可能那么大,所以需要按照数组的实际大小取模
  • 位运算的效率高域取模的效率,所以要尽可能转化为位运算
  • 当长度为的幂次方时,h%length取模的操作可以转化h&(length-1)

以上是关于SpringBoot 优雅停机的主要内容,如果未能解决你的问题,请参考以下文章

Dubbo源码学习--优雅停机原理及在SpringBoot中遇到的问题

SpringBoot 2.3.0 开启实时健康检查,以及配置优雅停机

你的SpringBoot应用是怎么停机的?

spring boot 2.0 实现优雅停机

StringBoot如何进行优雅停机

将springboot项目构建为docker镜像