在我们进行系统升级的时候,往往需要关闭我们的应用,然后重启。在关闭应用前,我们希望做一些前置操作,比如关闭数据库、redis 连接,清理 zookeeper 的临时节点,释放分布式锁,持久化缓存数据等等。
在 linux 上,我们关闭进程主要是使用 kill <pid> 的方式。
当执行该命令以后,linux 会向进程发送一个信号,进程收到以后之后,可以做一些清理工作。
kill 命令默认的信号值为
,即 SIGTERM 信号。
- 15
通过 kill -l 查看 linux 支持哪些信号:
linux 提供了 signal() api,可以将信号处理函数注册上去:
- #include <signal.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdbool.h>
- static void gracefulClose(int sig)
- {
- printf("执行清理工作\n");
- printf("JVM 已关闭\n");
- exit(0); //正常关闭
- }
- int main(int argc,char *argv[])
- {
- if(signal(SIGTERM,gracefulClose) == SIG_ERR)
- exit(-1);
- printf("JVM 已启动\n");
- while(true)
- {
- // 执行工作
- sleep(1);
- }
- }
Java 并不支持类似于 linux 的信号机制,但是提供了
的 api。
- Runtime.addShutdownHook(Thread hook)
在 JVM 关闭前,会并发执行各个 Hook 线程。
- public class ShutdownHook {
- public static void main(String[] args) throws InterruptedException {
- Runtime.getRuntime().addShutdownHook(new DbShutdownWork());
- System.out.println("JVM 已启动");
- while (true) {
- Thread.sleep(10L);
- }
- }
- static class DbShutdownWork extends Thread {
- public void run() {
- System.out.println("关闭数据库连接");
- }
- }
- }
我们一般采用如下的方式,启动一个 Spring boot 应用:
- public static void main(String[] args) throws Exception {
- SpringApplication.run(SampleController.class, args);
- }
SpringApplication.run() 代码如下,会调用到 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() 方法比较简单:
- private void refreshContext(ConfigurableApplicationContext context) {
- refresh(context); //调用ApplicationContext.refresh()
- if (this.registerShutdownHook) { //registerShutdownHook默认值为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 通过在启动时,向 JVM 注册一个 ShutdownHook,从而实现 JVM 关闭前,正常关闭 Spring 容器。而 Spring 在销毁时,会依次调用 bean 的 destroy 动作来销毁。
Dubbo 同样是基于 ShutdownHook 实现的。
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"));
- }
只要我们的应用运行在 linux 平台上,所有的优雅关闭方案都是基于 linux 提供的信号机制提供的, JVM 也是如此。
Java 并没有为我们提供与之一一对应的 api,而是给出了个 ShutdownHook 机制,也能达到类似的效果,缺点是我们无法得知 JVM 关闭的原因。
像 dubbo、spring boot 等成熟的开源框架,都实现了自动注册 ShutdownHook 的功能,从而避免使用者忘记调用优雅关闭 api 引发问题,降低框架的使用难度。
来源: http://www.tuicool.com/articles/EZFFVf3