在kill Spring Boot项目时,如果有访问请求过来,请求会被拒绝并返回错误提示。
在kill Spring Boot项目时,Spring Boot应用会先停止接收请求和内部线程,然后判断是否有线程正在执行,如果有正在执行的线程,就等待线程执行完毕,最后停止容器。因此,当有访问请求过来时,请求会被拒绝并返回错误提示。
Spring Boot的优雅停机功能,可以在收到终止信号后,不再接受、处理新请求,需要在终止进程之前预留一小段缓冲时间,以完成正在处理的请求。不要直接使用kill -9杀死进程。
使用此方法,需要先添加spring-boot-starter-actuator监控服务依赖包
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-actuator</artifactId>
- </dependency>
默认配置下,shutdown端点是关闭的,需要在application.properties里配置里面开启:
- management.endpoint.shutdown.enabled=true
- management.endpoints.web.exposure.include=shutdown
执行关闭接口
curl -X POST http://localhost:8080/actuator/shutdown
在应用启用的时候,获取ApplicationContext对象,然后在相关的位置调用close方法,就可以关闭服务。
- ConfigurableApplicationContext ctx = SpringApplication.run(ShutdowndemoApplication.class, args);
- try {
- TimeUnit.SECONDS.sleep(10);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- ctx.close();
我们也可以自己写一个Controller,获取对应的ApplicationContext对象,通过api操作调用close方法关停服务,示例代码如下:
-
-
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.DisposableBean;
- import org.springframework.context.ApplicationContext;
- import org.springframework.context.ApplicationContextAware;
- import org.springframework.context.ApplicationEvent;
- import org.springframework.context.ConfigurableApplicationContext;
- import org.springframework.context.annotation.Lazy;
- import org.springframework.stereotype.Service;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
-
- import javax.servlet.http.HttpServletRequest;
-
-
- @Slf4j
- @Service
- @Lazy(false)
- public class SpringContextHolder implements ApplicationContextAware, DisposableBean {
-
- private static ApplicationContext applicationContext = null;
-
-
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) {
- SpringContextHolder.applicationContext = applicationContext;
- }
-
-
- @Override
- public void destroy() {
- SpringContextHolder.clearHolder();
- }
-
- /**
- * 关闭服务
- *
- * @methodName: shutdownContext
- * @return: void
- * @author: weixiansheng
- * @date: 2023/10/25
- **/
- public static void shutdownContext() {
- ((ConfigurableApplicationContext) applicationContext).close();
- }
-
- }
- import com.ybw.util.SpringContextHolder;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- /**
- * @author weixiansheng
- * @version V1.0
- * @className ShutdownController
- * @date 2023/10/25
- **/
- @RestController
- public class ShutdownController {
-
- @GetMapping("/shutdown")
- public void shutdown(){
- SpringContextHolder.shutdownContext();
- }
- }
通过api方式来关停服务,在很多人看来并不安全,因为一旦接口泄漏了,意味着用户可以随便请求这个接口来关闭服务,其影响不言而喻,因此很多人建议在服务端,通过其他的方式来关闭服务,比如通过进程命令方式来关停。
在springboot启动的时候将应用进程 ID 写入一个app.pid文件,生成的路径可以指定,然后通过脚本命令方式来关闭服务。
- @SpringBootApplication
- public class SprintBootDemoApplication {
-
- public static void main(String[] args) {
- SpringApplication application = new SpringApplication(SprintBootDemoApplication.class);
- application.addListeners(new ApplicationPidFileWriter("D:\\app.pid"));
- application.run();
- }
-
- }
通过如下命令方式,可以安全的关闭服务。
cat /home/app/project1/app.pid | xargs kill
这种方式,也是目前在linux操作系统中,使用较为普遍的一种解决方案,区别在于实现的方式可能不同,有的不用写文件,通过其他方式来获取应用进程 ID。
注意
如果使用
kill -9的方式关闭服务,服务的监听器不会收到任何消息,类似于直接强杀应用进程,此方法不可取!
通过调用一个SpringApplication.exit()方法也可以退出程序,同时将生成一个退出码,这个退出码可以传递给所有的context。这个就是一个JVM的钩子,通过调用这个方法的话会把所有PreDestroy的方法执行并停止,并且传递给具体的退出码给所有Context。通过调用System.exit(exitCode)可以将这个错误码也传给JVM。程序执行完后最后会输出:Process finished with exit code 0,给JVM一个SIGNAL。
- import org.springframework.boot.ExitCodeGenerator;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.boot.context.ApplicationPidFileWriter;
- import org.springframework.context.ConfigurableApplicationContext;
-
- @SpringBootApplication
- public class SprintBootDemoApplication {
-
- public static void main(String[] args) {
- ConfigurableApplicationContext ctx = SpringApplication.run(SprintBootDemoApplication.class, args);
- exitApplication(ctx);
- }
-
- public static void exitApplication(ConfigurableApplicationContext context) {
- int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0);
- System.exit(exitCode);
- }
- }
日志如下
- [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)
- [INFO ] 2023-10-25 15:49:05.643 [main] c.y.s.SprintBootDemoApplication - The following 1 profile is active: "dev"
- [INFO ] 2023-10-25 15:49:06.638 [main] o.s.b.w.e.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8080 (http)
- [INFO ] 2023-10-25 15:49:06.647 [main] o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
- [INFO ] 2023-10-25 15:49:06.648 [main] o.a.catalina.core.StandardService - Starting service [Tomcat]
- [INFO ] 2023-10-25 15:49:06.649 [main] o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/9.0.60]
- [INFO ] 2023-10-25 15:49:06.738 [main] o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
- [INFO ] 2023-10-25 15:49:06.738 [main] o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1024 ms
- [INFO ] 2023-10-25 15:49:07.007 [main] o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
- [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 ''
- [INFO ] 2023-10-25 15:49:07.039 [main] c.y.s.SprintBootDemoApplication - Started SprintBootDemoApplication in 1.969 seconds (JVM running for 3.815)
- Disconnected from the target VM, address: '127.0.0.1:53475', transport: 'socket'
-
- Process finished with exit code 0
在真实的工作中的时候4.3比较常用,程序中一般使用内存队列或线程池的时候最好要优雅的关机,将内存队列没有处理的保存起来或线程池中没处理完的程序处理完。但是因为停机的时候比较快,所以停服务的时候最好不要处理大量的数据操作,这样会影响程序停止。
以上这几种方法实现的话比较简单,但是真实工作中还需要考虑的点还很多,比如需要保护暴露的点不被别人利用,一般要加一些防火墙,或者只在内网使用,保证程序安全。