SpringBoot實(shí)現(xiàn)優(yōu)雅停機(jī)的正確方法
一、介紹
什么叫優(yōu)雅停機(jī)?
簡(jiǎn)單的說(shuō),就是向應(yīng)用進(jìn)程發(fā)出停止指令之后,能保證正在執(zhí)行的業(yè)務(wù)操作不受影響,直到操作運(yùn)行完畢之后再停止服務(wù)。應(yīng)用程序接收到停止指令之后,會(huì)進(jìn)行如下操作:
- 1.停止接收新的訪問(wèn)請(qǐng)求
- 2.正在處理的請(qǐng)求,等待請(qǐng)求處理完畢;對(duì)于內(nèi)部正在執(zhí)行的其他任務(wù),比如定時(shí)任務(wù)、mq 消費(fèi)等等,也要等當(dāng)前正在執(zhí)行的任務(wù)執(zhí)行完畢,并且不再啟動(dòng)新的任務(wù)
- 3.當(dāng)應(yīng)用準(zhǔn)備關(guān)閉的時(shí)候,按需向外發(fā)出信號(hào),告知其他應(yīng)用服務(wù)準(zhǔn)備接手,以保證服務(wù)高可用
如果暴力的關(guān)閉應(yīng)用程序,比如通過(guò)kill -9 <pid>
命令強(qiáng)制直接關(guān)閉應(yīng)用程序進(jìn)程,可能會(huì)導(dǎo)致正在執(zhí)行的任務(wù)數(shù)據(jù)丟失或者錯(cuò)亂,也可能會(huì)導(dǎo)致任務(wù)所持有的全局資源等不到釋放,比如當(dāng)前任務(wù)持有 redis 的鎖,并且沒(méi)有設(shè)置過(guò)期時(shí)間,當(dāng)任務(wù)突然被終止并且沒(méi)有主動(dòng)釋放鎖,會(huì)導(dǎo)致其他進(jìn)程因無(wú)法獲取鎖而不能處理業(yè)務(wù)。
那么如何在不影響正在執(zhí)行的業(yè)務(wù)的情況下,將應(yīng)用程序安全的進(jìn)行關(guān)閉呢?
二、方案實(shí)踐
SpringBoot 官方文檔上,已經(jīng)告訴開(kāi)發(fā)者只需要實(shí)現(xiàn)特定接口即可監(jiān)聽(tīng)到項(xiàng)目啟動(dòng)成功與關(guān)閉時(shí)的事件,相關(guān)接口如下:
CommandLineRunner
接口:當(dāng)應(yīng)用啟動(dòng)成功后但在開(kāi)始接受流量之前,會(huì)回調(diào)此接口的實(shí)現(xiàn)類(lèi),也可以實(shí)現(xiàn)ApplicationRunner
接口,工作的方式與CommandLineRunner
與之類(lèi)似DisposableBean
接口:當(dāng)應(yīng)用正要被銷(xiāo)毀前,會(huì)回調(diào)此接口的實(shí)現(xiàn)類(lèi),也可以使用@PreDestroy
注解,被標(biāo)記的方法也會(huì)被調(diào)用
基于此流程,我們可以創(chuàng)建一個(gè)服務(wù)監(jiān)聽(tīng)類(lèi),用于監(jiān)聽(tīng)到項(xiàng)目啟動(dòng)成功與關(guān)閉時(shí)的回調(diào)服務(wù),示例代碼如下:
@Component public?class?AppListener?implements?CommandLineRunner,?DisposableBean?{ ????@Override ????public?void?run(String...?args)?throws?Exception?{ ????????System.out.println("應(yīng)用啟動(dòng)成功,預(yù)加載相關(guān)數(shù)據(jù)"); ????} ????@Override ????public?void?destroy()?throws?Exception?{ ????????System.out.println("應(yīng)用正在關(guān)閉,清理相關(guān)數(shù)據(jù)"); ????} }
每一個(gè)SpringApplication
在啟用的時(shí)候,都會(huì)向 JVM 注冊(cè)一個(gè)關(guān)閉鉤子shutdown hook
,以確保ApplicationContext
在退出的時(shí)候,通過(guò)這個(gè)勾子通知 JVM,實(shí)現(xiàn)服務(wù)正常的關(guān)閉,以下介紹的所有關(guān)閉服務(wù)的方法,都是基于這一原理進(jìn)行實(shí)現(xiàn)的。
2.1 方法一 通過(guò)Actuator的Endpoint機(jī)制關(guān)閉服務(wù)
使用此方法,需要先添加spring-boot-starter-actuator
監(jiān)控服務(wù)依賴包,
<dependency> ????<groupId>org.springframework.boot</groupId> ????<artifactId>spring-boot-starter-actuator</artifactId> </dependency>
默認(rèn)配置下,shutdown
端點(diǎn)是關(guān)閉的,需要在application.properties
里配置里面開(kāi)啟:
management.endpoint.shutdown.enabled=true
雖然Actuator
的端點(diǎn),支持通過(guò)JMX
或HTTP
進(jìn)行遠(yuǎn)程訪問(wèn)。而shutdown
默認(rèn)配置下是不支持HTTP
進(jìn)行Web
訪問(wèn)的,所以使用HTTP
請(qǐng)求進(jìn)行關(guān)閉時(shí)的配置,也需要開(kāi)啟:
management.endpoints.web.exposure.include=shutdown
最后將SpringBoot
服務(wù)啟動(dòng)之后,使用POST
請(qǐng)求類(lèi)型,調(diào)用以下接口,即可實(shí)現(xiàn)關(guān)閉服務(wù)!
http://127.0.0.1:8080/actuator/shutdown
2.2 方法二 使用ApplicationContext的close方法關(guān)閉服務(wù)
如果你不想添加spring-boot-starter-actuator
監(jiān)控服務(wù)依賴包來(lái)關(guān)停服務(wù),也可以使用ApplicationContext
的close
方法來(lái)關(guān)停服務(wù),他會(huì)自動(dòng)銷(xiāo)毀bean
對(duì)象并關(guān)停服務(wù)。
只需要在應(yīng)用啟用的時(shí)候,獲取ApplicationContext
對(duì)象,然后在相關(guān)的位置調(diào)用close
方法,就可以關(guān)閉服務(wù)。
示例代碼如下:
@SpringBootApplication public?class?Application?{ ????public?static?void?main(String[]?args)?{ ??????ConfigurableApplicationContext?context?=?SpringApplication.run(Application.class,?args); ??????try?{ ?????????TimeUnit.SECONDS.sleep(10); ??????}?catch?(InterruptedException?e)?{ ?????????e.printStackTrace(); ??????} ??????//啟動(dòng)10秒以后,自動(dòng)關(guān)閉 ??????context.close(); ????} }
當(dāng)然我們也可以自己寫(xiě)一個(gè)Controller
,獲取對(duì)應(yīng)的ApplicationContext
對(duì)象,通過(guò)api
操作調(diào)用close
方法關(guān)停服務(wù),示例代碼如下:
@RestController public?class?ShutdownController?implements?ApplicationContextAware?{ ????private?ApplicationContext?context; ????@Override ????public?void?setApplicationContext(ApplicationContext?applicationContext)?throws?BeansException?{ ????????this.context?=?applicationContext; ????} ????/** ?????*?關(guān)閉服務(wù) ?????*/ ????@GetMapping("/shutdown") ????public?void?shutdownContext()?{ ????????((ConfigurableApplicationContext)?context).close(); ????} }
2.3 方法三 監(jiān)聽(tīng)服務(wù)pid,通過(guò)kill方式關(guān)閉服務(wù)
通過(guò)api
方式來(lái)關(guān)停服務(wù),在很多人看來(lái)并不安全,因?yàn)橐坏┙涌谛孤┝?,意味著用戶可以隨便請(qǐng)求這個(gè)接口來(lái)關(guān)閉服務(wù),其影響不言而喻,因此很多人建議在服務(wù)端,通過(guò)其他的方式來(lái)關(guān)閉服務(wù),比如通過(guò)進(jìn)程命令方式來(lái)關(guān)停。
在springboot
啟動(dòng)的時(shí)候?qū)?yīng)用進(jìn)程 ID 寫(xiě)入一個(gè)app.pid
文件,生成的路徑可以指定,然后通過(guò)腳本命令方式來(lái)關(guān)閉服務(wù)。
啟動(dòng)示例代碼如下:
@SpringBootApplication public?class?Application?{ ????public?static?void?main(String[]?args)?{ ????????SpringApplication?application?=?new?SpringApplication(Application.class); ????????application.addListeners(new?ApplicationPidFileWriter("/home/app/project1/app.pid")); ????????application.run(); ????} }
通過(guò)如下命令方式,可以安全的關(guān)閉服務(wù)。
cat?/home/app/project1/app.pid?|?xargs?kill
這種方式,也是目前在linux
操作系統(tǒng)中,使用較為普遍的一種解決方案,區(qū)別在于實(shí)現(xiàn)的方式可能不同,有的不用寫(xiě)文件,通過(guò)其他方式來(lái)獲取應(yīng)用進(jìn)程 ID。
如果使用kill -9 <pid>
的方式關(guān)閉服務(wù),服務(wù)的監(jiān)聽(tīng)器不會(huì)收到任何消息,類(lèi)似于直接強(qiáng)殺應(yīng)用進(jìn)程,此方法不可取!
2.4 方法四 使用SpringApplication的exit方法關(guān)閉服務(wù)
通過(guò)調(diào)用一個(gè)SpringApplication.exit()
方法也可以安全的退出程序,同時(shí)會(huì)返回一個(gè)退出碼,這個(gè)退出碼可以傳遞給所有的context
,最后通過(guò)調(diào)用System.exit()
可以將這個(gè)錯(cuò)誤碼也傳給JVM
。
示例代碼如下:
@SpringBootApplication public?class?Application?{ ????public?static?void?main(String[]?args)?{ ????????ConfigurableApplicationContext?context?=?SpringApplication.run(Application.class,?args); ????????try?{ ????????????TimeUnit.SECONDS.sleep(5); ????????}?catch?(InterruptedException?e)?{ ????????????e.printStackTrace(); ????????} ????????//5秒后,關(guān)閉服務(wù) ????????exitApplication(context); ????} ????public?static?void?exitApplication(ConfigurableApplicationContext?context)?{ ?????//獲取退出碼 ????????int?exitCode?=?SpringApplication.exit(context,?(ExitCodeGenerator)?()?->?0); ????????//退出碼傳遞給jvm,安全退出程序 ????????System.exit(exitCode); ????} }
三、其他監(jiān)聽(tīng)介紹
3.1、ApplicationListener
如果有些服務(wù),比如定時(shí)任務(wù),我們想在SpringBoot
關(guān)閉數(shù)據(jù)源連接池之前,將其關(guān)閉,可以通過(guò)實(shí)現(xiàn)ApplicationListener
接口,監(jiān)聽(tīng)bean
對(duì)象的變化情況,在bean
對(duì)象銷(xiāo)毀之前,執(zhí)行相關(guān)的關(guān)閉任務(wù)。
示例代碼如下:
@Component public?class?JobTaskListener?implements?ApplicationListener?{ ????@Override ????public?void?onApplicationEvent(ApplicationEvent?applicationEvent)?{ ????????//?在spring?bean容器銷(xiāo)毀之前執(zhí)行的事件,防止數(shù)據(jù)庫(kù)連接池在任務(wù)終止前銷(xiāo)毀 ????????if?(applicationEvent?instanceof?ContextClosedEvent)?{ ????????????System.out.println("關(guān)閉相關(guān)的定時(shí)任務(wù)"); ????????} ????} }
3.2、PreDestroy
上文中,我們提到了實(shí)現(xiàn)DisposableBean
接口,可以監(jiān)聽(tīng)?wèi)?yīng)用關(guān)閉前的回調(diào)處理,其實(shí)在自定義的方法上加@PreDestroy
注解,也可以實(shí)現(xiàn)相同的效果。
示例代碼如下:
@Component public?class?AppDestroyConfig?{ ????@PreDestroy ????public?void?PreDestroy(){ ????????System.out.println("應(yīng)用程序正在關(guān)閉。。。"); ????} }
到此這篇關(guān)于SpringBoot實(shí)現(xiàn)優(yōu)雅停機(jī)的正確方法的文章就介紹到這了,更多相關(guān)SpringBoot優(yōu)雅停機(jī)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java 將數(shù)據(jù)加載到內(nèi)存中的操作
這篇文章主要介紹了java 將數(shù)據(jù)加載到內(nèi)存中的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09java調(diào)用WebService服務(wù)的四種方法總結(jié)
WebService是一種跨編程語(yǔ)言、跨操作系統(tǒng)平臺(tái)的遠(yuǎn)程調(diào)用技術(shù),已存在很多年了,很多接口也都是通過(guò)WebService方式來(lái)發(fā)布的,下面這篇文章主要給大家介紹了關(guān)于java調(diào)用WebService服務(wù)的四種方法,需要的朋友可以參考下2021-11-11mybatis 運(yùn)行時(shí)加載自定義mapper文件方式
這篇文章主要介紹了mybatis 運(yùn)行時(shí)加載自定義mapper文件方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07淺談Java設(shè)計(jì)模式之原型模式知識(shí)總結(jié)
Java原型模式主要用于創(chuàng)建重復(fù)的對(duì)象,同時(shí)又能保證性能,這篇文章就帶大家仔細(xì)了解一下原型模式的知識(shí),對(duì)正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下2021-05-05Spring如何通過(guò)@Lazy注解解決構(gòu)造方法循環(huán)依賴問(wèn)題
循環(huán)依賴其實(shí)就是循環(huán)引用,也就是兩個(gè)或則兩個(gè)以上的bean互相持有對(duì)方,最終形成閉環(huán),這篇文章主要給大家介紹了關(guān)于Spring如何通過(guò)@Lazy注解解決構(gòu)造方法循環(huán)依賴問(wèn)題的相關(guān)資料,需要的朋友可以參考下2023-03-03Java實(shí)現(xiàn)JDBC連接數(shù)據(jù)庫(kù)簡(jiǎn)單案例
這篇文章主要介紹了Java實(shí)現(xiàn)JDBC連接數(shù)據(jù)庫(kù)簡(jiǎn)單案例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08spring boot創(chuàng)建項(xiàng)目包依賴問(wèn)題的解決
本篇文章主要介紹了spring boot創(chuàng)建項(xiàng)目包依賴問(wèn)題的解決,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11