寫這篇文章是因為看到 “線程池在使用結束后應該正確關閉.” 那么如果我們的 Spring 應用都無法正確關閉, 那么線程池肯定也無從保障
1. 優雅關閉
- kill with pid, without
-9
大多數情況下無須在意這個問題, 正確使用 kill
命令關閉就行 (注意不能使用 kill -9
)
kill $(cat ./application.pid)
默認發送的信號是
SIGTERM
,其信號編號為15
. 在無法正常終止進程時使用-9
(SIGKILL): 強制立即終止進程。這個信號無法被捕獲或忽略,進程會被操作系統強制殺死
- 在啟動的時候, 將 pid 和 端口號 記錄到文件中 (這些文件在程序關閉后會自動刪除)
public static void main(String[] args) {try {long start = System.currentTimeMillis();SpringApplicationBuilder builder = new SpringApplicationBuilder(App.class);builder.beanNameGenerator(FullyQualifiedAnnotationBeanNameGenerator.INSTANCE);// startup with pid file and port filebuilder.listeners(new ApplicationPidFileWriter()); // pid file// 上面的類可以傳入文件路徑, 方便使用 kill 命令// builder.listeners(new ApplicationPidFileWriter("./bin/shutdown.pid")); // kill $(cat ./bin/shutdown.pid)builder.listeners(new WebServerPortFileWriter()); // port filebuilder.run(args);long costs = (System.currentTimeMillis() - start) / 1000;log.info("### APP STARTED ### It take {} seconds.", costs);} catch (Exception e) { log.error(" STARTUP FAILED ", e); }
}
2. 增加 URL 接口來關閉程序
- 使用
ConfigurableApplicationContext#close
- 使用
int exit = SpringApplication.exit(context);
- 關閉應用時執行某段邏輯
- 注解
@PreDestroy
- 事件
ContextClosedEvent
- 注解
@Slf4j
@RestController
public class ShutDownController {@Autowiredprivate ConfigurableApplicationContext applicationContext;@Autowiredprivate ApplicationContext context; // 兩個 context 都行@GetMapping("/shutdown")public String shutdown() {log.error("1. shutdown begin..."); // 前面的編號是執行順序// 清理資源并關閉((ConfigurableApplicationContext) context).close();// applicationContext.close();log.error("3. spring application closed.");// 在嵌入式應用或在某些情況下,關閉上下文后,JVM 可能不會自動退出。為了確保應用完全終止,使用 System.exit(0)System.exit(0); // not necessary// log.error("system closed."); // 這里日志對象 log 已經失效, 無法打印, 換用 printlnSystem.err.println("system closed.");return "shutdown";}@GetMapping("/exit")public String exit() {log.error("1. shutdown begin...");// 負責清理資源int exit = SpringApplication.exit(context);log.error("3. Spring app closed.");System.exit(exit); // not necessary// log.error("system closed."); // It will fail overSystem.err.println("system closed.");return "shutdown";}@PreDestroy // @PreDestroy 注解: 在 Spring 應用銷毀前, 執行某段邏輯public void onDestroy() throws Exception {log.error("3. Spring Container is destroyed!");}@Slf4j@Component // ContextClosedEvent 事件: 在 Spring 上下文關閉時機, 執行某段邏輯public // public 不是必須static // @Slf4j is not supported on non-static nested classes.class ApplicationShutdown implements ApplicationListener<ContextClosedEvent> {@Overridepublic void onApplicationEvent(ContextClosedEvent event) {log.error("2. graceful shutdown (ContextClosedEvent)");}}
}
Spring 并沒有強制要求內部類必須是
public
或static
。但在最佳實踐中:
- 使用
static
內部類可以避免使用處有不必要的引用,特別是當內部類不需要訪問外部類的實例時。public
不是必須: 如果內部類不需要在外部訪問,它可以是package-private
(默認)或protected
。通常建議將其定義為public
和static
,以提高可維護性。
3. 優雅啟動
- 顯示配置參數
/*** 顯示配置參數*/public static void printEnvironment(Environment environment) {try {log.info("******************JVM參數**********************");String[] properties = {"java.home", "java.version", "java.vm.name", "java.vm.vendor", "os.name", "user.dir"};for (String property : properties) {log.info("* {} = {}", property, System.getProperty(property));}MemoryMXBean bean = ManagementFactory.getMemoryMXBean();MemoryUsage heapUsage = bean.getHeapMemoryUsage();log.info("* 初始內存 = " + heapUsage.getInit() / 1024 / 1024 + "M");log.info("* 已使用內存 = " + heapUsage.getUsed() / 1024 / 1024 + "M");log.info("* 已提交內存 = " + heapUsage.getCommitted() / 1024 / 1024 + "M");log.info("* 最大內存 = " + heapUsage.getMax() / 1024 / 1024 + "M");} catch (Exception e) {log.error("異常: {}", e.getMessage());}}
- 啟動時調用:
printEnvironment(builder.context().getEnvironment());
4. 線程池的關閉
-
由 Spring 管理的線程池, 關閉 Spring 應用上下文時會自動關閉, 包括
@Async
或 通過 Spring 配置的線程池 -
自定義線程池 (未通過 Spring 管理), 在關閉應用時, 需要顯式地調用
shutdown()
來確保任務能夠正常完成, 避免丟失. 可以利用上面提到的, 關閉應用時執行某段邏輯的方式執行- 注解
@PreDestroy
- 事件
ContextClosedEvent
- 注解
(END)