深入探索Spring的事件处理机制,从事件的层次传播、PayloadApplicationEvent的使用,到为何选择自定义事件。本文详细剖析了Spring 5.x的事件模型、事件发布源码、ApplicationEventMulticaster的作用以及事件广播的核心逻辑。通过详细的流程图与图示,读者可以更好地理解Spring事件传播、异步处理等关键概念,为成为Spring高手奠定坚实基础。
在Spring中,ApplicationContext可以形成一个层次结构,通常由主容器和多个子容器组成。一个常见的疑问是:当一个事件在其中一个容器中发布时,这个事件会如何在这个层次结构中传播?
为了探讨这个问题,我们创建了一个名为HierarchicalEventPropagationEvent的事件类和一个对应的监听器HierarchicalEventPropagationListener。
全部代码如下:
package com.example.demo.event;
import org.springframework.context.ApplicationEvent;
// 事件类
public class HierarchicalEventPropagationEvent extends ApplicationEvent {
private String message;
public HierarchicalEventPropagationEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
相应地,为 HierarchicalEventPropagationEvent 定义一个监听器HierarchicalEventPropagationListener :
package com.example.demo.listener;
import com.example.demo.event.HierarchicalEventPropagationEvent;
import org.springframework.context.ApplicationListener;
// 监听器类
public class HierarchicalEventPropagationListener implements ApplicationListener<HierarchicalEventPropagationEvent> {
private String listenerId;
public HierarchicalEventPropagationListener(String listenerId) {
this.listenerId = listenerId;
}
@Override
public void onApplicationEvent(HierarchicalEventPropagationEvent event) {
System.out.println(listenerId + " received event - " + event.getMessage());
}
}
为了测试继承机制,我们需要构建主容器和子容器,并为每个容器注册了一个监听器。初始化容器后,我们在两个容器中分别发布事件。
请注意,首先需要刷新主容器,然后刷新子容器。否则会出现异常:Exception in thread "main" java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: org.springframework.context.annotation.AnnotationConfigApplicationContext@somehashcode
主程序如下:
package com.example.demo;
import com.example.demo.event.HierarchicalEventPropagationEvent;
import com.example.demo.listener.HierarchicalEventPropagationListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
// 创建父容器,注册监听器
AnnotationConfigApplicationContext parentCtx = new AnnotationConfigApplicationContext();
parentCtx.addApplicationListener(new HierarchicalEventPropagationListener("Parent Listener"));
parentCtx.refresh();
// 创建子容器,注册监听器
AnnotationConfigApplicationContext childCtx = new AnnotationConfigApplicationContext();
childCtx.setParent(parentCtx);
childCtx.addApplicationListener(new HierarchicalEventPropagationListener("Child Listener"));
childCtx.refresh();
// 发布事件
HierarchicalEventPropagationEvent event1 = new HierarchicalEventPropagationEvent(parentCtx, "Event from parent");
parentCtx.publishEvent(event1);
HierarchicalEventPropagationEvent event2 = new HierarchicalEventPropagationEvent(childCtx, "Event from child");
childCtx.publishEvent(event2);
}
}
运行结果
主容器发布的事件只触发了一次监听,而子容器发布的事件触发了两次监听。父容器和子容器都监听到了来自子容器的事件,而只有父容器监听到了来自父容器的事件。
所以得出结论:在Spring的父子容器结构中,事件会从子容器向上传播至其父容器,但父容器中发布的事件不会向下传播至子容器。 这种设计可以帮助开发者在父容器中集中处理所有的事件,而不必担心事件在多个子容器之间的传播。
PayloadApplicationEvent是Spring提供的一种特殊事件,用于传递数据(称为"payload")。所以不需要自定义事件,PayloadApplicationEvent可以直接传递任何类型的数据,只需要指定它的类型即可。
全部代码如下:
首先,我们来看怎样定义一个监听器来接收这个事件:
通用监听器 - 会监听到所有种类的PayloadApplicationEvent:
package com.example.demo.listener;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
/**
* 通用监听器,能监听到所有类型的PayloadApplicationEvent
*/
public class CustomObjectApplicationListener implements ApplicationListener<PayloadApplicationEvent> {
@Override
public void onApplicationEvent(PayloadApplicationEvent event) {
System.out.println("收到PayloadApplicationEvent,数据是:" + event.getPayload());
}
}
特定数据类型的监听器 - 只会监听指定类型的数据。例如,如果我们只对字符串数据感兴趣,我们可以如此定义:
package com.example.demo.listener;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
/**
* 特定数据类型的监听器。这个监听器专门监听String类型的PayloadApplicationEvent
*/
public class CustomStringApplicationListener implements ApplicationListener<PayloadApplicationEvent<String>> {
@Override
public void onApplicationEvent(PayloadApplicationEvent<String> event) {
System.out.println("收到了字符串数据:" + event.getPayload());
}
}
要看这两种监听器如何工作,我们来写一个测试。
package com.example.demo;
import com.example.demo.listener.CustomObjectApplicationListener;
import com.example.demo.listener.CustomStringApplicationListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Date;
public class DemoApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
// 注册监听器
ctx.addApplicationListener(new CustomObjectApplicationListener());
ctx.addApplicationListener(new CustomStringApplicationListener());
ctx.refresh();
// 发送事件
ctx.publishEvent("Hello, World!"); // 发送一个字符串
ctx.publishEvent(2023); // 发送一个整数
ctx.publishEvent(new Date()); // 发送一个日期对象
}
}
在这个测试中,我们发送了三种类型的数据:一个字符串、一个整数和一个日期。
执行结果如下:
从输出可以看出:
第一种监听器(通用的)接收到了所有三个事件,因为它不关心数据的具体类型。
第二种监听器(字符串专用的)只接收到了字符串类型的事件。
虽然PayloadApplicationEvent提供了简化事件监听的能力,但其可能不足以满足特定的业务需求,尤其是当需要更多上下文和数据时。下面是一个使用自定义事件ArticlePublishedEvent的例子。
全部代码如下:
自定义事件: ArticlePublishedEvent
这个事件代表了“新文章发布”,附带有文章的标题、作者和发布日期等信息。
package com.example.demo.event;
import org.springframework.context.ApplicationEvent;
public class ArticlePublishedEvent extends ApplicationEvent {
private String title;
private String author;
private String publishedDate;
public ArticlePublishedEvent(Object source, String title, String author, String publishedDate) {
super(source);
this.title = title;
this.author = author;
this.publishedDate = publishedDate;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public String getPublishedDate() {
return publishedDate;
}
}
自定义监听器: ArticlePublishedListener
这个监听器专门响应ArticlePublishedEvent,执行特定的业务逻辑,例如通知订阅者、更新搜索索引等。
import org.springframework.context.ApplicationListener;
public class ArticlePublishedListener implements ApplicationListener<ArticlePublishedEvent> {
@Override
public void onApplicationEvent(ArticlePublishedEvent event) {
System.out.println("A new article has been published!");
System.out.println("Title: " + event.getTitle());
System.out.println("Author: " + event.getAuthor());
System.out.println("Published Date: " + event.getPublishedDate());
// Notify subscribers about the new article
notifySubscribers(event);
// Update search engine index with new article details
updateSearchIndex(event);
// Update statistical data about articles
updateStatistics(event);
}
private void notifySubscribers(ArticlePublishedEvent event) {
// Logic to notify subscribers (dummy logic for demonstration)
System.out.println("Notifying subscribers about the new article titled: " + event.getTitle());
}
private void updateSearchIndex(ArticlePublishedEvent event) {
// Logic to update search engine index (dummy logic for demonstration)
System.out.println("Updating search index with the new article titled: " + event.getTitle());
}
private void updateStatistics(ArticlePublishedEvent event) {
// Logic to update statistical data (dummy logic for demonstration)
System.out.println("Updating statistics with the new article titled: " + event.getTitle());
}
}
在接收到新文章发布的事件后,监听器ArticlePublishedListener需要执行以下业务逻辑:
这样,每次新文章发布的事件被触发时,订阅者都会被通知,搜索引擎的索引将会得到更新,同时相关的统计数据也会得到更新。
主程序:
模拟文章发布的场景
package com.example.demo;
import com.example.demo.event.ArticlePublishedEvent;
import com.example.demo.listener.ArticlePublishedListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
// Initialize Spring ApplicationContext
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
// Register listener
ctx.addApplicationListener(new ArticlePublishedListener());
ctx.refresh();
// Simulate publishing an article
ctx.publishEvent(new ArticlePublishedEvent(ctx, "Spring Events", "John Doe", "2023-09-15"));
}
}
运行结果如下:
我们可以看到ArticlePublishedEvent比PayloadApplicationEvent具有更多的业务含义和上下文。这样的设计使我们能够更具体地响应和处理特定的业务事件。
实际上,在企业级应用中,文章发布可能会触发多种不同的后续动作,使用Spring的事件监听器模式可以带来如下优势:
Spring为开发者提供了强大的事件监听机制,无论是使用自定义事件还是利用PayloadApplicationEvent进行快速开发,都使我们能够构建一个高度解耦、可扩展且易于维护的系统。
1.核心概念:
2.事件发布:
用户可以通过ApplicationEventPublisher接口或ApplicationContext来发布事件。通常情况下,当我们在Spring bean中需要发布事件时,可以让这个bean实现ApplicationEventPublisherAware接口,这样Spring容器会注入一个事件发布器。
3.异步事件:
从Spring 4.2开始,我们可以轻松地使事件监听器异步化。在Spring 5中,这一功能仍然得到支持。只需要在监听器方法上添加@Async注解并确保启用了异步支持。这使得事件处理可以在单独的线程中执行,不阻塞发布者。
4.泛型事件:
Spring 4.2引入了对泛型事件的支持,这在Spring 5中得到了维护。这意味着监听器现在可以根据事件的泛型类型进行过滤。例如,一个ApplicationListener<ApplicationEvent<String>>将只接收到携带String负载的事件。
5.事件的排序:
监听器可以实现Ordered接口或使用@Order注解来指定事件的执行顺序。
6.新的事件类型:
Spring 5引入了新的事件类型,如ServletRequestHandledEvent,为web请求处理提供更多的钩子。而像ContextRefreshedEvent这样的事件,虽然不是Spring 5新引入的,但它为特定的生命周期回调提供了钩子。
7.Reactive事件模型:
与Spring 5引入的WebFlux一起,还引入了对反应式编程模型的事件监听和发布的支持。
总结:
在Spring 5.x中,事件模型得到了进一步的增强和优化,增加了对异步、泛型和反应式编程的支持,提供了更强大、灵活和高效的机制来处理应用程序事件。对于开发者来说,这为在解耦的同时实现复杂的业务逻辑提供了便利。
上图,这里是Spring 5.3.7的源码,下面讲单独抽出来分析
public void publishEvent(ApplicationEvent event) {
this.publishEvent(event, (ResolvableType)null);
}
分析:
该方法接受一个ApplicationEvent对象并调用其重载版本publishEvent(Object event, @Nullable ResolvableType eventType),为其传递null作为事件类型。这是为了简化用户使用,用户可以直接传递一个ApplicationEvent对象而无需考虑其具体的类型。
public void publishEvent(Object event) {
this.publishEvent(event, (ResolvableType)null);
}
分析:
与上一个方法类似,但它接受任何Object作为事件,并将其与null的eventType一起传递给核心方法。这增加了灵活性,用户可以发送任何对象作为事件,而不仅仅是ApplicationEvent对象。
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
// 检查事件对象是否为空,确保发布的事件是有意义的
Assert.notNull(event, "Event must not be null");
// 判断传入的事件是否已经是ApplicationEvent类型,如果是,则无需再进行包装
Object applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent)event;
} else {
// 如果传入的事件不是ApplicationEvent类型,则将其包装为PayloadApplicationEvent
applicationEvent = new PayloadApplicationEvent(this, event);
// 如果未指定事件类型,那么从包装后的事件中获取其真实类型
if (eventType == null) {
eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
}
}
// 判断当前是否存在earlyApplicationEvents列表
if (this.earlyApplicationEvents != null) {
// 如果存在,说明ApplicationContext还未完全初始化,将事件添加到此列表中,稍后再进行处理
this.earlyApplicationEvents.add(applicationEvent);
} else {
// 如果ApplicationContext已经初始化,那么直接通过事件多播器广播事件
this.getApplicationEventMulticaster().multicastEvent((ApplicationEvent)applicationEvent, eventType);
}
// 如果存在父ApplicationContext,则也将事件发布到父容器中
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
// 如果父容器是AbstractApplicationContext类型,则带上事件类型进行发布
((AbstractApplicationContext)this.parent).publishEvent(event, eventType);
} else {
// 否则,只传递事件对象进行发布
this.parent.publishEvent(event);
}
}
}
这个方法究竟做了什么?
通过这种方式,Spring的事件发布机制确保了事件在不同的上下文和生命周期阶段都能被正确处理和广播。
上面说到事件广播是ApplicationEventMulticaster完成的,这个是什么?下面来看看
当我们在Spring中讨论事件,我们实际上是在讨论两件事:事件(即发生的事情)和监听器(即对这些事件感兴趣并作出反应的实体)。
ApplicationEventMulticaster的主要职责是管理事件监听器并广播事件给这些监听器。我们主要关注SimpleApplicationEventMulticaster,因为这是默认的实现,但请注意,Spring允许替换为自定义的ApplicationEventMulticaster。
以下是SimpleApplicationEventMulticaster中的相关代码与分析,有兴趣的小伙伴可以自行查看:
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
// 可选的任务执行器,用于异步调用事件监听器。
@Nullable
private Executor taskExecutor;
// 可选的错误处理器,用于处理在广播事件过程中出现的错误。
@Nullable
private ErrorHandler errorHandler;
// 用于记录日志的logger,它是延迟初始化的。
@Nullable
private volatile Log lazyLogger;
// 默认构造函数。
public SimpleApplicationEventMulticaster() {
}
// 带有BeanFactory参数的构造函数,通常用于更复杂的应用上下文配置中。
public SimpleApplicationEventMulticaster(BeanFactory beanFactory) {
this.setBeanFactory(beanFactory);
}
// 设置任务执行器。可以是任何Java Executor,比如Spring的SimpleAsyncTaskExecutor或Java的FixedThreadPool。
public void setTaskExecutor(@Nullable Executor taskExecutor) {
this.taskExecutor = taskExecutor;
}
// 获取当前设置的任务执行器。
@Nullable
protected Executor getTaskExecutor() {
return this.taskExecutor;
}
// 设置错误处理器。
public void setErrorHandler(@Nullable ErrorHandler errorHandler) {
this.errorHandler = errorHandler;
}
// 获取当前设置的错误处理器。
@Nullable
protected ErrorHandler getErrorHandler() {
return this.errorHandler;
}
// 这是广播事件的主要方法。它首先解析事件的类型,然后调用具有额外参数的重载方法。
public void multicastEvent(ApplicationEvent event) {
this.multicastEvent(event, this.resolveDefaultEventType(event));
}
// 这个方法是真正执行广播操作的方法。
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
// 确定事件类型。
ResolvableType type = (eventType != null) ? eventType : this.resolveDefaultEventType(event);
// 获取任务执行器。
Executor executor = this.getTaskExecutor();
// 获取匹配此事件类型的所有监听器。
Iterator<ApplicationListener<?>> listeners = this.getApplicationListeners(event, type).iterator();
// 遍历每个监听器并调用它。
while(listeners.hasNext()) {
ApplicationListener<?> listener = listeners.next();
// 如果有设置任务执行器,则异步调用监听器。
if (executor != null) {
executor.execute(() -> this.invokeListener(listener, event));
} else {
// 如果没有设置任务执行器,则同步调用监听器。
this.invokeListener(listener, event);
}
}
}
// 为给定的事件解析默认的事件类型。
private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
return ResolvableType.forInstance(event);
}
// 调用指定的监听器来处理给定的事件,并根据需要处理错误。
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
// 获取当前的错误处理器。
ErrorHandler errorHandler = this.getErrorHandler();
if (errorHandler != null) {
try {
// 尝试调用监听器。
this.doInvokeListener(listener, event);
} catch (Throwable ex) {
// 如果出现错误,使用错误处理器处理。
errorHandler.handleError(ex);
}
} else {
// 如果没有设置错误处理器,则直接调用监听器。
this.doInvokeListener(listener, event);
}
}
// 直接调用监听器,捕获任何类型不匹配的异常。
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
} catch (ClassCastException ex) {
// 捕获类型转换异常,并根据需要进行处理。
// 这可以确保如果监听器不能处理特定类型的事件,不会导致整个广播操作失败。
String msg = ex.getMessage();
if (msg != null && !this.matchesClassCastMessage(msg, event.getClass()) && (!(event instanceof PayloadApplicationEvent) || !this.matchesClassCastMessage(msg, ((PayloadApplicationEvent)event).getPayload().getClass()))) {
throw ex;
}
// 在预期情况下捕获并记录异常,而不是抛出它。
Log loggerToUse = this.lazyLogger;
if (loggerToUse == null) {
loggerToUse = LogFactory.getLog(this.getClass());
this.lazyLogger = loggerToUse;
}
if (loggerToUse.isTraceEnabled()) {
loggerToUse.trace("Non-matching event type for listener: " + listener, ex);
}
}
}
// 根据给定的类型错误消息和事件类来检查ClassCastException是否是预期的。
private boolean matchesClassCastMessage(String classCastMessage, Class<?> eventClass) {
if (classCastMessage.startsWith(eventClass.getName())) {
return true;
} else if (classCastMessage.startsWith(eventClass.toString())) {
return true;
} else {
int moduleSeparatorIndex = classCastMessage.indexOf(47);
return moduleSeparatorIndex != -1 && classCastMessage.startsWith(eventClass.getName(), moduleSeparatorIndex + 1);
}
}
}
关于核心方法multicastEvent需要作出特别说明:
// 这个方法是真正执行广播操作的方法。
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
// 确定事件类型。
ResolvableType type = (eventType != null) ? eventType : this.resolveDefaultEventType(event);
......
// 获取匹配此事件类型的所有监听器。
Iterator<ApplicationListener<?>> listeners = this.getApplicationListeners(event, type).iterator();
......
}
方法第一行得到一个ResolvableType类型的对象,为什么 Spring 选择使用 ResolvableType 而不是直接使用 Java 类型?最主要的原因是 Java 的泛型擦除。 在 Java 中,泛型只存在于编译时,一旦代码被编译,泛型信息就会被擦除,运行时就不能直接获取到泛型的实际类型。
为了解决这个问题,Spring 引入了 ResolvableType,一个能够解析泛型类型信息的工具类。
举个例子:
假设有如下的类定义:
public class Sample {
private List<String> names;
}
我们可以这样获取 names 字段的泛型类型:
ResolvableType t = ResolvableType.forField(Sample.class.getDeclaredField("names"));
Class<?> genericType = t.getGeneric(0).resolve(); // 得到 String.class
在 Spring 事件中的使用
ResolvableType 在 Spring 事件中的应用主要是确定事件的类型和监听器监听的事件类型。当我们发布一个事件:
ApplicationEvent event = new MyCustomEvent(this, "data");
applicationContext.publishEvent(event);
Spring 内部会使用 ResolvableType.forInstance(event) 来获取这个事件的类型。然后,它会找到所有注册的监听器,查看它们监听的事件类型是否与此事件匹配(通过比较 ResolvableType)。匹配的监听器会被调用。
对于一个监听器:
public class MyListener implements ApplicationListener<MyCustomEvent> {
@Override
public void onApplicationEvent(MyCustomEvent event) {
// handle the event
}
}
Spring 内部会使用 ResolvableType 来解析这个监听器监听的事件类型(在这个例子中是 MyCustomEvent)。
总之,ResolvableType 在 Spring 中的主要用途是提供了一种方式来解析和操作运行时的泛型类型信息,特别是在事件发布和监听中。
如果看不清,建议在新标签页中打开图片后放大看
再来看看监听器内部逻辑,我们来分析在multicastEvent方法中调用的getApplicationListeners(event, type)来分析下
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。