本文将介绍几种方式来优雅的关闭 Spring Boot,分别如下:
假如我们创建了一个 Spring Boot 项目,该项目的结构如下图:
其中,TerminateBean 类的代码如下:
package com.huangx.springboot.springboot_shutdown_demo1.bean; import javax.annotation.PreDestroy; public class TerminateBean { @PreDestroy public void preDestroy() { System.out.println("TerminalBean is destroyed"); } }
上面类中,使用 @PreDestroy 标记 preDestroy() 方法,表示当 Spring Boot 关闭前将会调用该方法。
ShutDownConfig 类的代码如下:
package com.huangx.springboot.springboot_shutdown_demo1.config; import com.huangx.springboot.springboot_shutdown_demo1.bean.TerminateBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ShutDownConfig { @Bean public TerminateBean getTerminateService() { return new TerminateBean(); } }
上面代码使用 @Configuration 和 @Bean 注解将 TerminateBean 配置到 Spring Boot,Spring Boot 关闭时才能调用它的 preDestroy() 方
法。
在下面介绍的几种关闭 Spring Boot 的方式中,仅仅只有 SpringbootShutdownDemo1Application 类代码实现不同。上面两个类的代码均是一致的,因此就不会赘述。
Spring Boot 自带监控功能 Actuator,可以帮助实现对程序内部运行情况监控,比如监控状况、Bean加载情况、环境变量、日志信息、线程信息等。我们将利用 Actuator 实现关闭 Spring Boot,如下:
(1)添加 actuator 的 maven 依赖,如下:
<!-- 添加该依赖,可以实现通过 URL 关闭 Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
(2)默认情况下,actuator 的 shutdown 是禁用的,我们需要打开它。修改 application.properties 或 application.yml 文件,如下:
# 配置远程关闭 SpringBoot management.endpoint.shutdown.enabled=true management.endpoints.web.exposure.include=shutdown
(3)SpringbootShutdownDemo1Application 类代码如下:
@SpringBootApplication @RestController public class SpringbootShutdownDemo1Application { public static void main(String[] args) { SpringApplication.run(SpringbootShutdownDemo1Application.class, args); } @RequestMapping("/") public String index() { return "Spring Boot Shutdown Demo1"; } }
(4)启动 Spring Boot 项目,使用浏览器访问 http://localhost:8080/actuator 将会输出如下信息:
{ "_links":{ "self":{ "href":"http://localhost:8080/actuator", "templated":false }, "shutdown":{ "href":"http://localhost:8080/actuator/shutdown", "templated":false } } }
我们可以通过调用 http://localhost:8080/actuator/shutdown 去关闭 Spring Boot 应用(注意:该接口需要 POST 提交),如果调用成功,接口返回如下信息:
{ "message": "Shutting down, bye..." }
同时,Spring Boot 将会调用 TerminateBean 对象的 preDestroy() 方法,如下:
我们直接调用 ConfigurableApplicationContext 对象的 close() 方法。其中,SpringApplication.run() 方法将返回 ConfigurableApplicationContext 对象。我们可以提供一个 Restful 接口 /shutdown 实现通过接口方式关闭 Spring Boot 程序。代码如下:
@SpringBootApplication @RestController public class SpringbootShutdownDemo2Application { private static ConfigurableApplicationContext content; public static void main(String[] args) { content = SpringApplication.run(SpringbootShutdownDemo2Application.class, args); } @RequestMapping("/") public String index() { return "Spring Boot Shutdown Demo2"; } @RequestMapping("/shutdown") public void shutdown() { try { if(null == content) { throw new NullPointerException("not ConfigurableApplicationContext object"); } content.close(); } catch (Exception e) { e.printStackTrace(); } } }
启动程序后,调用 http://localhost:8080/shutdown 该接口支持 GET 和 POST。执行效果如下:
注意:TerminateBean 对象的 preDestroy() 方法依然被调用了。
在 Spring Boot 启动的时候将进程号写入一个 pid 文件(如:my.pid),生成的路径你可以任意指定。然后通过 DOS命令或者Shell命令根据 PID Kill 进程即可。这个时候 TerminateBean 对象的 preDestroy() 方法也会调用的。这种方法大家使用的比较普遍。写一个 start.sh/start.bat 脚本用于启动 Spring Boot 程序,然后写一个 shutdown.sh/shutdown.bat 停止脚本将服务停止。实现如下:
package com.huangx.springboot.springboot_shutdown_demo3; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.ApplicationPidFileWriter; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.File; import java.io.FileInputStream; @SpringBootApplication @RestController public class SpringbootShutdownDemo3Application { public static void main(String[] args) { SpringApplication application = new SpringApplication(SpringbootShutdownDemo3Application.class); String tmpDir = System.getProperty("java.io.tmpdir"); application.addListeners(new ApplicationPidFileWriter(tmpDir + "/shutdown.pid")); application.run(); } @RequestMapping("/") public String index() { return "Spring Boot Shutdown Demo3"; } @RequestMapping("/shutdown") public void shutdown() { try { String pidFilePath = System.getProperty("java.io.tmpdir") + "/shutdown.pid"; File file = new File(pidFilePath); if(!file.exists() || !file.isFile()) { throw new IllegalArgumentException("pid 文件不存在或不是一个文件"); } // 获取 PID FileInputStream inputStream = new FileInputStream(file); byte[] bytes = new byte[inputStream.available()]; inputStream.read(bytes); inputStream.close(); String pid = new String(bytes); System.out.println("PID=" + pid); // 根据 pid 结束进程 boolean flag = cmd("taskkill /pid " + pid + " /F"); if(flag) { file.delete(); // 删除 PID 文件 System.out.println("kill process success"); } else { System.err.println("kill process fail"); } } catch (Exception e) { e.printStackTrace(); } } private static boolean cmd(String command){ boolean flag = false; try{ Runtime.getRuntime().exec("cmd.exe /C "+command); flag = true; }catch(Exception e){ e.printStackTrace(); } return flag; } }
上面代码,将 pid 保存到临时目录的 shutdown.pid 文件,然后通过调用 /shutdown 接口去读取 pid,使用 Windows 的“taskkill /pid /F”命令强制结束进程。结果如下图:
根据结果得知,该种方法并没有触发TerminateBean 对象的 preDestroy() 方法。
直接通过调用 SpringApplication.exit()方法也可以退出程序,同时将生成一个退出码,这个退出码可以传递给所有的 context。这就是一个JVM 的钩子,通过调用这个方法的话会把 TerminateBean 对象的 preDestroy() 方法执行并停止,并且传递给具体的退出码给所有 Context。通过调用 System.exit(exitCode) 可以将这个错误码也传给 JVM。程序执行完后最后会输出:Process finished with exit code 0,给 JVM 一个信号。代码如下:
package com.huangx.springboot.springboot_shutdown_demo4; import org.springframework.boot.ExitCodeGenerator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class SpringbootShutdownDemo4Application { private static ConfigurableApplicationContext context; public static void main(String[] args) { context = SpringApplication.run(SpringbootShutdownDemo4Application.class, args); } @RequestMapping("/") public String index() { return "Spring Boot Shutdown Demo4"; } @RequestMapping("/shutdown") public void shutdown() { try { int exitCode = SpringApplication.exit(context, new ExitCodeGenerator(){ @Override public int getExitCode() { return 0; } }); System.exit(exitCode); } catch (Exception e) { e.printStackTrace(); } } }
启动 Spring Boot 程序,调用 /shutdown 接口,输出结果如下图:
注意:TerminateBean 对象的 preDestroy() 方法依然被调用了