Runtime.getRuntime().addShutdownHook(Thread hook)
System.exit()、使用者 Ctrl+C、SIGTERM、最後一個非守護執行緒結束),會並行啟動所有已註冊鉤子。SIGKILL、Runtime.halt()、JVM 崩潰)時執行。System.exit() 會阻塞/死鎖;若需中止改用 Runtime.halt()。
import java.io.*;
import java.util.Date;
public class GracefulShutdownDemo {
public GracefulShutdownDemo() {
registerShutdownHook();
}
private void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try (FileWriter fw = new FileWriter("shutdown.log", true)) {
String msg = "應用程式關閉時間: " + new Date() + System.lineSeparator();
fw.write(msg);
System.out.println("關機鉤子執行: " + msg.trim());
} catch (IOException e) {
System.err.println("關機日誌寫入失敗: " + e.getMessage());
}
}), "Shutdown-Logger");
}
public static void main(String[] args) throws InterruptedException {
new GracefulShutdownDemo();
System.out.println("應用程式啟動,按 Ctrl+C 或關閉終端機測試關機鉤子");
// 模擬長時間運行
Thread.sleep(Long.MAX_VALUE);
}
}
@Component
public class GracefulShutdown implements DisposableBean {
private final DataSource dataSource;
private final RedisTemplate<String, Object> redisTemplate;
private final Scheduler scheduler;
public GracefulShutdown(DataSource dataSource,
RedisTemplate<String, Object> redisTemplate,
Scheduler scheduler) {
this.dataSource = dataSource;
this.redisTemplate = redisTemplate;
this.scheduler = scheduler;
}
@Override
public void destroy() {
// 1. 停止接受新請求(若用 Tomcat 可配置 server.shutdown=graceful)
// 2. 關閉排程器
scheduler.shutdown();
try { scheduler.awaitTermination(30, TimeUnit.SECONDS); }
catch (InterruptedException ignored) {}
// 3. 釋放連線池
if (dataSource instanceof HikariDataSource hk) hk.close();
// 4. 關閉 Redis 連線
redisTemplate.getConnectionFactory().getConnection().close();
}
}
或直接使用 @PreDestroy 標註方法,Spring 容器關閉時自動調用。
| 項目 | 建議 |
|---|---|
| 冪等性 | 鉤子邏輯必須冪等,重複執行不造成副作用 |
| 超時保護 | 單一鉤子執行時間不宜過長,建議 < 10s;總關機逾時由 OS/容器控制 |
| 異常吞噬 | 捕獲所有異常並記錄,避免一個鉤子崩潰影響其他鉤子 |
| 無相依 | 鉤子間不應有順序相依;若有順序需求,合併為單一鉤子內部按序執行 |
| 守護執行緒 | 鉤子執行緒本身不應設為 daemon,否則 JVM 可能在鉤子完成前退出 |
| 測試驗證 | 單元測試模擬 System.exit() 驗證鉤子邏輯;整合測試用 Docker stop 驗證優雅關閉 |
addShutdownHook 能攔截 kill -9:SIGKILL 直接由 OS 終止進程,JVM 無機會執行任何代碼。Thread 實例只能 start() 一次,重複註冊會拋 IllegalThreadStateException。main() 或 Spring @PostConstruct 中註冊。