問題
最近項目重構,改用 Spring Boot 框架,遇到個問題:當程序 catch 住某些 exception ,需要停掉整個 application ,然后人工介入查看。但是,發現沒有辦法停掉應用,應用本身也不繼續跑下去,它就 hang 在那了。報錯如下:
o.s.c.s.DefaultLifecycleProcessor.stop(387) - Failed to shut down 1 bean with phase value 2147483647 within timeout of 30000ms: [messageListenerContainer]
調查
定位到相關代碼塊如下:
public void onMessage(Message msg) {
try {
processMsg(msg);
} catch (Throwable t) {
System.exit(-1);
}
}
收到了JMS消息,但是processMsg
報錯,然后會被 catch 住,然后執行System.exit
,但是失敗了。
這個DefaultLifecycleProcessor
是 springframework 的類,在程序 shutdown 的時候會被調用來銷毀/關閉 bean 。
分析
結合 jstack.review
查看 thread dump 如下:
沒有死鎖,進一步分析可知,SpringContextShutdownHook
想要 shutdown messageListenerContainer
,但是后者還在等消息。所以前者 timeout 了。
解決1
最簡單的方案,退出時不要調用 Spring 的 ShutdownHook ,就不會有后面一系列的問題。在 properties 文件加入一行配置:
spring.main.register-shutdown-hook=false
這個方案足夠簡單,也奏效。但是沒有合理地關閉資源,可能會造成資源浪費。如果頻繁啟停應用,可能會有問題。
解決2
這個問題的本質是,處理消息的線程不能自己關閉自己的 JMS container 。
那么,就建一個 monitor 線程,如果需要退出時,發一個信號給 monitor 線程,讓它去關閉 JMS container (以及 DataSource, File, etc.) 。示例代碼如下:
Executors.newSingleThreadExecutor.execute(new Runnable() {
public void run() {
if (signal)
stopTheContainer();
}
}
這個signal
,可以用一個AtomicBoolean
shutdownFlag 來實現。
解決2 - 補充
再“優雅”一點,遇到異常需要退出時,拋一個自定義的 Exception
,比如 ApplicationExitException
,然后用一個自定義的 ErrorHandler
去接住這個異常,然后再新起線程發出退出信號。
@Service
public class ApplicationExitErrorHandler implements ErrorHandler {
@Override
public void handleError(Throwable t) {
// actual exit logic
}
}
參考 -> https://www.baeldung.com/spring-jms#error-handler
這樣做的好處是:解耦,業務代碼和異常處理代碼分離,邏輯上更加清晰。
壞處是:不熟悉 Spring Error Handling 框架的人 查代碼/debug 起來更加困難。
解決3
網上有說升級 Spring Boot version to 2.3.4.RELEASE
就能解決問題的,這個筆者沒有試過。僅僅列在這里作為一個可能的選項。
參考這里 -> https://github.com/spring-cloud/spring-cloud-gateway/issues/2037