1. 前言
上一节我们介绍了生命周期管理的三个特点,针对其不足之处,引入了事件的概念。接着介绍了观察者模式,以及 Java 对订阅发布模式和监听器模式的支持。Spring 框架实现了基于监听器模式的事件机制,事件多播器持有一组监听器列表,可以发送多种类型的事件。本节的重点有两个,一是将 Spring 事件机制整合到 ApplicationContext 的现有流程之中,实现容器到组件、组件与组件之间自由、灵活的通信。二是实现声明式的监听器,即监听器方法。
2. Spring 整合
2.1 ApplicationEventPublisher
ApplicationEventPublisher 的接口的作用只有一个,就是发布事件。ApplicationContext 接口继承了 ApplicationEventPublisher 接口,从而拥有了发布事件的能力。
public interface ApplicationEventPublisher {
void publishEvent(ApplicationEvent event);
}
public interface ApplicationContext extends ApplicationEventPublisher {}
2.2 ConfigurableApplicationContext
ConfigurableApplicationContext 接口虽然定义了 addApplicationListener 方法,但这与观察者模式的主题角色没有任何关系。作为对外的配置接口,提供一个添加组件的方法无可厚非,事实上该方法主要是框架内部在使用。此外,没有对应的移除监听器的方法也能说明问题。
public interface ConfigurableApplicationContext {
void addApplicationListener(ApplicationListener<?> listener);
}
2.3 监听器的适配
ApplicationContext 持有一个 ApplicationEventMulticaster 实例,事件多播器存储了所有的监听器。Spring 实现了两种类型的监听器,一种是监听器方法,即声明了 @EventListener 注解的方法。另一种是监听器类,直接实现 ApplicationListener 接口即可。为了统一调用逻辑,这两种监听器都需要先包装成适配器对象,然后再注册到事件多播器中。
3. 监听器类
监听器类除了直接实现 ApplicationListener 接口之外,同时还是 Spring 容器管理的单例。关于监听器类的初始化操作,我们在容器的刷新流程中一并介绍。至于监听器类的事件回调,已经在事件多播器的 multicastEvent 方法中介绍过了。
3.1 容器初始化
AbstractApplicationContext 类新增了两个字段,其中 applicationEventMulticaster 表示事件多播器,applicationListeners 表示通过 addApplicationListener 方法手动添加的监听器类。在 refresh 方法中,有多个步骤与事件有关。大多数的操作是事件机制的通用逻辑,只有少部分是属于监听器类的,至于监听器方法的处理另有一套解决方案。
public abstract class AbstractApplicationContext {
//事件多播器
private ApplicationEventMulticaster applicationEventMulticaster;
//临时存储的监听器类列表
private final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();
@Override
public void refresh() {
//3. ApplicationContext使用BeanFactory前的准备工作,比如添加必要的组件
prepareBeanFactory(beanFactory);
//7. 注册事件多播器,默认为SimpleApplicationEventMulticaster
initApplicationEventMulticaster(beanFactory);
//9. 注册监听器,发送早期事件(earlyApplicationEvents)
registerListeners();
//11. 发送refresh完成事件
finishRefresh();
}
}
第三步属于准备阶段,prepareBeanFactory 方法负责注册基础组件。这里将容器本身注册为 ApplicationEventPublisher 类型的依赖项,届时在业务代码中注入 ApplicationEventPublisher 实例来发布事件。
//step-3 准备工作
private void prepareBeanFactory(ConfigurableBeanFactory beanFactory) {
//将ApplicationEventPublisher注册为依赖项
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
}
第七步,创建事件多播器实例,并注册成一个单例。这意味着我们有三种方式来发布事件,一是注入 ApplicationEventMulticaster 类型的依赖,二是注入 ApplicationEventPublisher 类型的依赖,三是注入 ApplicationContext 类型的依赖。其中,后两种类型都是 Spring 容器本身。
注:第七步和第九步可以看做一个整体。两个步骤之所以分开,是因为第八步
onRefresh是一个扩展方法,子类有可能会向 Spring 容器中添加新的监听器类。
//step-7 注册事件多播器
protected void initApplicationEventMulticaster(ConfigurableBeanFactory beanFactory) {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster();
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
}
第九步负责将所有的监听器对象注册到 ApplicationEventMulticaster 组件中,需要注意的是,监听器对象有两个来源:
- 临时存放在
applicationEventMulticaster字段,这些是手动注册的监听器类 - 注册在 Spring 容器中的监听器单例,通过扫描或工厂方法的方式加载的监听器类
//step-9 注册监听器类
protected void registerListeners() {
//注册手动添加的监听器
for (ApplicationListener<?> listener : this.applicationListeners) {
this.applicationEventMulticaster.addApplicationListener(listener);
}
//注册BeanFactory中的事件监听器
List<String> names = getBeanNamesForType(ApplicationListener.class);
for (String name : names) {
this.applicationEventMulticaster.addApplicationListener((ApplicationListener<?>) getBean(name));
}
}
finishRefresh 方法是容器刷新操作的最后一步,之前我们处理了 Lifecycle 组件的初始化操作,现在还需要发送 ContextRefreshedEvent 事件。此时,Spring 容器的启动流程执行完毕,应用程序是可用的。
protected void finishRefresh() {
//处理Lifecycle(略)
//发送ContextRefreshedEvent事件
publishEvent(new ContextRefreshedEvent(this));
}
3.2 容器关闭
不管是调用 close 方法手动关闭,还是通过向 JVM 注册钩子自动关闭,最终都会执行 doClose 方法,发送容器关闭事件。
protected void doClose() {
//发送容器关闭事件
publishEvent(new ContextClosedEvent(this));
//处理Lifecycle(略)
//销毁所有的单例(略)
}
4. 监听器方法
4.1 概述
监听器方法是指声明了 @EventListener 注解的方法,只要是由 Spring 容器管理的单例都可以定义监听器方法。@EventListener 注解的 value 属性的类型是 Class[],表示监听的一组事件。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EventListener {
//指定监听的事件(组)
Class<?>[] value() default {};
}
监听器方法由 EventListenerMethodProcessor 类进行处理,并包装成 ApplicationListenerMethodAdapter 适配器类。与 @Autowired、@Configuration 等注解类似,负责解析 @EventListener 注解的组件也是通过 AnnotationConfigUtils 工具类的 registerAnnotationConfigProcessors 方法注册到 Spring 容器中。
public class AnnotationConfigUtils {
public static final String EVENT_LISTENER_PROCESSOR_BEAN_NAME = "internalEventListenerProcessor";
public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {
//注册支持@Autowired和@Value注解的组件(略)
//注册支持@Configuration注解的组件(略)
//注册支持@PostConstruct和@PreDestroy注解的组件(略)
//注册支持@EventListener注解的组件
if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);
registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME);
}
}
}
4.2 监听器方法的初始化
EventListenerMethodProcessor 实现了 SmartInitializingSingleton 接口,说明该组件的是在所有的单例 Bean 实例化之后执行。在 afterSingletonsInstantiated 方法中,此时容器中所有的单例已实例化,遍历所有单例,依次执行 processBean 方法。
public class EventListenerMethodProcessor implements SmartInitializingSingleton, ApplicationContextAware {
@Override
public void afterSingletonsInstantiated() {
//遍历所有的单例Bean
List<String> beanNames = this.applicationContext.getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
Class<?> type = this.applicationContext.getType(beanName);
processBean(beanName, type);
}
}
}
在 processBean 方法中,首先找出指定类型中所有声明了 @EventListener 注解的方法,如果监听器方法存在,则为每一个监听器方法创建一个适配器对象。然后将适配器对象注册到 ApplicationEventMulticaster 组件的监听器列表中。
//解析单例,寻找监听器方法,并包装成适配器对象
protected void processBean(final String beanName, final Class<?> targetType) {
if (!this.nonAnnotatedClasses.contains(targetType)) {
//1) 寻找所有声明了@EventListener注解的方法
Map<Method, EventListener> annotatedMethods = MethodIntrospector.selectMethods(targetType,
new MethodIntrospector.MetadataLookup<EventListener>() {
@Override
public EventListener inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, EventListener.class);
}
});
if (CollectionUtils.isEmpty(annotatedMethods)) {
this.nonAnnotatedClasses.add(targetType);
}else{
//2) 为每个监听方法创建一个适配器对象,并添加到监听器集合中
for (Method method : annotatedMethods.keySet()) {
ApplicationListener<?> listener = new ApplicationListenerMethodAdapter(beanName, targetType, method, this.applicationContext);
this.applicationContext.addApplicationListener(listener);
}
}
}
}
4.3 适配器的监听回调
当应用程序发布事件时,事件多播器从监听器列表中寻找感兴趣的监听器集合,遍历执行每个监听器的 onApplicationEvent 回调方法。对于监听器方法,这一工作是由适配器类 ApplicationListenerMethodAdapter 完成的。onApplicationEvent 方法可以分为两步:
- 对参数进行解析,由于监听器方法是通过反射的方式调用方法,参数的类型必须是
Object[],因此需要对原始的事件类型进行转换。 - 通过反射的方式调用监听器方法,执行监听回调
public class ApplicationListenerMethodAdapter implements GenericApplicationListener {
private final String beanName;
private final Method bridgedMethod;
private final List<ResolvableType> declaredEventTypes;
private ApplicationContext applicationContext;
@Override
public void onApplicationEvent(ApplicationEvent event) {
Object[] args = resolveArguments(event);
doInvoke(args);
}
}
第一步,解析参数。在 resolveArguments 方法中,首先检查监听器方法是否有资格执行,具体来说就是判断 @EventListener 注解声明的事件列表(存储在 declaredEventTypes 字段中)与传入的事件类型是否匹配。其次是对方法参数的处理,如果没有参数返回一个空数组,否则将传入的事件作为参数。从这里可以看到,监听器方法的一大特点,要么无参,要么只有一个参数,且必须是 ApplicationEvent 或子类型。
//解析监听器方法的参数
protected Object[] resolveArguments(ApplicationEvent event) {
//如果@EventListener注解声明的事件列表与监听器方法的参数不匹配,则返回空
ResolvableType declaredEventType = getResolvableType(event);
if (declaredEventType == null) {
return null;
}
//方法没有参数,返回空数组
if (this.method.getParameterTypes().length == 0) {
return new Object[0];
}
//将事件对象作为方法的参数
return new Object[] {event};
}
第二步,通过反射的方式调用监听方法。反射调用需要满足三个条件,声明类的实例、目标方法、传入的参数。此时我们已经通过解析得到了参数,目标方法本来就是适配器类的 bridgedMethod 字段,声明类的实例可以从 Spring 容器中获得(监听器方法往往定义在一个配置类或组件类中)。
//调用监听器方法
protected Object doInvoke(Object... args) {
//获取声明类实例
Object bean = this.applicationContext.getBean(this.beanName);
//bridgedMethod字段就是监听器方法
ReflectionUtils.makeAccessible(this.bridgedMethod);
return this.bridgedMethod.invoke(bean, args);
}
通过反射的方式调用方法,我们需要特别注意参数的类型。在示例代码中,监听器方法支持两种类型的事件,但是方法参数指定为 AEvent。当发送 BEvent 事件时,由于传入的事件类型与方法参数类型不一致(argument type mismatch),会导致抛出 IllegalArgumentException 异常。因此,如果监听器方法支持多种事件,方法参数必须是所有事件的共同父类,比如 ApplicationEvent 或 Object 类型。
//示例代码,监听器方法的参数问题
@EventListener({AEvent.class, BEvent.class})
public void XxxEvent(AEvent event) {
//当发送BEvent事件时,反射调用方法会报错
}
5. 整体流程分析
Spring 容器在对两种类型的监听器进行处理时,既有各自的逻辑,也有共同的逻辑。接下来,我们回顾有关事件机制的所有操作,从整体层面加深认识。整个流程分为七个步骤,如下所示:
AnnotationConfigUtils工具类的registerAnnotationConfigProcessors方法,注册EventListenerMethodProcessor组件。AbstractApplicationContext的refresh方法中执行 2~6 步。首先调用prepareBeanFactory方法添加ApplicationEventPublisher类型的依赖,实际上是容器自身。- 调用
initApplicationEventMulticaster方法注册事件多播器组件。 - 调用
registerListeners方法注册监听器类,具体是通过事件多播器将监听器类对象转换成GenericApplicationListenerAdapter适配器对象。 - 调用
finishBeanFactoryInitialization方法,回调SmartInitializingSingleton接口,通过EventListenerMethodProcessor组件将监听器方法转换为ApplicationListenerMethodAdapter适配器对象。 - 调用
finishRefresh方法,发布容器刷新事件。 - 当容器关闭时,回调
onClose方法,发布容器关闭事件。
6. 测试
6.1 监听器类
在测试方法中,我们向 Spring 容器注册了三个监听器类(当然也可以通过组件扫描或 BeanMethod 的方式),在容器的刷新过程中,会侦测到这些监听器类,并注册到事件多播器的监听器列表中。最后分别发送了两个事件,由监听器负责接收。
//测试方法
@Test
public void testEventListener(){
GenericApplicationContext context = new GenericApplicationContext();
ConfigurableBeanFactory factory = context.getBeanFactory();
factory.registerSingleton("l1", new NewsListener("张三"));
factory.registerSingleton("l2", new NewsListener("李四"));
factory.registerSingleton("l3", new MilkListener("李四"));
context.refresh();
context.publishEvent(new NewsEvent("《人民日报》"));
context.publishEvent(new MilkEvent("光明牛奶"));
}
从测试结果来看,张三只监听了报纸的事件,而李四监听了报纸和牛奶的事件,符合预期。
张三读 《人民日报》
李四读 《人民日报》
李四喝 光明牛奶
6.2 监听器方法
首先在配置类 EventConfig 中定义两个监听器方法,其中一个只监听 NewsEvent,另一个同时监听 NewsEvent 和 MilkEvent 两个事件。
//测试类:配置两个监听方法
@Configuration
public class EventConfig {
//监听单一事件
@EventListener(NewsEvent.class)
public void newsListener(NewsEvent event){
System.out.println("张三读 " + event.getSource());
}
//监听多个事件,方法参数必须是事件列表的共同父类,比如ApplicationEvent或Object
@EventListener({NewsEvent.class, MilkEvent.class})
public void newsAndMilkListener(ApplicationEvent event){
if (event instanceof NewsEvent) {
System.out.println("李四读 " + event.getSource());
}
if (event instanceof MilkEvent) {
System.out.println("李四喝 " + event.getSource());
}
}
}
在测试方法中,Spring 容器的类型是 AnnotationConfigApplicationContext,自动注册 EventListenerMethodProcessor 组件,然后对配置类 EventConfig 进行解析,将两个监听器方法包装成适配器对象,并添加到 Spring 容器的监听器列表中。最后分别发送了两个事件,由监听器负责接收。
//测试方法
@Test
public void testEventMethod(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(EventConfig.class);
context.publishEvent(new NewsEvent("《人民日报》"));
context.publishEvent(new MilkEvent("光明牛奶"));
}
测试结果与上一个测试相同,这说明监听器方法和监听器类的功能是相同的。
张三读 《人民日报》
李四读 《人民日报》
李四喝 光明牛奶
6.3 容器事件
除了监听自定义事件,我们也可以监听容器的生命周期。定义两个监听器类,分别监听容器的刷新和关闭事件。ContextRefreshedEvent 和 ContextClosedEvent 是 Spring 预定义的容器事件。
//测试类
public class SimpleContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("监听容器刷新事件");
}
}
public class SimpleContextClosedListener implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("监听容器关闭事件");
}
}
在测试方法中,我们先将两个监听器类注册到 Spring 容器中,并向虚拟机注册销毁的回调。然后执行 refresh 方法,在容器刷新流程的最后阶段,发送 ContextRefreshedEvent 事件。代码执行完毕,由虚拟机触发 close 回调,发送 ContextClosedEvent 事件。
//测试方法
@Test
public void testContextEvent() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(SimpleContextRefreshedListener.class);
context.register(SimpleContextClosedListener.class);
//注册虚拟机钩子,当程序调用完毕后,由JVM来回调close方法,发送ContextClosedEvent
context.registerShutdownHook();
context.refresh();
}
从测试结果可以看到,两个监听器分别对指定的事件进行了拦截。
监听容器刷新事件
监听容器关闭事件
7. 总结
本节我们讨论了 Spring 容器与事件机制的整合,ApplicationContext 通过继承 ApplicationEventPublisher 接口,从而拥有了发布事件的能力。此外,ApplicationContext 持有一个事件多播器实例,负责处理事件的具体逻辑。
Spring 实现了两种类型的监听器,即监听器类和监听器方法。为了统一调用逻辑,这两种监听器都需要先包装成适配器对象,然后再注册到事件多播器中。我们得到了两个非常重要的结论:其一,事件多播器存储了所有的监听器。其二,这些监听器对象是以适配器的形式存在。
此外,在 Spring 容器的生命周期中,在特定的时间点发布事件,比如容器刷新操作的结尾发布 ContextRefreshedEvent 事件,当容器关闭时发布 ContextClosedEvent 方法。我们可以通过监听器类或监听器方法的形式,来监听感兴趣的事件。
8. 项目信息
新增修改一览,新增(10),修改(5)。
context
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring.context
│ ├─ annotation
│ │ └─ AnnotationConfigUtils.java (*)
│ ├─ event
│ │ ├─ ApplicationContextEvent.java (+)
│ │ ├─ ApplicationEventPublisher.java (+)
│ │ ├─ ApplicationListenerMethodAdapter.java (+)
│ │ ├─ ContextClosedEvent.java (+)
│ │ ├─ ContextRefreshedEvent.java (+)
│ │ ├─ EventListener.java (+)
│ │ └─ EventListenerMethodProcessor.java (+)
│ ├─ support
│ │ └─ AbstractApplicationContext.java (*)
│ ├─ ApplicationContext.java(*)
│ └─ ConfigurableApplicationContext.java(*)
└─ test
└─ java
└─ context
└─ event
├─ EventConfig.java (+)
├─ EventTest.java (*)
├─ SimpleContextClosedListener.java (+)
└─ SimpleContextRefreshedListener.java (+)
注:+号表示新增、*号表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。