常見(jiàn)Java應(yīng)用如何優(yōu)雅關(guān)閉
一、前言
在我們進(jìn)行系統(tǒng)升級(jí)的時(shí)候,往往需要關(guān)閉我們的應(yīng)用,然后重啟。在關(guān)閉應(yīng)用前,我們希望做一些前置操作,比如關(guān)閉數(shù)據(jù)庫(kù)、redis連接,清理zookeeper的臨時(shí)節(jié)點(diǎn),釋放分布式鎖,持久化緩存數(shù)據(jù)等等。
二、Linux的信號(hào)機(jī)制
在linux上,我們關(guān)閉進(jìn)程主要是使用 kill 的方式。
當(dāng)執(zhí)行該命令以后,linux會(huì)向進(jìn)程發(fā)送一個(gè)信號(hào),進(jìn)程收到以后之后,可以做一些清理工作。
kill 命令默認(rèn)的信號(hào)值為 15 ,即 SIGTERM 信號(hào)。
通過(guò) kill -l 查看linux支持哪些信號(hào):
linux提供了 signal() api,可以將信號(hào)處理函數(shù)注冊(cè)上去:
- #include <signal.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdbool.h>
- static void gracefulClose(int sig)
- {
- printf("執(zhí)行清理工作\n");
- printf("JVM 已關(guān)閉\n");
- exit(0); //正常關(guān)閉
- }
- int main(int argc,char *argv[])
- {
- if(signal(SIGTERM,gracefulClose) == SIG_ERR)
- exit(-1);
- printf("JVM 已啟動(dòng)\n");
- while(true)
- {
- // 執(zhí)行工作
- sleep(1);
- }
- }
三、Java提供的Shutdown Hook
Java并不支持類(lèi)似于linux的信號(hào)機(jī)制,但是提供了 Runtime.addShutdownHook(Thread hook) 的api。
在JVM關(guān)閉前,會(huì)并發(fā)執(zhí)行各個(gè)Hook線程。
- public class ShutdownHook {
- public static void main(String[] args) throws InterruptedException {
- Runtime.getRuntime().addShutdownHook(new DbShutdownWork());
- System.out.println("JVM 已啟動(dòng)");
- while(true){
- Thread.sleep(10L);
- }
- }
- static class DbShutdownWork extends Thread{
- public void run(){
- System.out.println("關(guān)閉數(shù)據(jù)庫(kù)連接");
- }
- }
- }
四、Spring Boot提供的優(yōu)雅關(guān)閉功能
我們一般采用如下的方式,啟動(dòng)一個(gè)Spring boot應(yīng)用:
- public static void main(String[] args) throws Exception {
- SpringApplication.run(SampleController.class, args);
- }
SpringApplication.run()代碼如下,會(huì)調(diào)用到refreshContext(context)方法:
- public ConfigurableApplicationContext run(String... args) {
- StopWatch stopWatch = new StopWatch();
- stopWatch.start();
- ConfigurableApplicationContext context = null;
- FailureAnalyzers analyzers = null;
- configureHeadlessProperty();
- SpringApplicationRunListeners listeners = getRunListeners(args);
- listeners.started();
- try {
- ApplicationArguments applicationArguments = new DefaultApplicationArguments(
- args);
- ConfigurableEnvironment environment = prepareEnvironment(listeners,
- applicationArguments);
- Banner printedBanner = printBanner(environment);
- context = createApplicationContext();
- analyzers = new FailureAnalyzers(context);
- prepareContext(context, environment, listeners, applicationArguments,
- printedBanner);
- refreshContext(context);
- afterRefresh(context, applicationArguments);
- listeners.finished(context, null);
- stopWatch.stop();
- if (this.logStartupInfo) {
- new StartupInfoLogger(this.mainApplicationClass)
- .logStarted(getApplicationLog(), stopWatch);
- }
- return context;
- }
- catch (Throwable ex) {
- handleRunFailure(context, listeners, analyzers, ex);
- throw new IllegalStateException(ex);
- }
- }
refreshContext()方法比較簡(jiǎn)單:
- private void refreshContext(ConfigurableApplicationContext context) {
- refresh(context); //調(diào)用ApplicationContext.refresh()
- if (this.registerShutdownHook) { //registerShutdownHook默認(rèn)值為true
- try {
- context.registerShutdownHook();
- }
- catch (AccessControlException ex) {
- // Not allowed in some environments.
- }
- }
- }
AbstractApplicationContext.registerShutdownHook()代碼:
- public void registerShutdownHook() {
- if (this.shutdownHook == null) {
- this.shutdownHook = new Thread() {
- @Override
- public void run() {
- synchronized (startupShutdownMonitor) {
- doClose();
- }
- }
- };
- Runtime.getRuntime().addShutdownHook(this.shutdownHook);
- }
- }
很明顯,Spring boot通過(guò)在啟動(dòng)時(shí),向JVM注冊(cè)一個(gè)ShutdownHook,從而實(shí)現(xiàn)JVM關(guān)閉前,正常關(guān)閉Spring容器。而Spring在銷(xiāo)毀時(shí),會(huì)依次調(diào)用bean的destroy動(dòng)作來(lái)銷(xiāo)毀。
五、Dubbo的優(yōu)雅關(guān)閉策略
Dubbo同樣是基于ShutdownHook實(shí)現(xiàn)的。
AbstractConfig的static代碼:
- static {
- Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
- public void run() {
- if (logger.isInfoEnabled()) {
- logger.info("Run shutdown hook now.");
- }
- ProtocolConfig.destroyAll();
- }
- }, "DubboShutdownHook"));
- }
六、總結(jié)
只要我們的應(yīng)用運(yùn)行在linux平臺(tái)上,所有的優(yōu)雅關(guān)閉方案都是基于linux提供的信號(hào)機(jī)制提供的,JVM也是如此。
Java并沒(méi)有為我們提供與之一一對(duì)應(yīng)的api,而是給出了個(gè)ShutdownHook機(jī)制,也能達(dá)到類(lèi)似的效果,缺點(diǎn)是我們無(wú)法得知JVM關(guān)閉的原因。
像dubbo、spring boot等成熟的開(kāi)源框架,都實(shí)現(xiàn)了自動(dòng)注冊(cè)ShutdownHook的功能,從而避免使用者忘記調(diào)用優(yōu)雅關(guān)閉api引發(fā)問(wèn)題,降低框架的使用難度。