【重写SpringFramework】Spring事件机制上(chapter 3-11)观察者模式是常用的设计模式之一,
1. 前言
上一节我们讨论了生命周期管理的问题,无论是 Bean 的生命周期还是 Lifecycle 机制都是由 Spring 容器驱动的。它们有三个特点,一是在特定时间点执行具有一定语义的操作,比如初始化和销毁。二是这些操作是容器到组件的单向通知。第三点,容器直接调用组件的相关方法,两者的耦合程度太高。
Spring 提供了事件机制,解决了上述的三个痛点。首先,执行时机不受限制,允许在任意时间点发送事件。其次,事件既可以是容器到组件的通信,也可以是组件之间的通信。三是将容器与组件进行解耦,容器只管发送事件即可,监听器组件会侦测到这些事件,并进行相应的处理。
2. 观察者模式
2.1 概述
观察者模式定义了一种一对多的依赖关系,让多个观察者对象监听同一个主题对象(被观察者),当主题对象的状态发生变化时,会通知所有的观察者对象做出反应。观察者模式实现了主题对象与观察者对象之间的解耦,主题对象只需要发送事件,而观察者只关注如何处理事件,双方并不知道对方的存在。观察者模式由以下四个角色构成,简单介绍如下:
-
Subject
接口定义了管理观察者的方法addObserver
和removeObserver
,以及通知所有观察者的方法notifyObservers
。 -
ConcreteSubject
持有一个观察者的集合observers
,并实现了Subject
接口的相关方法。 -
Observer
接口代表一个观察者对象,update
方法定义了观察者应该如何处理事件 -
ConcreteObserver
代表一个具体的观察者对象,需要实现update
方法的具体逻辑
2.2 订阅发布模式
Java 提供了观察者模式的简单实现,主要涉及 java.util
包中的 Observer
接口和 Observable
类。其中 Observer
接口表示一个观察者对象,当被观察者对象的状态发生变化时,会调用观察者的 update
方法进行更新。
public interface Observer {
void update(Observable o, Object arg);
}
Observable
类代表一个被观察对象,有时也称为主题对象。一个被观察对象可以有数个观察者对象,每个观察者对象都是 Observer
接口的实现类。当被观察对象发生变化时,会调用 notifyObservers
方法,通知所有观察者调用 update
方法进行更新。此外,addObserver
和 removeObserver
等方法是用来管理观察者的。
public class Observable {
private Vector<Observer> obs; //观察者列表
//通知观察者
public void notifyObservers(Object arg) {
Object[] arrLocal = obs.toArray();
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
//添加一个观察者
public synchronized void addObserver(Observer o) {
if (!obs.contains(o)) {
obs.addElement(o);
}
}
}
所谓的简单实现包括两方面,一是结构简化,将抽象主题角色与具体主题角色合并为一个 Observable
类,二是使用场景比较简单,所有的观察者都订阅同一个主题,主题对象适时地发布事件,并通知所有已订阅的观察者。需要注意的是,「通知所有观察者」这一行为说明,一个主题只发布一种事件,否则就要对所有的观察者进行筛选。
举个例子,张三订了一份报纸,每天早上都会收到一份报纸。如果李四也订报纸,同样会收到一份报纸。我们把报纸看成订阅关系中传递的数据,就会发现张三和李四其实并不关心数据的类型,因为他们肯定会收到一份报纸,而不是一瓶牛奶。除非他们又订了牛奶,但这是新的订阅关系了。由于一个订阅关系只处理一种事情,不会出现张三订报纸却收到牛奶,或者李四订牛奶却收到报纸的乌龙。因此这种单一事件的观察者模式也称为订阅发布模式。
2.3 监听器模式
此外,Java 还提供了基于多事件的观察者模式,我们称之为监听器模式。其中 EventListener
是一个标记接口,所有的监听器类都必须实现该接口。EventObject
类表示一个事件对象,source
字段表示事件源,也就是产生事件的载体。具体的事件类应继承 EventObject
,可以携带不同类型的参数。
public class EventObject implements java.io.Serializable {
private static final long serialVersionUID = 5516075349620653480L;
protected transient Object source;
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}
public Object getSource() {
return source;
}
}
监听器模式有两个特点,一是对事件的类型进行区分,主题对象可以发送多种类型的事件,监听器只处理感兴趣的事件即可。二是没有定义主题(被观察者)相关的接口,需要用户自行实现主题对象。将两种模式进行对比,订阅发布模式是所有人一起吃大锅饭,而监听器模式则是每个人都可以点小炒。在实际应用中,我们要根据具体的情况来选择合适的模式。
3. Spring 事件机制
3.1 概述
Spring 的事件机制是基于监听器模式的,从而拥有了处理多事件的能力。事件相关的类图可以分为三部分,黄色区域是监听器模式的核心实现,红色区域是监听器的具体构成,紫色区域是ApplicationContext
与事件的整合。我们将分别对这三部分进行介绍,一步一步构建 Spring 事件机制的轮廓。
注:紫色区域 Spring 的整合放在下一节,本节的目标是实现基本的事件流程。
3.2 核心部分
ApplicationEvent
表示事件对象,所有的 Spring 事件类都要继承该类。Spring 内置了很多事件类,比如 ApplicationContextEvent
是用于处理 Spring 上下文相关的事件,还可以细分为 ContextStartedEvent
、ContextRefreshedEvent
、ContextClosedEvent
、ContextStoppedEvent
等,每个事件类都代表着不同的运行时刻。
public class ApplicationEvent extends EventObject {
public ApplicationEvent(Object source) {
super(source);
this.timestamp = System.currentTimeMillis();
}
}
ApplicationListener
接口表示监听器对象,也就是观察者。onApplicationEvent
方法定义了处理事件的逻辑,参数 event
是一个泛型类型,且必须是 ApplicationEvent
的子类。
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
//处理事件
void onApplicationEvent(E event);
}
在监听器模式中,Java 没有提供主题角色的接口,因而 Spring 使用 ApplicationEventMulticaster
接口作为主题角色。接口名可以直译为「应用事件多播器」,所谓多播是指发布一个事件可能被多个监听器捕获。ApplicationEventMulticaster
接口定义了两种方法,其中 multicastEvent
方法用于通知监听器处理事件,addApplicationListener
方法用于添加监听器(其余管理监听器的方法略)。
public interface ApplicationEventMulticaster {
//添加监听器
void addApplicationListener(ApplicationListener<?> listener);
//发布多播事件
void multicastEvent(ApplicationEvent event);
}
综上所述,观察者模式的三要素已齐备。ApplicationEventMulticaster
是事件源(被观察者),ApplicationListener
是观察者,ApplicationEvent
是事件对象,由事件源和数据组成。
3.3 监听器
对于用户(开发者)来说,直接继承 ApplicationListener
接口即可,GenericApplicationListener
接口及其子类实际上是供 Spring 内部使用的。GenericApplicationListener
接口的作用是判断监听器是否可以处理指定的事件,也就是说当前监听器对哪类事件感兴趣。
public interface GenericApplicationListener extends ApplicationListener<ApplicationEvent> {
//是否支持指定的事件类型
boolean supportsEventType(ResolvableType eventType);
}
Spring 对于同一个功能通常会提供两种方式,即编程式和声明式,比较典型的是通过 FactoryBean
接口和 @Bean
注解实现工厂方法。事件监听器也是一样,既可以实现 ApplicationListener
接口,也可以使用声明了 @EventListener
注解的方法。对于这两种不同的实现方式,Spring 使用两个适配器子类来分别处理。
ApplicationListenerMethodAdapter
:对事件方法进行适配,即处理声明@EventListener
注解的方法GenericApplicationListenerAdapter
:对监听器类进行适配,即处理ApplicationListener
接口的实现类
4. 事件多播器
4.1 主要属性
SimpleApplicationEventMulticaster
是 ApplicationEventMulticaster
接口的简单实现,allListeners
字段存储了所有的监听器,添加监听器的工作是由 Spring 容器完成的,我们将在下节 Spring 容器整合事件功能中讲解。retrieverCache
字段是一个按事件类型进行分类的缓存,由于同一个事件可能存在多个感兴趣的监听器,缓存起来以便重复使用。
public class SimpleApplicationEventMulticaster implements ApplicationEventMulticaster {
//所有的监听器集合
private final Set<ApplicationListener<?>> allListeners = new LinkedHashSet<>();
//按事件类型分类的监听器列表缓存
private final Map<ListenerCacheKey, Set<ApplicationListener<?>>> retrieverCache = new ConcurrentHashMap<>(64);
}
4.2 发布事件
SimpleApplicationEventMulticaster
实现了 multicastEvent
方法。首先获取可以处理指定事件类型的监听器列表,然后遍历整个列表,依次执行每个监听器的回调。监听器的回调是由具体的监听器类来实现的,我们关心的是如何获取与当前事件的类型相匹配的监听器列表,也就是说关键在于 getApplicationListeners
方法的实现。
@Override
public void multicastEvent(final ApplicationEvent event) {
ResolvableType type = ResolvableType.forInstance(event);
for (final ApplicationListener listener : getApplicationListeners(event, type)) {
listener.onApplicationEvent(event);
}
}
第一步,尝试从缓存中查找。发布事件是一个频繁执行的操作,而每种事件类型对应的监听器列表在应用初始化之后是固定的,因此 getApplicationListeners
方法的主要工作是与缓存打交道。先尝试从缓存中查找对应的监听器列表,如果没有找到,则需要进一步调用 retrieveApplicationListeners
方法。该方法只执行一次,以后再调用 getApplicationListeners
方法直接从缓存中获取,以此提高效率。
protected Collection<ApplicationListener<?>> getApplicationListeners(ApplicationEvent event, ResolvableType eventType) {
Object source = event.getSource();
Class<?> sourceType = (source != null ? source.getClass() : null);
ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);
//尝试从缓存中获取指定事件的监听器列表
Set<ApplicationListener<?>> listeners = this.retrieverCache.get(cacheKey);
if (listeners != null) {
return listeners;
}
synchronized (this.retrievalMutex) {
listeners = retrieveApplicationListeners(eventType, sourceType);
this.retrieverCache.put(cacheKey, listeners);
return listeners;
}
}
第二步,遍历所有的监听器。allListeners
字段存储了所有已注册的监听器,依次检查每个监听器,将符合条件的项加入集合中。supportsEvent
方法封装了判断逻辑的细节,分别对两种监听器的实现方式进行处理。
- 监听器方法:实际类型为
ApplicationListenerMethodAdapter
对象,直接强转即可 - 监听器类:实现了
ApplicationListener
接口,需要包装成GenericApplicationListenerAdapter
对象
private Set<ApplicationListener<?>> retrieveApplicationListeners(ResolvableType eventType, Class<?> sourceType) {
Set<ApplicationListener<?>> listeners = new LinkedHashSet<>();
for (ApplicationListener<?> listener : allListeners) {
if(supportsEvent(listener, eventType, sourceType)){
listeners.add(listener);
}
}
return listeners;
}
protected boolean supportsEvent(ApplicationListener<?> listener, ResolvableType eventType, Class<?> sourceType) {
//1) ApplicationListenerMethodAdapter 是 GenericApplicationListener 接口的直接子类,表示监听方法
//2) 否则 listener 是一个监听器类,需要使用 GenericApplicationListenerAdapter 来包装
GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
return smartListener.supportsEventType(eventType);
}
第三步,对两种类型的监听器对象进行检查。先来看监听器类,在 GenericApplicationListenerAdapter
类中,declaredEventType
字段表示监听器类的泛型类型,方法入参 eventType
表示事件对象的类型。如果事件对象的类型是监听器类的泛型的子类型,则表示该监听器可以处理该事件。比如 FooListener<FooEvent>
监听器类,可以处理 FooEvent
类型的事件。
public class GenericApplicationListenerAdapter implements GenericApplicationListener {
private final ResolvableType declaredEventType;
@Override
public boolean supportsEventType(ResolvableType eventType) {
return (this.declaredEventType == null || this.declaredEventType.isAssignableFrom(eventType));
}
}
对于监听器方法来说,declaredEventTypes
字段存储了支持的事件类型列表。该字段是在 ApplicationListenerMethodAdapter
的构造方法中初始化的,实际上解析了 @EventListener
注解的 value
属性。比如 @EventListener(AEvent.class, BEvent.class)
声明在某个方法上,那么 declaredEventTypes
字段包括 AEvent
和 BEvent
。可以看到,监听器类只支持单一的事件类型,而监听器方法可以同时支持多种事件。
注:本节我们不涉及事件方法的具体实现,仅介绍与发布事件有关的逻辑。
//本节不实现,仅介绍判断逻辑
public class ApplicationListenerMethodAdapter implements GenericApplicationListener {
private final List<ResolvableType> declaredEventTypes;
@Override
public boolean supportsEventType(ResolvableType eventType) {
for (ResolvableType declaredEventType : this.declaredEventTypes) {
if (declaredEventType.isAssignableFrom(eventType)) {
return true;
}
}
return eventType.hasUnresolvableGenerics();
}
}
5. 测试
5.1 订阅发布模式
本测试的目的是验证 Java 的订阅发布模式,虽然不是我们关心的重点,但有必要对其有所了解。首先定义两组测试类,NewsObservable
是被观察者对象,负责向订阅报纸的用户派发报纸。NewsObserver
是观察者对象,当他收到报纸时,就开始读报。此外,MilkObservable
和 MilkObserver
的相关代码略。
//测试类
public class NewsObservable extends Observable {
public void delivery(){
setChanged();
notifyObservers("报纸");
}
}
public class NewsObserver implements Observer {
private String name;
public NewsObserver(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
System.out.println(name + " 读 " + arg);
}
}
在测试方法中,张三和李四都订了一份报纸,当执行 NewsObservable
的 delivery
方法时,张三和李四会收到报纸,然后开始读报。仔细观察NewsObserver
类的 update
方法的实现,并没有对参数 arg
进行额外的处理,因为所有的订阅者都知道,他们会收到一份报纸,而不是一瓶牛奶。同样地,在第二组订阅关系中,张三和王五会收到一瓶牛奶。
//测试方法
@Test
public void testObserver() {
//订报纸
NewsObservable no = new NewsObservable();
no.addObserver(new NewsObserver("张三"));
no.addObserver(new NewsObserver("李四"));
no.delivery();
//订牛奶
MilkObservable mo = new MilkObservable();
mo.addObserver(new MilkObserver("张三"));
mo.addObserver(new MilkObserver("王五"));
mo.delivery();
}
从测试结果可以看到,第一组订阅的用户收到了报纸,第二组订阅的用户收到了牛奶。
李四 读 报纸
张三 读 报纸
王五 喝 牛奶
张三 喝 牛奶
5.2 监听器模式
首先定义两组事件和监听器,分别表示订阅报纸和牛奶。NewsListener
感兴趣的事件类型是 NewsEvent
,MilkListener
感兴趣的事件类型是 MilkEvent
(代码略)。
//测试类
public class NewsListener implements ApplicationListener<NewsEvent> {
private String name;
public NewsListener(String name) {
this.name = name;
}
@Override
public void onApplicationEvent(NewsEvent event) {
System.out.println(name + "读 " + event.getSource());
}
}
public class NewsEvent extends ApplicationEvent {
public NewsEvent(Object source) {
super(source);
}
}
在测试方法中,我们给张三订阅了报纸,给李四订阅了报纸和牛奶。当发送 NewsEvent
和 MilkEvent
事件时,张三只能读报纸,而李四可以一边读报纸,一边喝牛奶。
//测试方法
@Test
public void testEventMulticaster(){
ApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.addApplicationListener(new NewsListener("张三"));
multicaster.addApplicationListener(new NewsListener("李四"));
multicaster.addApplicationListener(new MilkListener("李四"));
multicaster.multicastEvent(new NewsEvent("《人民日报》"));
multicaster.multicastEvent(new MilkEvent("光明牛奶"));
}
从测试结果可以看到,监听器模式支持发布多种事件,每个观察者都可以监听感兴趣的事件,拥有极大的灵活性。
张三读 《人民日报》
李四读 《人民日报》
李四喝 光明牛奶
6. 总结
本节初步讨论了 Spring 框架的事件机制,而事件机制是基于观察者模式实现的。观察者模式可以分为两类,一是订阅发布模式,二是监听器模式。两者的区别在于,订阅发布模式只能处理一种事件,监听器模式则可以同时处理多种事件。Spring 的事件机制是基于监听器模式实现的,使用的范围更广泛。监听器模式需要实现三个基本组件:
- 事件对象:
ApplicationEvent
扩展自EventObject
,代表事件在传播过程中携带的数据。 - 监听器:
ApplicationListener
接口代表观察者角色,作用是对发生的事件进行响应。 - 事件多播器:
ApplicationEventMulticaster
接口代表主题角色,持有一组监听器,负责发布事件。
Spring 提供了两种监听器实现,一种是编程式,通过实现 ApplicationListener
接口,称之为监听器类。一种是声明式,将 @EventListener
注解声明在方法上,称为监听器方法。事件多播器对这两种类型的监听器进行了适配,其中监听器类被包装成 GenericApplicationListenerAdapter
对象,监听器方法被包装成 ApplicationListenerMethodAdapter
对象。本节我们仅实现了监听器类的处理,有关监听器方法的实现将在下节介绍。
7. 项目信息
新增修改一览,新增(15),修改(0)。
context
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring.context
│ └─ event
│ ├─ ApplicationEvent.java (+)
│ ├─ ApplicationEventMulticaster.java (+)
│ ├─ ApplicationListener.java (+)
│ ├─ GenericApplicationListener.java (+)
│ ├─ GenericApplicationListenerAdapter.java (+)
│ └─ SimpleApplicationEventMulticaster.java (+)
└─ test
└─ java
└─ context
└─ event
├─ EventTest.java (+)
├─ MilkEvent.java (+)
├─ MilkListener.java (+)
├─ MilkObservable.java (+)
├─ MilkObserver.java (+)
├─ NewsEvent.java (+)
├─ NewsListener.java (+)
├─ NewsObservable.java (+)
└─ NewsObserver.java (+)
注:+号表示新增、*号表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。
转载自:https://juejin.cn/post/7418462670418559027