SpringBoot版本:2.0.2.RELEASE
SpringFramework版本:5.0.6.RELEASE
谁是SpringFramework的事件发布器
从UML图看,ApplicationEventPublisher只声明了发布事件的方法,到了ConfigurableApplicationContext才补充了与监听器关联的方法。这些方法最终由AbstractApplicationContext实现。而它的子类一般也没有重载这些方法。因此,从类定义的角度来看,AbstractApplicationContext承担了发布事件的责任。
那么在一个SpringBoot工程里,它是如何成为事件发布者呢?在前文多处已经暗示,有一个发布器移交的这么一个过程,详见EventPublishingRunListener的各发布事件方法。
在SpringApplication#run方法的整体过程控制下,在SpringBoot发布ApplicationPreparedEvent事件的时候,有个将SpringBoot自己的Listener添加到ConfigurableApplicationContext的动作。然后AbstractApplicationContext#refresh方法会被调用,在该方法里完成了加载SpringFramework的监听器和初始化了SpringFramework层面的事件发布器SimpleApplicationEventMulticaster,最后调用finishRefresh方法发布ContextRefreshedEvent事件。下面列出AbstractApplicationContext#refresh方法内重点函数:
@Override
public void refresh() throws BeansException, IllegalStateException {
....
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
....
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
....
// Check for listener beans and register them.
registerListeners();
....
// Last step: publish corresponding event.
finishRefresh();
}
这些函数不难,读者可自行翻阅。但有一点要注意的整个SpringBoot启动过程中,事件发布的顺序,以及在发布各个事件的时候系统为此做了什么准备工作。控制着这些节奏的其实是SpringApplication,在它的run方法里已经编好了整个大纲。
双子座的消息发布者
在《SpringBoot事件与监听机制》已经知道SpringBoot的事件发布器是EventPublishingRunListener,而它最终是委托SimpleApplicationEventMulticaster的来完成事件的发布。
在EventPublishingRunListener#contextLoaded方法看到为发点布器交棒的准备,在started和running方法,已经看到发布事件的工作交给了ConfigurableApplicationContext去完成。我们回头看contextLoaded方法,只是将SpringBoot的监听器添加到ConfigurableApplicationContext,但并没有将自己的SimpleApplicationEventMulticaster交过去,那么ConfigurableApplicationContext是用什么来发布信息呢?
在AbstractApplicationContext里我们看到ApplicationEventMulticaster接口类型属性的声明:
/** Helper class used in event publishing */
@Nullable
private ApplicationEventMulticaster applicationEventMulticaster;
在AbstractApplicationContext#refresh方法里会调用initApplicationEventMulticaster函数,里面就对该变量进行赋值:
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isDebugEnabled()) {
logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
"': using default [" + this.applicationEventMulticaster + "]");
}
}
}
就是说,AbstractApplicationContext也有一个SimpleApplicationEventMulticaster作为自己的属性,名字叫applicationEventMulticaster。
我们再来观察AbstractApplicationContext是如何发布事件的,我们从publishEvent(ApplicationEvent event)方法开始跟踪,然后会来到publishEvent(Object event, @Nullable ResolvableType eventType)方法:
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
....
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}
getApplicationEventMulticaster方法如下:
ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {
if (this.applicationEventMulticaster == null) {
throw new IllegalStateException("ApplicationEventMulticaster not initialized - " +
"call 'refresh' before multicasting events via the context: " + this);
}
return this.applicationEventMulticaster;
}
到这里就明白了,SpringFramwork最终也是用SimpleApplicationEventMulticaster来发布事件,但此SimpleApplicationEventMulticaster并不是SpringBoot的SimpleApplicationEventMulticaster。那么这样的话会产生什么效果呢?前文已多次讲到在AbstractApplicationContext#refresh方法执行前,在EventPublishingRunListener#contextLoaded方法里已经将SpringBoot监听器添加给ConfigurableApplicationContext(即AbstractApplicationContext),所以后续SpringBoot委托AbstractApplicationContext发布的任何事件,SpringBoot的监听器都有可能会收到(“有可能”是因为还有事件类型以及事件源类型匹配的问题在里面)。
从EventPublishingRunListener各发布事件的方法看来,SpringBoot层面的SimpleApplicationEventMulticaster只是起到临时发布事件的作用,在ConfigurableApplicationContext未完成refresh前,SpringBoot启动经历的各状态先由SpringBoot层面的SimpleApplicationEventMulticaster发布事件。当ConfigurableApplicationContext完成refresh后,就交由它去完成发布事件的工作。那么为什么SpringBoot不把自己的EventPublishingRunListener传给ConfigurableApplicationContext呢?其实在SpringBoot1.x版本里确实是这样做的,到了2.x版本就调整成现在这样。为什么有这个调整,我猜SpringBoot毕竟是基于SpringFramework发展而来,但它毕竟不是SpringFramework的一部分,二者存在一定的独立性,所以作者有这么一个调整。以后随着实践的反馈,往后会怎么调整计划,我们再一起见证吧。
发布的事件类型
Spring Framework除了支持实现了ApplicationEvent接口事件的发布外,还支持泛型事件的发布,具体代码位置在AbstractApplicationContext#publishEvent:
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
....
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
}
else {
applicationEvent = new PayloadApplicationEvent<>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType();
}
}
// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
....
}
当事件不属于ApplicationEvent类型的时候,就以PayloadApplicationEvent进行封装,后续会有专门的文章介绍。在此先有个印象,Spring Framework是支持泛型事件的发布。
自定义事件监听者
有了《SpringBoot事件与监听机制》和本文的探讨,我们大概知道知道SpringBoot的启停过程中如何加载和交接监听器。我们可以粗略画出以下图:
下面的实验希望达到这样的效果:
1.有个监听器能监听到所有的事件
2.有个监听器只能监听SrpingBoot的一种事件(及子事件)和SpringFramework的两种事件(及子事件)。
3.监听器能够通过配置的形式加载到系统里
经过前文的分析,如果我们想添加自定义的监听器的话,最合适的方式莫过于在/Meta-INF/spring.factories添加配置了。
为了实现本实验功能,我们自定义两个配置类:
- CustomizedListener1
它要监听所有的事件,比较方便的方法是实现ApplicationListener接口并指定泛型为ApplicationEvent
public class CustomerizedListener1 implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println(Thread.currentThread().getName() + ":CustomerizedListener1:" + event.getClass().getName());
}
}
- CustomerizedListener2
它只监听部分的事件,可实现GenericApplicationListener接口或者SmartApplicationListener接口
public class CustomerizedListener2 implements GenericApplicationListener {
@Override
public boolean supportsEventType(ResolvableType eventType) {
Class<?> type = eventType.getRawClass();
if (type == null) {
return false;
}
return ApplicationStartingEvent.class.isAssignableFrom(type)
|| ContextRefreshedEvent.class.isAssignableFrom(type)
|| ContextClosedEvent.class.isAssignableFrom(type);
}
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println( Thread.currentThread().getName() + ":CustomizedListener2:" + event.getClass().getName() );
}
@Override
public int getOrder() {
return 0;
}
}
- spring.factory配置如下:
org.springframework.context.ApplicationListener=test.listener.CustomerizedListener1,test.listener.CustomerizedListener2
运行结果如下:
D:\Software\Java\jdk1.8.0_111\bin\java.exe "-javaagent:D:\Software\JetBrains\IntelliJ IDEA Community Edition 2019.3.4\lib\idea_rt.jar=55667:D:\Software\JetBrains\IntelliJ IDEA Community Edition 2019.3.4\bin" -Dfile.encoding=UTF-8 -classpath D:\Software\Java\jdk1.8.0_111\jre\lib\charsets.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\deploy.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\access-bridge-64.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\cldrdata.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\dnsns.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\jaccess.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\jfxrt.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\localedata.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\nashorn.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\sunec.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\sunjce_provider.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\sunmscapi.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\sunpkcs11.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\ext\zipfs.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\javaws.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\jce.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\jfr.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\jfxswt.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\jsse.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\management-agent.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\plugin.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\resources.jar;D:\Software\Java\jdk1.8.0_111\jre\lib\rt.jar;D:\Temp\mywebapp\target\classes;D:\m2_repository\org\springframework\boot\spring-boot-loader\2.0.2.RELEASE\spring-boot-loader-2.0.2.RELEASE.jar;D:\m2_repository\org\springframework\boot\spring-boot-starter\2.0.2.RELEASE\spring-boot-starter-2.0.2.RELEASE.jar;D:\m2_repository\org\springframework\boot\spring-boot\2.0.2.RELEASE\spring-boot-2.0.2.RELEASE.jar;D:\m2_repository\org\springframework\spring-context\5.0.6.RELEASE\spring-context-5.0.6.RELEASE.jar;D:\m2_repository\org\springframework\spring-aop\5.0.6.RELEASE\spring-aop-5.0.6.RELEASE.jar;D:\m2_repository\org\springframework\spring-beans\5.0.6.RELEASE\spring-beans-5.0.6.RELEASE.jar;D:\m2_repository\org\springframework\spring-expression\5.0.6.RELEASE\spring-expression-5.0.6.RELEASE.jar;D:\m2_repository\org\springframework\boot\spring-boot-autoconfigure\2.0.2.RELEASE\spring-boot-autoconfigure-2.0.2.RELEASE.jar;D:\m2_repository\org\springframework\boot\spring-boot-starter-logging\2.0.2.RELEASE\spring-boot-starter-logging-2.0.2.RELEASE.jar;D:\m2_repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\m2_repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\m2_repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\m2_repository\org\apache\logging\log4j\log4j-to-slf4j\2.10.0\log4j-to-slf4j-2.10.0.jar;D:\m2_repository\org\apache\logging\log4j\log4j-api\2.10.0\log4j-api-2.10.0.jar;D:\m2_repository\org\slf4j\jul-to-slf4j\1.7.25\jul-to-slf4j-1.7.25.jar;D:\m2_repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\m2_repository\org\springframework\spring-core\5.0.6.RELEASE\spring-core-5.0.6.RELEASE.jar;D:\m2_repository\org\springframework\spring-jcl\5.0.6.RELEASE\spring-jcl-5.0.6.RELEASE.jar;D:\m2_repository\org\yaml\snakeyaml\1.19\snakeyaml-1.19.jar;D:\m2_repository\com\fasterxml\jackson\core\jackson-databind\2.9.5\jackson-databind-2.9.5.jar;D:\m2_repository\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;D:\m2_repository\com\fasterxml\jackson\core\jackson-core\2.9.5\jackson-core-2.9.5.jar test.MyClass
main:CustomizedListener2:org.springframework.boot.context.event.ApplicationStartingEvent
main:CustomerizedListener1:org.springframework.boot.context.event.ApplicationStartingEvent
main:CustomerizedListener1:org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.2.RELEASE)
2020-08-16 15:47:42.608 INFO 9908 --- [ main] test.MyClass : Starting MyClass on yangyongbin-PC with PID 9908 (D:\Temp\mywebapp\target\classes started by yangyongbin in D:\Temp\mywebapp)
2020-08-16 15:47:42.620 INFO 9908 --- [ main] test.MyClass : No active profile set, falling back to default profiles: default
main:CustomerizedListener1:org.springframework.boot.context.event.ApplicationPreparedEvent
2020-08-16 15:47:42.779 INFO 9908 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6591f517: startup date [Sun Aug 16 15:47:42 CST 2020]; root of context hierarchy
2020-08-16 15:47:46.840 INFO 9908 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
main:CustomizedListener2:org.springframework.context.event.ContextRefreshedEvent
main:CustomerizedListener1:org.springframework.context.event.ContextRefreshedEvent
2020-08-16 15:47:46.878 INFO 9908 --- [ main] test.MyClass : Started MyClass in 5.234 seconds (JVM running for 6.8)
main:CustomerizedListener1:org.springframework.boot.context.event.ApplicationStartedEvent
main:CustomerizedListener1:org.springframework.boot.context.event.ApplicationReadyEvent
2020-08-16 15:47:46.895 INFO 9908 --- [ Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@6591f517: startup date [Sun Aug 16 15:47:42 CST 2020]; root of context hierarchy
Thread-2:CustomizedListener2:org.springframework.context.event.ContextClosedEvent
Thread-2:CustomerizedListener1:org.springframework.context.event.ContextClosedEvent
2020-08-16 15:47:46.899 INFO 9908 --- [ Thread-2] o.s.j.e.a.AnnotationMBeanExporter : Unregistering JMX-exposed beans on shutdown
Process finished with exit code 0
从运行结果可知,CustomerizedListener1监听了全部的事件,CustomerizedListener2也监听到了指定的事件。并且它们都是在另一个线程里监听到ContextClosedEvent事件。这些都符合前文的描述。
在《SpringFramework事件与监听机制(事件)》文章里介绍了ContextClosedEvent事件是怎么样在另一个线程发布的。