监听器
前面所讲的监听器都是SpringBoot中的监听器,在创建应用上下文之后,刷新容器之前,会将监听器转给应用上下文(这一步的代码见SpringApplication->prepareContext():listeners.contextLoaded(context))。
总结
- SpringApplication实例化时,从
spring.factories拿到org.springframework.context.ApplicationListener的全限定名,实例化,赋值给listeners成员变量 - SpringApplication启动时,从
spring.factories拿到org.springframework.boot.SpringApplicationRunListener的全限定名,实例化,这里是EventPublishingRunListener类,将监听器添加到它的成员变量initialMulticaster的成员变量defaultRetriever的applicationListeners中 ApplicationStartingEvent事件,监听器启动- 环境创建完成,
ApplicationEnvironmentPreparedEvent事件,加载配置文件 - IoC容器创建完成,
ApplicationContextInitializedEvent事件 - IoC容器刷新之前,先将SpringBoot中的监听器赋给应用上下文,然后
ApplicationPreparedEvent事件 - IoC容器单例bean实例化完成。从后面开始,监听器=上下文的listeners+容器中的ApplicationListener实现类。
ContextRefreshedEvent事件,启动定时任务 - web服务器开启监听,
ServletWebServerInitializedEvent事件 - IoC流程完成,
ApplicationStartedEvent事件 - 启动完成,
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的线程方法。
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即可。
验证:
-
使用
docker exec -it 容器id /bin/bash进入容器,再使用ps -ef查看进程,java进程是1。 -
执行docker stop,查看日志,
@PreDestroy注解方法的log被输出,验证成功。