Spring的事件机制是我在项目里面用得很多、但身边同事经常讲不清原理的一个功能。很多面试候选人能把ApplicationListener背出来,可真要他说一次事件从发布到监听器执行的完整链路,就卡壳了。这篇文章我打算把ApplicationListener的使用姿势和源码执行路径一起扒一遍,内容包括怎么自定义事件、怎么用接口和注解两种方式注册监听器、@TransactionalEventListener怎么用,以及publishEvent前后到底发生了什么。适合想系统搞懂Spring事件机制的开发者,也适合准备Spring相关面试的人。
1. 先理解Spring事件机制是干什么的
1.1 三要素:事件、监听器、发布器
Spring的事件模型本质上就是观察者模式,核心角色只有三个。
- 事件(ApplicationEvent):继承自
ApplicationEvent的对象,用来描述“发生了什么事”。比如OrderCreatedEvent表示订单创建成功。 - 监听器(ApplicationListener):对某个事件感兴趣、并做出反应的组件。
ApplicationListener<E extends ApplicationEvent>接口只有一个方法onApplicationEvent(E event)。 - 发布器(ApplicationEventPublisher):负责“广播”事件。Spring容器本身就是一个
ApplicationEventPublisher,所以你在任何被Spring管理的bean里都能通过ApplicationContext或ApplicationEventPublisherAware拿到发布能力。
一个完整的流程是这样:业务代码调用publisher.publishEvent(event),Spring内部找到所有匹配该事件的监听器,逐个调用它们的处理方法。发布方不需要知道谁在听,监听方也不需要被发布方反向依赖。
1.2 事件机制和直接调用的区别
有的读者可能会问:我直接orderService.onOrderCreated(orderId)调用不就完了吗?为什么要绕一圈事件?我用一个实际场景说明。
假设下单接口成功后要做三件事:发短信通知、扣减库存、更新用户积分。如果直接调用,OrderService就依赖了SmsService、StockService、CreditService三个服务。后面要新增“推送优惠券”,你得改OrderService;某个服务在测试环境不需要,你得改逻辑。时间一长,下单方法变成一团乱麻。
事件机制把“下单成功”这个事实广播出去,SmsService、StockService各自用监听器订阅OrderCreatedEvent。新增“推送优惠券”只需要新写一个监听器,完全不动OrderService。这就是解耦。用一个生活类比:主播在直播间说“今天上链接了”,所有关注他的粉丝各自行动,有人买货、有人录屏、有人发弹幕,主播不需要挨个通知。
1.3 典型使用场景
结合我自己的项目经验,事件机制最常见的几个落点:
- 业务模块解耦:下单、支付、退款等核心域事件,让外部模块异步感知。
- 领域事件的记录与审计:监听关键操作,统一写操作日志。
- 缓存刷新:数据变更后发布事件,监听器刷新本地缓存或分布式缓存。
- 异步任务触发:在事件监听器里配合
@Async或线程池,把耗时操作从主流程摘出去。 - 事务边界处理:用
@TransactionalEventListener实现“事务提交后再发消息”,避免消息发出后事务回滚造成不一致。
如果仅仅是写个ApplicationListener跑通demo,其实很简单,难的是真正理解它背后的执行机制,那样出了问题才知道往哪儿排。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ApplicationListener的两种主流写法
2.1 方式一:实现ApplicationListener接口
先定义一个事件:
java复制public class OrderCreatedEvent extends ApplicationEvent {
private final Long orderId;
public OrderCreatedEvent(Object source, Long orderId) {
super(source);
this.orderId = orderId;
}
public Long getOrderId() {
return orderId;
}
}
再定义一个监听器:
java复制@Component
public class OrderCreatedEventListener implements ApplicationListener<OrderCreatedEvent> {
@Override
public void onApplicationEvent(OrderCreatedEvent event) {
System.out.println("收到订单创建事件,订单ID:" + event.getOrderId());
}
}
这是最传统的写法。ApplicationListener接口带泛型,Spring在注册监听器时会解析泛型类型,只有事件类型匹配的监听器才会被触发。这么做的好处是类型安全,缺点也很明显:一个类只能监听一种事件(准确说是一个泛型参数),如果想监听多个事件,得写多个类,不够灵活。
2.2 方式二:@EventListener注解
注解方式是我个人更推荐的写法:
java复制@Component
public class OrderEventListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("监听器A处理订单:" + event.getOrderId());
}
@EventListener
public void onOrderPaid(OrderPaidEvent event) {
System.out.println("处理支付事件:" + event.getPayId());
}
}
一个类可以写多个监听方法,方法名可以随意,方法参数决定监听哪个事件。@EventListener还能通过condition属性写SpEL表达式做条件过滤:
java复制@EventListener(condition = "#event.orderId % 2 == 0")
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("只处理订单ID为偶数的订单:" + event.getOrderId());
}
这里的#event对应方法参数名。条件不满足时,监听器不会执行。
2.3 监听器注册到容器的过程
很多人用注解用习惯了,反而不知道监听器是怎么注册进容器的。这里我补充一下两种方式的注册差异。
- 实现
ApplicationListener接口的bean,Spring在registerListeners()阶段通过getBeanNamesForType(ApplicationListener.class, true, false)查找,然后注册到ApplicationEventMulticaster。 @EventListener注解方法不是直接注册成监听器。Spring会通过EventListenerMethodProcessor这个SmartInitializingSingleton后置处理器,在所有单例bean实例化完成后扫描@EventListener方法,再通过DefaultEventListenerFactory创建ApplicationListenerMethodAdapter对象,最终注册到多播器。
换句话说,注解方式的底层仍然会被适配成ApplicationListener,只是它在运行时帮你做了一层方法映射。
2.4 异步监听与事务监听用法
@EventListener默认是同步执行的。发布事件的方法会阻塞等待所有监听器执行完。要实现异步,加@Async注解:
java复制@Component
public class OrderEventListener {
@EventListener
@Async
public void asyncProcess(OrderCreatedEvent event) {
// 耗时的通知、日志、消息推送放到这里
}
}
注意,@Async要生效,必须在启动类或配置类上加上@EnableAsync,并且这个监听器bean最终获取到的应该是代理对象,否则异步不生效,执行还是同步。关于异步失效的排查我放到第4章讲。
事务监听是@TransactionalEventListener,它和@EventListener的区别在于可以指定在事务的某个阶段触发:
java复制@Component
public class OrderEventListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterCommit(OrderCreatedEvent event) {
// 事务提交后才执行
}
}
phase可选BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK、AFTER_COMPLETION。默认是AFTER_COMMIT。如果当前没有事务,默认不会执行,可以设置fallbackExecution = true让它在无事务时也执行。
这个特性特别适合“事务提交后再通知外部系统”的消息场景。比如往消息队列里发一条订单消息,如果放在事务提交前发送,一旦后续事务回滚,消息已经发出去了,下游收到一个不存在的订单,就会出现数据不一致。用AFTER_COMMIT能规避这个问题。
3. 源码级拆解:publishEvent之后发生了什么
3.1 publishEvent入口与早期事件缓存
先看发布入口。AbstractApplicationContext实现了publishEvent方法,核心逻辑我简化一下:
java复制protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
} else {
applicationEvent = new PayloadApplicationEvent<>(this, event);
}
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// 父容器也会收到事件
if (this.parent != null) {
// 向父容器发布
}
}
这里有两个细节值得关注。
第一,你发布一个任意对象(比如String或普通POJO)也可以,Spring会包装成PayloadApplicationEvent。
第二,earlyApplicationEvents。容器在启动早期,ApplicationEventMulticaster还没初始化好,这时候如果有人调用publishEvent,事件不会立即广播,而是先暂存在earlyApplicationEvents列表里。等initApplicationEventMulticaster()执行完、registerListeners()注册完所有监听器之后,Spring会把这个列表里的事件统一拿出来发布,然后置空。这个机制保证了容器启动早期的业务事件不会丢失。
从源码可以看到,发布流程的顺序是:publishEvent -> getApplicationEventMulticaster -> multicastEvent。真正干活的不是ApplicationContext,而是多播器。
3.2 ApplicationEventMulticaster如何筛选监听器
默认的多播器实现是SimpleApplicationEventMulticaster,它的multicastEvent方法是这样:
java复制@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
} else {
invokeListener(listener, event);
}
}
}
先说getApplicationListeners(event, type),这一步是“找到所有匹配的监听器”。底层在AbstractApplicationEventMulticaster里,源码大致流程是:
- 用事件类型(
ResolvableType)和事件来源类型(sourceType)组成的ListenerCacheKey查缓存。 - 缓存没命中,就遍历所有已注册的监听器,逐个调用
supportsEvent(listener, eventType, sourceType)判断是否匹配。 - 匹配的监听器放进列表,排序后存入缓存,下次再发同样类型的事件直接走缓存。
invokeListener里面就是真正的回调:
java复制private void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
listener.onApplicationEvent(event);
} catch (Throwable err) {
errorHandler.handleError(err);
}
} else {
listener.onApplicationEvent(event);
}
}
所以如果某个监听器抛异常,默认情况下异常会向上抛,直接中断发布事件的主流程。想隔离监听器异常,需要给SimpleApplicationEventMulticaster设置一个ErrorHandler。
3.3 泛型匹配为什么能精确命中
这里有个常被忽视的点:ApplicationListener<OrderCreatedEvent>和ApplicationListener<OrderPaidEvent>都注册在容器里,事件发布时为什么能精确匹配到正确的监听器?
关键在supportsEvent。看这段逻辑:
java复制private boolean supportsEvent(ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) {
GenericApplicationListener smartListener =
(listener instanceof GenericApplicationListener ?
(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}
GenericApplicationListenerAdapter会去解析监听器类实现的接口泛型。以OrderCreatedEventListener为例,它会通过ResolvableType反射拿到ApplicationListener<OrderCreatedEvent>这个泛型参数,然后调用isAssignableFrom判断当前发布的事件类型是不是目标类型的子类或同类型。
默认的ApplicationEventMulticaster并不是单纯用instanceof判断,而是用Spring的ResolvableType体系,所以它能够识别继承关系。比如监听器监听的是父类事件,发布子类事件时也能匹配上。
@EventListener注解方法在转成ApplicationListenerMethodAdapter后,同样重写了supportsEventType,通过方法参数类型判断是否匹配。这也是为什么@EventListener方法参数能决定监听哪个事件。
3.4 缓存、排序与顺序控制
AbstractApplicationEventMulticaster里有一个retrieverCache,缓存键是ListenerCacheKey,缓存值是ListenerRetriever。同一个事件类型和来源类型,第一次发送时要遍历所有监听器做类型匹配,后面就直接从缓存取,性能开销很小。这个设计有点像CPU的指令缓存,典型的空间换时间。
排序方面,Spring会使用AnnotationAwareOrderComparator对匹配到的监听器排序。写法上可以通过@Order注解或实现Ordered接口控制多个监听器的执行顺序。
java复制@Component
@Order(1)
public class FirstListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("第一个执行");
}
}
@Component
@Order(2)
public class SecondListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
System.out.println("第二个执行");
}
}
@Order的值越小优先级越高。需要注意的是,Spring容器里已经有很多内置监听器,它们的顺序在很多场景下也很重要,比如EventPublishingRunListener在Spring Boot启动流程里的有序发布。
4. 翻车现场:监听器不生效的排查清单
4.1 监听器没执行,先查这五件事
我在团队里经常遇到同事说“我写了监听器但是没反应”,排查来排查去,原因往往就那么几个。
- 监听器没有交给Spring管理:最常见的错误。
@EventListener注解的方法所在的类没有加@Component或类似的注解,Spring根本扫描不到它。 - 事件对象不匹配:发布的是
OrderCreatedEvent,监听器参数写的是OrderPaidEvent,当然不会执行。要注意泛型擦除和继承关系,最好是相同类型,或者监听父类事件。 - 事件发布和监听不在同一个容器:Spring MVC项目里容易遇到
DispatcherServlet的子容器和ContextLoaderListener的父容器两套上下文,事件发布到子容器,监听器注册在父容器,或者反过来,都会出现“发出去没人接”。这个问题在Spring Boot单容器场景下基本不会遇到。 - 事务事件没有事务:
@TransactionalEventListener默认fallbackExecution=false,没有事务时不触发。 @Async异步方法内部异常没打印:监听器确实执行了,但异常被吞了或者没进日志,看起来像没执行。
排查顺序建议:先在监听器方法第一行打日志,再在publishEvent前后打日志,逐步缩小范围。不要一上来就看源码,那样效率低。
4.2 异步监听失效与线程池
前面提到@EventListener和@Async组合可以让监听器异步执行。但有几个坑:
- 没有加
@EnableAsync,异步不生效。 - 监听器方法内部调用同类方法,
@Async不生效,因为Spring AOP代理无法拦截目标对象内部的自调用。 - 线程池没有自定义,直接走默认的
SimpleAsyncTaskExecutor,这个线程池每来一个任务就新开一个线程,高并发下可能造成线程数爆炸。
我的习惯是显式定义一个线程池:
java复制@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("eventExecutor")
public Executor eventExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("event-exec-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
然后在监听器上指定执行器:
java复制@EventListener
@Async("eventExecutor")
public void onOrderCreated(OrderCreatedEvent event) {
// ...
}
用CallerRunsPolicy的好处是队列满了以后,任务会回退到调用线程执行,不会默默丢弃,适合对消息可靠性要求较高的业务。
4.3 监听器抛异常:同步失败与事务回滚
默认情况下,同步监听器里的异常会直接抛给发布者。如果监听器里做数据库操作,而这个监听器方法是在@Transactional方法里被调用的,那么异常会导致事务回滚。
这个行为有时候是好事,比如监听器里校验失败,希望主流程回滚;但有时候是灾难。举个例子:订单服务里publishEvent发布事件,一个积分监听器因为下游接口超时抛异常,结果把订单主流程的事务也回滚了。这种场景下,要么监听器内部自行捕获异常,要么给多播器配置ErrorHandler,把监听器异常隔离。
配置ErrorHandler的前提是覆盖容器默认的多播器:
java复制@Configuration
public class EventConfig {
@Bean
public ApplicationEventMulticaster applicationEventMulticaster() {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.setErrorHandler(throwable -> {
log.error("事件监听器执行异常", throwable);
});
return multicaster;
}
}
经过这样配置,某个监听器抛异常不会影响其他监听器,也不会影响发布方主流程。
4.4 监听器重复执行与内存泄漏
监听器重复执行最典型的原因是重复注册。
一是@EventListener注解扫描机制注册了,又手动调用addApplicationListener注册了一次,同一个逻辑执行两遍。
二是在配置类里每个实例化的ApplicationListener都注册一遍:
java复制@Configuration
public class BadConfig {
@Bean
public ApplicationListener<OrderCreatedEvent> orderListener() {
return event -> System.out.println("处理订单");
}
}
这种情况下,Spring会通过getBeanNamesForType自动发现这个监听器bean并注册。如果你又在某个地方configurableApplicationContext.addApplicationListener(orderListener()),就会注册两次。
内存泄漏的常见场景是:把ApplicationContext.addApplicationListener写在了会被反复调用的方法里,比如每次请求都注册一个匿名内部类监听器。监听器被多播器持有,不能回收,时间久了内存占用越来越高。排查时可以用jstack看线程栈,也可以看看AbstractApplicationEventMulticaster的applicationListeners集合数量。
5. 事件机制在Spring Boot和AI应用里的延伸
5.1 Spring Boot启动核心事件的发布顺序
Spring Boot的启动过程本身就是事件驱动的。SpringApplication.run()方法里会通过SpringApplicationRunListeners发布一系列事件,常见的有:
ApplicationStartingEvent:启动开始,环境还没准备好。ApplicationEnvironmentPreparedEvent:环境准备好了,上下文还没创建。ApplicationContextInitializedEvent:上下文已初始化。ApplicationPreparedEvent:上下文已准备,单例bean还没创建。ApplicationStartedEvent:上下文刷新完成,bean都创建好了。ApplicationReadyEvent:应用满足对外服务条件。ApplicationFailedEvent:启动失败。
读者可以自己实现一个ApplicationListener<ApplicationReadyEvent>,在应用完全启动后执行数据预加载、连接池预热等操作。这里面有个容易踩坑的点:ApplicationListener接口监听ApplicationReadyEvent,但如果你在Web应用里用@PostConstruct做初始化,它执行得更早,可能要等ApplicationReadyEvent才能拿到完整可用的依赖,所以两者要区分清楚使用场景。
5.2 面向AI链路的解耦思路
最近Spring AI相关的话题很热,AI应用里同样可以用事件机制做编排。举一个实际思路:一个问答系统,用户提问后,先调用大模型生成答案,再记录调用统计、做敏感词审核、异步打日志。
如果把这些步骤串行写在Controller里,一次请求可能耗时很长,一个环节出错还影响主流程。更好的做法是:大模型返回结果后,发布一个AnswerGeneratedEvent,统计模块、审计模块、日志模块分别监听这个事件。这样主流程只负责返回答案,附加处理全部异步化。
具体到代码层面,本质上还是@EventListener + @Async。事件机制的价值并不是某个特定框架的功能,而是一种设计模式在Spring生态里的落地。AI应用也好,传统业务也罢,只要存在“事实发生,多个模块各自响应”的场景,都可以用这一套。
再补充一个细节,大模型调用耗时长,如果涉及会话状态持久化,建议把持久化放在@TransactionalEventListener(phase = AFTER_COMMIT)里,避免大模型结果已经入库确认、事务又回滚的尴尬情况。
我在实际项目里最常采用的组合是:核心业务入口发布领域事件,同步监听器只做轻量级状态更新,耗时操作一律走异步监听器,涉及外部系统通知的放到事务提交后。这套组合跑得很稳,问题也少。
最后分享一个小技巧:排查事件问题时,可以临时在SimpleApplicationEventMulticaster上打点,或者直接给多播器设置ErrorHandler并打印完整堆栈,比挨个监听器加日志快得多。事件机制的坑大多不在功能上,而在线程模型和事务边界上,把握住这两条主线,基本就能避开大部分问题。
