• spring boot项目优雅停机


    1、关闭流程

    1. 停止接收请求和内部线程。
    2. 判断是否有线程正在执行。
    3. 等待正在执行的线程执行完毕。
    4. 停止容器。

    2、关闭过程有新的请求

            在kill Spring Boot项目时,如果有访问请求过来,请求会被拒绝并返回错误提示

            在kill Spring Boot项目时,Spring Boot应用会先停止接收请求和内部线程,然后判断是否有线程正在执行,如果有正在执行的线程,就等待线程执行完毕,最后停止容器。因此,当有访问请求过来时,请求会被拒绝并返回错误提示。

    3、预留缓冲时间

            Spring Boot的优雅停机功能,可以在收到终止信号后,不再接受、处理新请求,需要在终止进程之前预留一小段缓冲时间,以完成正在处理的请求。不要直接使用kill -9杀死进程。

    4、优雅停机方式

    4.1 通过Actuator的Endpoint机制关闭服务

            使用此方法,需要先添加spring-boot-starter-actuator监控服务依赖包

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-actuator</artifactId>
    4. </dependency>

    默认配置下,shutdown端点是关闭的,需要在application.properties里配置里面开启:

    1. management.endpoint.shutdown.enabled=true
    2. management.endpoints.web.exposure.include=shutdown

    执行关闭接口

    curl -X POST http://localhost:8080/actuator/shutdown

    4.2 使用ApplicationContext的close方法关闭服务

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

    1. ConfigurableApplicationContext ctx = SpringApplication.run(ShutdowndemoApplication.class, args);
    2. try {
    3. TimeUnit.SECONDS.sleep(10);
    4. } catch (InterruptedException e) {
    5. e.printStackTrace();
    6. }
    7. ctx.close();

            我们也可以自己写一个Controller,获取对应的ApplicationContext对象,通过api操作调用close方法关停服务,示例代码如下:

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.springframework.beans.factory.DisposableBean;
    3. import org.springframework.context.ApplicationContext;
    4. import org.springframework.context.ApplicationContextAware;
    5. import org.springframework.context.ApplicationEvent;
    6. import org.springframework.context.ConfigurableApplicationContext;
    7. import org.springframework.context.annotation.Lazy;
    8. import org.springframework.stereotype.Service;
    9. import org.springframework.web.context.request.RequestContextHolder;
    10. import org.springframework.web.context.request.ServletRequestAttributes;
    11. import javax.servlet.http.HttpServletRequest;
    12. @Slf4j
    13. @Service
    14. @Lazy(false)
    15. public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
    16. private static ApplicationContext applicationContext = null;
    17. @Override
    18. public void setApplicationContext(ApplicationContext applicationContext) {
    19. SpringContextHolder.applicationContext = applicationContext;
    20. }
    21. @Override
    22. public void destroy() {
    23. SpringContextHolder.clearHolder();
    24. }
    25. /**
    26. * 关闭服务
    27. *
    28. * @methodName: shutdownContext
    29. * @return: void
    30. * @author: weixiansheng
    31. * @date: 2023/10/25
    32. **/
    33. public static void shutdownContext() {
    34. ((ConfigurableApplicationContext) applicationContext).close();
    35. }
    36. }
    1. import com.ybw.util.SpringContextHolder;
    2. import org.springframework.web.bind.annotation.GetMapping;
    3. import org.springframework.web.bind.annotation.RestController;
    4. /**
    5. * @author weixiansheng
    6. * @version V1.0
    7. * @className ShutdownController
    8. * @date 2023/10/25
    9. **/
    10. @RestController
    11. public class ShutdownController {
    12. @GetMapping("/shutdown")
    13. public void shutdown(){
    14. SpringContextHolder.shutdownContext();
    15. }
    16. }

    4.3 监听服务pid,通过kill方式关闭服务(推荐)

            通过api方式来关停服务,在很多人看来并不安全,因为一旦接口泄漏了,意味着用户可以随便请求这个接口来关闭服务,其影响不言而喻,因此很多人建议在服务端,通过其他的方式来关闭服务,比如通过进程命令方式来关停。

            在springboot启动的时候将应用进程 ID 写入一个app.pid文件,生成的路径可以指定,然后通过脚本命令方式来关闭服务。

    1. @SpringBootApplication
    2. public class SprintBootDemoApplication {
    3. public static void main(String[] args) {
    4. SpringApplication application = new SpringApplication(SprintBootDemoApplication.class);
    5. application.addListeners(new ApplicationPidFileWriter("D:\\app.pid"));
    6. application.run();
    7. }
    8. }

    通过如下命令方式,可以安全的关闭服务。

    cat /home/app/project1/app.pid | xargs kill

            这种方式,也是目前在linux操作系统中,使用较为普遍的一种解决方案,区别在于实现的方式可能不同,有的不用写文件,通过其他方式来获取应用进程 ID。

    注意

            如果使用kill -9 的方式关闭服务,服务的监听器不会收到任何消息,类似于直接强杀应用进程,此方法不可取

    4.4 使用SpringApplication的exit方法关闭服务

            通过调用一个SpringApplication.exit()方法也可以退出程序,同时将生成一个退出码,这个退出码可以传递给所有的context。这个就是一个JVM的钩子,通过调用这个方法的话会把所有PreDestroy的方法执行并停止,并且传递给具体的退出码给所有Context。通过调用System.exit(exitCode)可以将这个错误码也传给JVM。程序执行完后最后会输出:Process finished with exit code 0,给JVM一个SIGNAL。

    1. import org.springframework.boot.ExitCodeGenerator;
    2. import org.springframework.boot.SpringApplication;
    3. import org.springframework.boot.autoconfigure.SpringBootApplication;
    4. import org.springframework.boot.context.ApplicationPidFileWriter;
    5. import org.springframework.context.ConfigurableApplicationContext;
    6. @SpringBootApplication
    7. public class SprintBootDemoApplication {
    8. public static void main(String[] args) {
    9. ConfigurableApplicationContext ctx = SpringApplication.run(SprintBootDemoApplication.class, args);
    10. exitApplication(ctx);
    11. }
    12. public static void exitApplication(ConfigurableApplicationContext context) {
    13. int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0);
    14. System.exit(exitCode);
    15. }
    16. }

    日志如下

    1. [INFO ] 2023-10-25 15:49:05.640 [main] c.y.s.SprintBootDemoApplication - Starting SprintBootDemoApplication using Java 17.0.8 on LAPTOP-V56V2EJT with PID 44520 (D:\git-code\mygit\learn\spring\sprint-boot-demo\target\classes started by weixiansheng in D:\git-code\mygit\learn\spring\sprint-boot-demo)
    2. [INFO ] 2023-10-25 15:49:05.643 [main] c.y.s.SprintBootDemoApplication - The following 1 profile is active: "dev"
    3. [INFO ] 2023-10-25 15:49:06.638 [main] o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8080 (http)
    4. [INFO ] 2023-10-25 15:49:06.647 [main] o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
    5. [INFO ] 2023-10-25 15:49:06.648 [main] o.a.catalina.core.StandardService - Starting service [Tomcat]
    6. [INFO ] 2023-10-25 15:49:06.649 [main] o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.60]
    7. [INFO ] 2023-10-25 15:49:06.738 [main] o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
    8. [INFO ] 2023-10-25 15:49:06.738 [main] o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1024 ms
    9. [INFO ] 2023-10-25 15:49:07.007 [main] o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
    10. [INFO ] 2023-10-25 15:49:07.031 [main] o.s.b.w.e.tomcat.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path ''
    11. [INFO ] 2023-10-25 15:49:07.039 [main] c.y.s.SprintBootDemoApplication - Started SprintBootDemoApplication in 1.969 seconds (JVM running for 3.815)
    12. Disconnected from the target VM, address: '127.0.0.1:53475', transport: 'socket'
    13. Process finished with exit code 0

    4.5 总结

            在真实的工作中的时候4.3比较常用,程序中一般使用内存队列或线程池的时候最好要优雅的关机,将内存队列没有处理的保存起来或线程池中没处理完的程序处理完。但是因为停机的时候比较快,所以停服务的时候最好不要处理大量的数据操作,这样会影响程序停止。

            以上这几种方法实现的话比较简单,但是真实工作中还需要考虑的点还很多,比如需要保护暴露的点不被别人利用,一般要加一些防火墙,或者只在内网使用,保证程序安全。

  • 相关阅读:
    计算机网络 | 物理层
    【夜读】影响一生的五大定律
    RedisTemplate使用详解
    spring boot 自定义注解封装(@RequestLimit注解)
    Prefix-Tuning源码解析
    翻墙工作?承德程序员被罚款 108 万元!
    Spring源码--Bean的加载
    [排序]leetcode1636:按照频率将数组升序排序(easy)
    ES6基本语法(一)
    Unity URP14.0 自定义后处理框架
  • 原文地址:https://blog.csdn.net/xixingzhe2/article/details/134035543