spring boot项目优雅停机
1、关闭流程
- 停止接收请求和内部线程。
- 判断是否有线程正在执行。
- 等待正在执行的线程执行完毕。
- 停止容器。
2、关闭过程有新的请求
在kill Spring Boot项目时,如果有访问请求过来,请求会被拒绝并返回错误提示。
在kill Spring Boot项目时,Spring Boot应用会先停止接收请求和内部线程,然后判断是否有线程正在执行,如果有正在执行的线程,就等待线程执行完毕,最后停止容器。因此,当有访问请求过来时,请求会被拒绝并返回错误提示。
3、预留缓冲时间
Spring Boot的优雅停机功能,可以在收到终止信号后,不再接受、处理新请求,需要在终止进程之前预留一小段缓冲时间,以完成正在处理的请求。不要直接使用kill -9杀死进程。
4、优雅停机方式
4.1 通过Actuator的Endpoint机制关闭服务
使用此方法,需要先添加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
4.2 使用ApplicationContext的close方法关闭服务
在应用启用的时候,获取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();
}
}
4.3 监听服务pid,通过kill方式关闭服务(推荐)
通过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 <pid>
的方式关闭服务,服务的监听器不会收到任何消息,类似于直接强杀应用进程,此方法不可取!
4.4 使用SpringApplication的exit方法关闭服务
通过调用一个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.5 总结
在真实的工作中的时候4.3比较常用,程序中一般使用内存队列或线程池的时候最好要优雅的关机,将内存队列没有处理的保存起来或线程池中没处理完的程序处理完。但是因为停机的时候比较快,所以停服务的时候最好不要处理大量的数据操作,这样会影响程序停止。
以上这几种方法实现的话比较简单,但是真实工作中还需要考虑的点还很多,比如需要保护暴露的点不被别人利用,一般要加一些防火墙,或者只在内网使用,保证程序安全。