SpringBoot (6)监听器与应用上下文的关闭

2,047 阅读3分钟

监听器

前面所讲的监听器都是SpringBoot中的监听器,在创建应用上下文之后,刷新容器之前,会将监听器转给应用上下文(这一步的代码见SpringApplication->prepareContext():listeners.contextLoaded(context))。

总结

  1. SpringApplication实例化时,从spring.factories拿到org.springframework.context.ApplicationListener的全限定名,实例化,赋值给listeners成员变量
  2. SpringApplication启动时,从spring.factories拿到org.springframework.boot.SpringApplicationRunListener的全限定名,实例化,这里是EventPublishingRunListener类,将监听器添加到它的成员变量initialMulticaster的成员变量defaultRetrieverapplicationListeners
  3. ApplicationStartingEvent事件,监听器启动
  4. 环境创建完成,ApplicationEnvironmentPreparedEvent事件,加载配置文件
  5. IoC容器创建完成,ApplicationContextInitializedEvent事件
  6. IoC容器刷新之前,先将SpringBoot中的监听器赋给应用上下文,然后ApplicationPreparedEvent事件
  7. IoC容器单例bean实例化完成。从后面开始,监听器=上下文的listeners+容器中的ApplicationListener实现类。ContextRefreshedEvent事件,启动定时任务
  8. web服务器开启监听,ServletWebServerInitializedEvent事件
  9. IoC流程完成,ApplicationStartedEvent事件
  10. 启动完成,ApplicationReadyEvent事件

若以上任意一步失败,ApplicationFailedEvent事件
应用上下文关闭,ContextClosedEvent事件

可以看出,实现ApplicationListener接口并注入IoC容器中只能监听IoC实例化单例bean完成之后的事件,如果想要监听之前的事件的话,只能使用在spring.factories中配置的方式了。

应用上下文的关闭

bean的实例化过程中曾讲述过IoC阶段bean的销毁,除此之外,SpringBoot启动阶段,运行阶段,都有可能触发bean的销毁。

异常时关闭

SpringApplication->run():
    try {
        ......
    } catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }
SpringApplication->handleRunFailure():
    try {
        handleExitCode(context, exception);
        if (listeners != null) {
            // ApplicationFailedEvent事件
            listeners.failed(context, exception);
        }
    }
    finally {
        reportFailure(exceptionReporters, exception);
        // 如果上下文已创建,则调用close()方法
        if (context != null) {
            context.close();
        }
    }
AbstractApplicationContext->close():
    doClose(); // --1
    if (this.shutdownHook != null) {
        Runtime.getRuntime().removeShutdownHook(this.shutdownHook); // --2
    }

以上是SpringBoot启动阶段异常时的处理,1处是上下文的关闭,会在这里销毁bean。注意到2处有remove,说明其有add的情况。

SpringApplication->refreshContext():
    // IoC容器的刷新
    refresh(context);
    if (this.registerShutdownHook) {
        context.registerShutdownHook();
    }
AbstractApplicationContext->registerShutdownHook():
    if (this.shutdownHook == null) {
        this.shutdownHook = new Thread() {
            @Override
            public void run() {
                synchronized (startupShutdownMonitor) {
                    doClose();
                }
            }
        };
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
    }

shutdownHook是一个线程类,Runtime.getRuntime().addShutdownHook(Thread hook)将线程类添加,之后在JVM关闭之前一一调用线程方法。

运行时关闭

启动结束后,执行System.exit(0)关闭进程,最终进入AbstractApplicationContext->doClose()方法,注意到下面的调用栈,确实执行了shutdownHook的线程方法。

image.png

AbstractApplicationContext->doClose(): // --1
    // active和closed都是原子boolean类,确保多线程或重复调用时只关闭一次
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        // 发布ContextClosedEvent事件
        publishEvent(new ContextClosedEvent(this));
        // IoC容器中实现Lifecycle接口的bean,如果isRunning返回true的话,则执行stop方法
        this.lifecycleProcessor.onClose();
        // 摧毁bean
        destroyBeans();
        // 将上下文状态置为关闭
        closeBeanFactory();
        // 关闭和释放web服务器
        onClose();
        this.active.set(false);
    }

附录:docker中优雅关闭服务

杀死进程使用kill命令,kill -9是强制关闭,kill -15给进程做一些准备然后再关闭,对于Java服务,如果是强制关闭,就不会执行shutdownHook的线程方法了,如果是kill -15,则可以称其为优雅地关闭。

将SpringBoot部署到服务器中,一般是打成jar包,构建docker镜像和容器,放在docker中发布。如果要关闭服务,使用docker stop或者docker restart命令,然而只有pid=1的进程能收到中断信号, 如果容器的pid=1的进程是 sh 进程, 它不具备转发结束信号到它的子进程的能力,所以我们真正的java程序得不到中断信号, 也就不能实现优雅关闭。

最简单的解决思路是将Java程序配成pid=1的进程,在Dockerfile中将java -jar改成exec java -jar即可。

验证:

  1. 使用docker exec -it 容器id /bin/bash进入容器,再使用ps -ef查看进程,java进程是1。

  2. 执行docker stop,查看日志,@PreDestroy注解方法的log被输出,验证成功。

qq_pic_merged_1619568789843.jpg