ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果一个实现 ApplicationListener 接口的 Bean 被部署到上下文中,每当 ApplicationEvent 被发布到 ApplicationContext 时,该 Bean 就会被通知。基本上,这是标准的 观察者设计模式。
:::tips 从 Spring 4.2 开始,事件基础架构得到了极大的改进,它提供了基于注解的模型以及发布任何任意事件的能力(也就是不一定从 ApplicationEvent 延伸出来的对象)。当这样的对象被发布时,我们会为你把它包装成一个事件。 :::
下表描述了 Spring 提供的标准事件:
Event | 描述 |
---|---|
ContextRefreshedEvent | 当 ApplicationContext 被初始化或刷新时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。这里,「初始化」意味着所有的 Bean 被加载,后处理器 Bean 被检测和激活,单例 Bean 被预先实例化,并且 ApplicationContext 对象可以使用。只要上下文没有被关闭,就可以多次触发刷新,前提是所选的 ApplicationContext 实际上支持这种 「热」刷新。例如, XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。 |
ContextStartedEvent | 当 ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 start() 方法启动时发布。在这里,「开始」意味着所有的生命周期 Bean 收到一个显式的启动信号。通常,这个信号用于在显式停止后重新启动 Bean,但它也可以用于启动未被配置为自动启动的组件(例如,初始化时尚未启动的组件)。 |
ContextStoppedEvent | 当 ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 stop() 方法而停止时发布。在这里,「停止」意味着所有的生命周期 Bean 收到一个明确的停止信号。停止的上下文可以通过 start() 调用重新启动。 |
ContextClosedEvent | 当 ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM 关闭钩子被关闭时发布。这里,「关闭」意味着所有的单例 Bean 将被销毁。一旦上下文被关闭,它的生命就结束了,不能被刷新或重新启动。 |
RequestHandledEvent | 一个针对 Web 的事件,告诉所有 Bean 一个 HTTP 请求已经被服务了。该事件在请求完成后被发布。这个事件只适用于使用 Spring 的DispatcherServlet 的 Web 应用程序。 |
ServletRequestHandledEvent | RequestHandledEvent 的一个子类,增加了 Servlet 特定的上下文信息。 |
关于 RequestHandledEvent 提供的信息,如下,自带一个 controller 处理耗时的信息
ServletRequestHandledEvent: url=[/xx/abc/1065]; client=[192.168.1.123]; method=[POST]; servlet=[dispatcherServlet]; session=[null]; user=[null]; time=[112ms]; status=[OK]
你也可以创建和发布你自己的自定义事件。下面的例子展示了一个简单的类,它继承了 Spring 的ApplicationEvent 类:
public class BlockedListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlockedListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
要发布一个自定义的 ApplicationEvent,请在 ApplicationEventPublisher上调用 publishEvent()
方法。通常情况下,这是通过创建一个实现 ApplicationEventPublisherAware 的类并将其注册为 Spring Bean 来实现的。下面的例子展示了这样一个类:
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blockedList;
private ApplicationEventPublisher publisher;
public void setBlockedList(List<String> blockedList) {
this.blockedList = blockedList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blockedList.contains(address)) {
// 发布一个事件
publisher.publishEvent(new BlockedListEvent(this, address, content));
return;
}
// send email...
}
}
在配置时,Spring 容器检测到 EmailService 实现了 ApplicationEventPublisherAware 并自动调用setApplicationEventPublisher()
。实际上,传入的参数是 Spring 容器本身。你正在通过应用环境的 ApplicationEventPublisher 接口与应用环境进行交互。
为了接收自定义的 ApplicationEvent,你可以创建一个实现 ApplicationListener 的类并将其注册为 Spring Bean。下面的例子展示了这样一个类:
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// 通过 notificationAddress 通知相关方...
// 这里处理监听到的 事件
}
}
请注意,ApplicationListener 是用你的自定义事件的类型(前面的例子中是 BlockedListEvent)进行通用参数化。这意味着 onApplicationEvent()
方法可以保持类型安全,避免了任何强转的需要。你可以根据你的需要注册任意多的事件监听器,但是请注意,在默认情况下,事件监听器是同步接收事件的。这意味着 publishEvent()
方法会阻塞,直到所有的监听器都完成对事件的处理。这种同步和单线程的方法的一个优点是,当一个监听器收到一个事件时,如果有一个事务上下文的话,它就在发布者的事务上下文中操作。如果需要另一种事件发布策略,请参阅 Spring 的 ApplicationEventMulticaster 接口和 SimpleApplicationEventMulticaster 实现的 javadoc,了解配置选项。
下面的例子显示了用于注册和配置上述每个类的 bean 定义:
<bean id="emailService" class="example.EmailService">
<property name="blockedList">
<list>
<value>known.spammer@example.org</value>
<value>known.hacker@example.org</value>
<value>john.doe@example.org</value>
</list>
</property>
</bean>
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
<property name="notificationAddress" value="blockedlist@example.org"/>
</bean>
把它放在一起,当调用 emailService bean 的 sendEmail()
方法时,如果有任何应该被阻止的电子邮件,就会发布一个 BlockedListEvent 类型的自定义事件。blockedListNotifier Bean 被注册为ApplicationListener,并接收 BlockedListEvent,这时它可以通知相关方。
:::info Spring 的事件机制是为同一应用环境下的 Spring Bean 之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的 spring-integration 项目为构建轻量级、面向模式、事件驱动的架构提供了完整的支持,这些架构建立在著名的 Spring 编程模型之上。 :::
基于注解的事件监听器
你可以通过使用 @EventListener
注解在托管 Bean(托管给 IOC 容器的 bean 对象) 的任何方法上注册一个事件监听器。BlockedListNotifier 可以被重写如下:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// 有这个事件时,该方法会被调用
}
}
方法签名再次声明了它所 监听的事件类型,但是,这一次,有了一个灵活的名字,并且没有实现特定的监听器接口。只要实际的事件类型在其实现层次中解决了你的泛型参数,事件类型也可以通过泛型来缩。:
如果你的方法应该监听几个事件,或者你想在没有参数的情况下定义它,事件类型也可以在注释本身中指定。下面的例子展示了如何做到这一点:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
也可以通过使用定义 SpEL 表达式 的注解的条件属性来添加额外的运行时过滤,该表达式应该匹配以实际调用特定事件的方法。
下面的例子显示了我们的通知器如何被改写成只有在事件的内容属性等于 my-event 时才会被调用:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
每个 SpEL 表达式都针对一个专门的上下文进行评估。下表列出了提供给上下文的项目,以便你可以使用它们进行条件性事件处理:
Name | Location | 描述 | 例子 |
---|---|---|---|
Event | root object(根对象) | 实际的 ApplicationEvent. | #root.event 或则 event |
参数数组 | root object | 用于调用方法的参数(作为一个对象数组)。 | #root.args 或则 args ; args[0] 访问第一个参数等 |
参数名称 | evaluation context(评估上下文) | 任何一个方法参数的名称。如果由于某种原因,这些名字无法使用(例如,因为在编译的字节码中没有调试信息),单个参数也可以使用 #a<#arg> 语法,其中 <#arg> 代表参数索引(从 0 开始)。 |
#blEvent 或则 #a0 (您也可以使用 #p0 或 #p<#arg> 参数表示法作为别名) |
请注意,#root.event
让你可以访问底层事件,即使你的方法签名实际上指的是一个被发布的任意对象。
如果你需要发布一个事件作为处理另一个事件的结果,你可以改变方法签名以返回应该被发布的事件,如下例所示:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
:::info 异步监听器 不支持此功能。这里指的是方法的返回类型,如果返回一个事件对象,则框架会将此事件对象发布出去。 :::
handleBlockedListEvent()
方法为其处理的每个 BlockedListEvent 发布一个新的 ListUpdateEvent。如果你需要发布几个事件,你可以返回一个集合或者一个事件数组来代替。
异步监听器
如果你想让一个特定的监听器异步处理事件,你可以重新使用常规的 @Async
支持。下面的例子展示了如何做到这一点:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
使用异步事件时要注意以下限制:
- 如果一个异步事件监听器抛出一个异常,它不会被传播给调用者。参见AsyncUncaughtExceptionHandler了解更多细节。
- 异步事件监听器方法 不能通过返回一个值来发布后续的事件。如果你需要发布另一个事件作为处理的结果,注入一个 ApplicationEventPublisher 来手动发布事件。
监听顺序
如果你需要一个监听器在另一个之前被调用,你可以在方法声明中添加 @Order
注解,如下例所示:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
通用事件
你也可以使用泛型来进一步定义你的事件的结构。考虑使用 EntityCreatedEvent<T>
,其中 T 是被创建的实际实体的类型。例如,你可以创建下面的监听器定义,只接收一个人的 EntityCreatedEvent:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
由于类型擦除,只有当被触发的事件解决了事件监听器过滤的通用参数时,这才起作用(也就是说,像class PersonCreatedEvent extends EntityCreatedEvent<Person> { ... }
)。
在某些情况下,如果所有的事件都遵循相同的结构(如前面例子中的事件应该是这样的),这可能会变得相当繁琐。在这种情况下,你可以实现 ResolvableTypeProvider 来引导框架超越运行时环境提供的内容。下面的事件展示了如何做到这一点:
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
super(entity);
}
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
}
}
一个例子
看完脑瓜子懵逼的吧?通用事件简单说:你后续的事件对象不用继承 ApplicationEvent 这个类了,看下面的例子
package cn.mrcode.study.springdocsread.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.core.ResolvableType;
import org.springframework.core.ResolvableTypeProvider;
/**
* 创建一个通用事件对象
* @author mrcode
* @date 2022/2/23 17:27
*/
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
public EntityCreatedEvent(T entity) {
// 记住这个:如果使用自定义事件的话,这里传入的是 发布事件的 对象
// 而这里传入的是,实际的事件对象,这个对象不需要继承 ApplicationEvent,所以说可以只是一个普通的 POJO 对象
super(entity);
}
@Override
public ResolvableType getResolvableType() {
// 这里做类型转换:
// getClass() 获取的是实际事件对象的 class
// getSource() 就是上面通过构造函数传入的实际事件对象
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
}
}
编写一个普通的事件对象,没有继承 ApplicationEvent
package cn.mrcode.study.springdocsread.event;
/**
* @author mrcode
* @date 2022/2/23 17:30
*/
public class MyCreateEvent{
private Integer age;
private String name;
public MyCreateEvent(Integer age, String name) {
this.age = age;
this.name = name;
}
}
编写事件发布程序,和监听事件
package cn.mrcode.study.springdocsread.event;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 事件发布器
* @author mrcode
* @date 2022/2/23 17:36
*/
@Component
public class EventPub implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* 事件发布方法
*/
public void pubMyMyCreateEvent() {
// 这里创建一个普通的对象
final MyCreateEvent myCreateEvent = new MyCreateEvent(1, "张三");
// 调用事件发布出去:注意这里使用了 刚刚定义的通用事件对象
applicationEventPublisher.publishEvent(new EntityCreatedEvent<>(myCreateEvent));
}
/**
* 为了方便测试,我将监听的程序也写在了这里
* @param event 这里写的事件发布器里面的声明
*/
@EventListener
public void myCreateEventProcess(EntityCreatedEvent<MyCreateEvent> event) {
// 所以这里要获取到实际的事件对象,需要强转
final MyCreateEvent source = (MyCreateEvent) event.getSource();
System.out.println(event);
}
}
当你程序启动后,调用 pubMyMyCreateEvent()
方法时,就发布了一个通用事件。在事件监听里面可以通过 event.getSource()
获取到没有继承 ApplicationEvent 的事件对象。
总结
通过上面的例子可以看到:
- 实际上事件发布和消费还是原来那种
- 只不过:将原来的 source 对象用来存储了自定义的普通事件对象
- 原来的 source 存储的是事件发布者的引用,比如:上面 EventPub 就是事件发布者,存储的就是 EventPub 实例,而通用事件中就没有事件发布者信息了,存储的就是普通的事件对象
- 框架通过 ResolvableType 中提供的 class 和 实际事件对象,来找到哪一个方法监听了这个事件