Spring Boot提供了2種優雅關閉進程的方式:
- 基於管理端口關閉進程
- 基於系統服務方式關閉進程
基於管理端口關閉進程
基於管理端口方式實現進程關閉實際上是模塊spring-boot-actuator
提供的功能。
首先,需要在項目中添加對應模塊依賴配置。
- 添加Maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 添加Gradle依賴
dependencies {
compile("org.springframework.boot:spring-boot-starter-actuator")
}
其次,在配置文件中添加對應的參數配置(以文件application.properties為例說明)。
# 允許執行關閉操作
management.endpoint.shutdown.enabled=true
# 處於安全考慮,只允許在本地指定關閉操作
management.server.address=127.0.0.1
# 管理端口
management.server.port=8000
# 管理URL基礎路徑,默認為“/”
management.endpoints.web.base-path=/ops
# 配置關閉進程Endpoint
management.endpoints.web.path-mapping.shutdown=shutdown
# 對外暴露管理Endpoint
management.endpoints.web.exposure.include=info, health, shutdown
完成上述准備工作以后,啟動Spring Boot應用,通過調用POST http://localhost:8000/ops/shutdown
即可關閉進程。
實踐中通常將上述關閉進程的URL調用寫到腳本中,同時還可以結合別的方式一起確保進程一定能退出,如下為腳本示例(pname指進程名稱):
#!/bin/bash
# 先通過管理端口關閉進程
curl -X POST http://127.0.0.1:8000/ops/shutdown --connect-timeout 3 --max-time 5
# 再次通過名稱檢查進程是否被成功停止
count=`ps -ef |grep pname |grep -v "grep" |wc -l`
if [ $count -gt 0 ]; then
if [ -f "$pid_file" ]; then
# 如果存在進程ID文件,則讀取進程ID使用信號量通知方式關閉進程
pid=`cat $pid_file`
kill -15 $pid
else
# 通過名稱方式查找到進程ID,使用信號量通知方式關閉進程
pid=`ps -ef |grep pname |grep -v "grep"| awk '{print $2}'`
kill -15 $pid
fi
fi
關於通過管理端口關閉Spring Boot進程的詳細說明參見:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-endpoints 。
通過系統服務方式停止進程
Spring Boot支持直接將打包好的可執行jar包以系統服務方式運行,具體實現方式如下所述。
首先,將應用打包為完全可執行的jar包。
- Maven打包配配置
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 這個配置非常重要,使打包好的jar包具備可執行權限-->
<executable>true</executable>
</configuration>
</plugin>
- Gradle打包配置
bootJar {
launchScript()
}
其次,將打包好的應用jar包添加為系統服務(在ubuntu18.04 LTS上實現,基於systemd)
1.假設將Spring Boot應用安裝到/var/myapp目錄下:將上述打包好的jar包拷貝到/var/myapp(目錄不存在,手動創建)
2.在/etc/systemd/system下添加指定名稱的系統服務:myapp.service,內容如下:
[Unit]
Description=myapp
After=syslog.target
[Service]
User=root ## 注意:這里配置的是將來啟動該服務的Linux系統用戶名,影響權限
ExecStart=/var/myapp/myapp.jar
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
3.啟動服務
$ sudo systemctl enable myapp.service
$ sudo systemctl start myapp.service
如果需要查看應用啟動日志,請執行:$ journalctl -f
。
如果啟動服務失敗,請檢查對應名稱的服務文件是否放在正確位置(如:systemd系統需要放在/etc/systemd/system目錄下),或者檢查啟動服務的用戶權限,一些錯誤情形可以參考:https://springjavatricks.blogspot.com/2018/06/installing-spring-boot-services-in.html 。
關於將Spring Boot應用部署為系統服務的詳細說明參見: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#deployment-install 。
寫在最后
我在如何優雅地停止Java進程中有講到如何實現在進程退出之前做一些收尾的工作,這在Spring Boot中同樣適用,只需要監聽對應的信號量並注冊JVM關閉鈎子即可。
@SpringBootApplication
public class SpringbootApplication {
private static final Logger logger = LoggerFactory.getLogger(SpringbootApplication.class);
public static void main(String[] args) {
// 在Spring Boot應用中通過監聽信號量和注冊關閉鈎子來實現在進程退出之前執行收尾工作
// 監聽信號量
Signal sg = new Signal("TERM");
Signal.handle(sg, new SignalHandler() {
@Override
public void handle(Signal signal) {
logger.info("do signal handle: {}", signal.getName());
System.exit(0);
}
});
// 注冊關閉鈎子
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
// 執行收尾工作
logger.info("do something on shutdown hook");
}
});
SpringApplication.run(SpringbootApplication.class, args);
logger.info("Start DONE.");
}
}
另外,需要注意的是:在普通的Java應用程序中,當出現RuntimeExeception或OOM時會觸發關閉鈎子的執行;但是在Spring Boot應用中,當出現RuntimeException或OOM時並不會觸發關閉鈎子的執行(Spring Boot使用了嵌入式Tomcat)。
【參考】
https://www.jianshu.com/p/44ef43b282f0 正確、安全地停止SpringBoot應用服務