
简介这是一份Spring事件机制Spring Event的可运行示例工程面向需要理解并快速上手Spring事件驱动开发的Java学习者。资源以Eclipse Maven工程形式组织代码完整覆盖事件定义、事件发布、事件监听三大核心环节同时演示EventListener注解与ApplicationListener接口两种监听写法以及Order控制监听顺序、Async异步处理等进阶用法适合在项目解耦或模块通信场景中参考模仿。资源共75个文件其中25个Java源码与25个编译后class文件是核心配套8个XML配置、3个properties文件及Eclipse工程配置便于直接导入运行验证。压缩包仅44KB结构清晰无冗余。目前已有680人浏览学习。借助该示例可快速理解Spring事件从创建UserRegisteredEvent、通过ApplicationContext.publishEvent发布到监听器接收响应的完整链路同时掌握监听器注册、优先级排序、线程模型等关键知识点是深入掌握Spring框架事件机制的高性价比参考资料。1. Spring Event 不是魔法先搞清楚它解决什么问题Spring Event 是 Spring 框架内置的事件发布与监听机制常被拿来解耦业务链路。很多人第一次看到示例时以为它只是观察者模式的样板代码复制过来一跑发现日志顺序不对、事务没提交、异步不生效甚至性能翻车。我会从一个最小可复现示例开始带你把同步监听、异步监听、事务事件监听器的姿势都调对顺带讲清楚线程模型和事务边界这两个最容易踩坑的地方。适合 Spring Boot 开发者尤其是正在做订单、支付、通知这类需要横向扩展业务的模块。2. 事件驱动还是直接调用Spring Event 的原理与选型判断2.1 事件发布与监听的核心链路ApplicationEventPublisher 与 ApplicationListenerSpring Event 的底层并不复杂。容器里保存着一张监听器映射表发布事件时ApplicationEventPublisher 会查这张表按事件类型找到对应的 ApplicationListener逐个调用 onApplicationEvent 方法。这里的关键是“逐个调用”默认是同步的发布线程会阻塞在发布调用上直到所有同步监听器执行完返回。所以传统印象里的“发布一下就走”并不准确除非你主动加上异步。代码层面如果你追过源码会看到 Spring 把带 EventListener 的方法解析成 ApplicationListenerMethodAdapter它实现了 ApplicationListener方法参数类型用来匹配事件。这个适配器在容器刷新时就已经注册。事件对象通常继承 ApplicationEvent但 Spring 4.2 之后支持任意 Object你可以不继承任何类直接 publishEvent(new MyEvent(...))。后面示例我就用普通 POJO 做事件这样不污染业务领域对象。一个最简单的发布调用长这样Service public class OrderService { private final ApplicationEventPublisher eventPublisher; public OrderService(ApplicationEventPublisher eventPublisher) { this.eventPublisher eventPublisher; } public void createOrder(OrderDTO dto) { // 业务逻辑校验、落库 Order order save(dto); // 发布事件同步监听器此刻就要执行 eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), order.getUserId())); } }publishEvent 后会发生什么Spring 会遍历所有匹配 OrderCreatedEvent 的监听器在同一个线程里调用它们。监听器里抛异常发布方法也会抛异常如果 createOrder 处于事务中异常会导致事务回滚。这一点决定了你在设计事件内容时不能塞那种“失败也无所谓”的通知逻辑。参数上publishEvent 有一个重载支持 source 参数另外 Spring 4.2 里还有 EventListener 上的 condition 属性后面第 6 章会用 SpEL 控制监听粒度。核心链路到此为止真正要花心思的是“到底该不该用它”。2.2 直接调用 vs 事件解耦什么时候该用 Spring Event直接调用最简单比如在 createOrder 里调用 notifyService.sendMessage(order)代码顺序明确调试时点进栈就能看到调用关系。但缺点也很明显目标服务类硬编码在业务代码里跨模块时很容易形成底层依赖关系。比如订单模块直接依赖消息中心模块的接口后续消息中心升级订单模块就得跟着改。事件解耦的优势是订单模块只需要发布一个 OrderCreatedEvent不关心谁在监听。这个监听器可以放在消息中心模块也可以放在售后模块甚至一个新模块加一个 EventListener 就行不需要改动已有业务方法。适合的事件场景有三类第一类是审计、日志、通知这类横向关注点第二类是业务状态变化后需要触发多个模块联动比如订单支付成功要发短信、通知仓库、更新积分第三类是同一事务内需要多个组件在提交后做处理用事务事件监听器。不适合的场景也明确事件只在当前 JVM 进程内传播进程重启、消息堆积、投递失败都没有保障。如果你需要持久化、重试、跨服务广播应该上 MQ而不是用 Spring Event。还有一个容易被忽视的边界当监听器很多或者链路很长时同步事件会拖慢主流程异步事件又带来可靠性问题所以选型要把“写入一致性”放在第一位不能在监听器里做必须强一致的数据更新。下面是判断用的对照表维度直接调用Spring Event同步Spring Event异步耦合度发布方依赖目标类发布方不依赖监听器发布方不依赖监听器调用线程发布方线程同一个线程线程池线程异常传播异常直接向上抛监听器异常向上抛监听器异常不影响发布方事务边界发布的业务方法控制监听器在同一事务内执行监听器不能分享发布方事务调试难度调用链清晰调用链经过容器广播日志难串需要 traceId可靠性调用失败当场知道失败能感知可能回滚业务失败难以感知易丢通知这张表我建议直接存下来。选型时先问这个联动如果失败主业务要不要跟着失败如果不要别用默认同步事件要么捕获异常要么异步如果一定要在同一事务里保证就考虑事务事件监听器和同步事件。2.3 事件模型选型继承 ApplicationEvent 还是普通 POJO如果你翻开老代码会看到不少事件类继承 ApplicationEvent比如public class OldOrderEvent extends ApplicationEvent { private final Order order; public OldOrderEvent(Object source, Order order) { super(source); this.order order; } }这种写法在 Spring 4.2 之前是常态因为旧版发布方法只能接受 ApplicationEvent。但现在再这么写你会遇到几个实际问题第一构造器必须传 source这个 source 通常没有业务含义但每个事件实例都要多存一个引用第二你的领域事件跟 Spring 的 BaseEvent 绑在一起以后想把这个事件在 JSON 里传递序列化时会出现奇怪的 source 字段第三事件类的继承树多了一层框架类写单元测试时容易初始化失败。我建议用普通 POJOpublic class NewOrderEvent { private final Long orderId; public NewOrderEvent(Long orderId) { this.orderId orderId; } public Long getOrderId() { return orderId; } }publishEvent(new NewOrderEvent(id)) 照样能触发监听器。Spring 会自动把它包装成 PayloadApplicationEvent你的监听器方法参数只要写 NewOrderEvent 就能匹配完全不需要关心包装层。事件对象的核心是“携带哪些数据”不是“属于哪个框架类型”。这里有个容易翻车的细节使用普通 POJO 时同一个对象实例同时被多个监听器共享如果你在事件里放了可变字段多监听器之间会相互影响。所以事件类尽量把字段做成 final只提供 getter。如果你实在需要一个可累积的结果容器单独放一个 AtomicReference 之类的组件不要暴露 setter 污染事件。2.4 快速落地的依赖与初始化一个空项目就能开始Spring Event 的核心在 spring-contextSpring Boot 项目只要装了 spring-boot-starter 或 spring-boot-starter-web 就能用。不需要额外引入 spring-boot-starter-activemq 之类的东西很多新手会把事件和消息队列搞混去加不必要的依赖反而让启动变复杂。如果你用的是 Maven项目里至少要有下面的基础依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency如果你需要验证 HTTP 接口或者写个 web 场景再加 spring-boot-starter-web。版本由 Spring Boot 父工程管理不要在子模块里单独写版本否则容易遇到 Spring 4.2 之前不支持普通对象事件的问题。如果你还在用老版本 Spring Framework最好升到 4.2 以上或者把事件类继承 ApplicationEvent否则监听器可能匹配不上。初始化动作只有一个保证事件发布器和监听器都在 Spring IoC 容器的扫描范围内。启动类上加 SpringBootApplication默认扫描主类所在包及其子包。我之前帮某个开发同学排查过“监听器不触发”的问题结果就是把监听器放在了另一个包而主类在父包扫描路径覆盖不到。这时候要么把启动类移动到两个包的公共父包要么在 SpringBootApplication(scanBasePackages ...) 里显式指定。如果你想先不写业务快速验证 Spring Event 链路可以在启动类里放一个 CommandLineRunnerBean public CommandLineRunner eventCheck(ApplicationContext context) { return args - Arrays.stream(context.getBeanDefinitionNames()) .filter(name - name.contains(Listener)) .forEach(name - System.out.println(listener bean: name)); }这只是帮助你看扫描到的监听器 bean能不能真正触发还是需要发布一个事件。所以第 3 章我会带你把完整示例跑起来这里的验证只是保证监听器确实进入了容器。3. 第一个可复现的 Spring Event 示例同步监听与最小依赖3.1 定义业务事件订单创建事件的实体设计干净的示例得先从事件类开始。我不建议继承 ApplicationEvent原因在前一章已经讲过。订单创建事件最朴素的写法package com.example.order.event; import java.time.Instant; public class OrderCreatedEvent { private final Long orderId; private final Long userId; private final Integer amount; private final Instant occurredAt; public OrderCreatedEvent(Long orderId, Long userId, Integer amount, Instant occurredAt) { this.orderId orderId; this.userId userId; this.amount amount; this.occurredAt occurredAt; } public Long getOrderId() { return orderId; } public Long getUserId() { return userId; } public Integer getAmount() { return amount; } public Instant getOccurredAt() { return occurredAt; } }字段用 final 修饰通过构造器初始化对外提供 getter这样事件在监听器里不能被改动。事件是跨方法传递的上下文应当尽量不可变。我见过有人把事件做成带 setter 的 JavaBean监听器里顺手改几个字段多个监听器之间互相覆盖查了半天数据不对。如果你确实需要监听器返回结果不要往事件里塞 setter用 3.4 节说的结果容器。参数说明orderId 和 userId 是核心维度amount 供后续金额处理使用occurredAt 记录发生时间注意最好用 Instant不要用 LocalDateTime避免时区混淆。事件类不需要放到某个固定包但按模块建一个 event 包扫描路径相对清晰。3.2 发布事件把 publishEvent 放进业务代码的正确位置发布事件的位置决定了监听器是在事务提交前执行还是提交后执行。如果你现在还没加事务只是做一个普通 Service最简单的方式是业务逻辑完成后立刻发布。package com.example.order.service; import com.example.order.event.OrderCreatedEvent; import com.example.order.model.Order; import com.example.order.repository.OrderRepository; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.Instant; import java.util.concurrent.atomic.AtomicLong; Service public class OrderService { private final ApplicationEventPublisher eventPublisher; private final OrderRepository orderRepository; private final AtomicLong idGen new AtomicLong(1000); public OrderService(ApplicationEventPublisher eventPublisher, OrderRepository orderRepository) { this.eventPublisher eventPublisher; this.orderRepository orderRepository; } Transactional public void createOrder(OrderDTO dto) { Order order new Order(idGen.incrementAndGet(), dto.getUserId(), dto.getAmount()); orderRepository.save(order); // 这里发布的事件监听器会在当前事务内执行 eventPublisher.publishEvent(new OrderCreatedEvent( order.getId(), order.getUserId(), order.getAmount(), Instant.now() )); // 方法结束后事务提交 } }注意我在 createOrder 上加了 Transactional。同步监听器在 publishEvent 调用点执行所以它还在事务内。如果监听器对数据库做查询读到的是尚未提交的订单数据如果监听器抛异常事务会回滚订单和监听器的更新都会被撤销。这个行为符合“强一致联动”的需求但不符合“通知失败不影响主订单”的需求。所以发布位置的原则是把事件当成业务结果的一部分而不是后续动作。业务主体已完成、订单已落库、事务还没提交这个区间发布事件是标准做法。你要是把事件挪到事务方法外面发布监听器就看不到事务内数据事务边界又要重新设计。参数说明ApplicationEventPublisher 的 publishEvent 有两个常用重载一个是 Object一个是 ApplicationEvent。我们不继承 ApplicationEvent直接传普通对象。少数场景下 Spring 会要求你传一个带“revision”的 PayloadApplicationEvent其实内部包装不需要你处理。3.3 监听事件EventListener 注解与监听器编写规范有了事件和发布方接下来就是监听器。监听器是普通 Spring Bean方法上标 EventListener参数类型表示要监听的事件。Spring 会给每个事件类型生成一个监听器适配器匹配规则是方法参数的赋值兼容性也就是 OrderCreatedEvent 可以匹配 MyOrderCreatedEvent 的子类。package com.example.order.listener; import com.example.order.event.OrderCreatedEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; Component public class OrderNotificationListener { private static final Logger log LoggerFactory.getLogger(OrderNotificationListener.class); EventListener public void onOrderCreated(OrderCreatedEvent event) { // 这里是同步执行异常会导致发布方事务回滚 log.info(收到订单创建事件订单ID{}, 用户{}, 金额{}, event.getOrderId(), event.getUserId(), event.getAmount()); // 调用远程通知服务 notificationClient.pushUser(event.getUserId(), 订单已创建); } }这个监听器是典型的同步副作用型监听器打日志、调通知。问题在于notificationClient.pushUser 如果失败抛异常事件发布方的事务会一起回滚用户会看到一个“订单没创建成功”的提示但实际上订单可能已经保存了。这个矛盾后面会专门讲。监听器方法命名随意但建议统一叫 onXxx 或 handleXxx代码里一眼看得出是事件处理方法。监听器内不要写复杂业务更不要反过来调用事务方法写主库否则你在同一个线程里又开了一个事务看起来在同一事件里实际上事务边界不同容易死锁。如果要在监听器里写数据请明确标注“这里是监听副作用”。3.4 同步事件的执行顺序和返回值不要忽略的细节同步事件里同一个事件的多个监听器执行顺序默认不保证。Spring 提供 Order 注解来安排顺序数值越小越先执行。注意 Order 放在监听器方法上Spring 解析 EventListener 时会读取它的排序值。EventListener Order(1) public void onOrderCreatedForAudit(OrderCreatedEvent event) { } EventListener Order(2) public void onOrderCreatedForNotify(OrderCreatedEvent event) { }第一个监听器先执行。但你不能依赖这个顺序做核心状态流转因为异步事件和多模块项目里顺序控制会因为代理、线程池和监听器装配顺序变得不可控。顺序只适合“审计先记录通知后发送”这种弱依赖场景。返回值问题Spring Event 原生不支持监听器返回值返回给发布方。你如果写了 public String handle(OrderCreatedEvent event)Spring 会忽略除非再定义一个监听器监听返回值事件但那是另一套机制容易把自己绕晕。常用的做法是事件对象里放一个结果容器比如 AtomicReferencepublic class OrderCreatedEvent { private final Long orderId; private final AtomicReferenceString auditResult new AtomicReference(); // getter... public void setAuditResult(String result) { auditResult.compareAndSet(null, result); } public String getAuditResult() { return auditResult.get(); } }发布方 publishEvent 返回后读 auditResult就能拿到监听器产生的数据。这是一种逃逸用法适合少量“需要等待一个监听器计算”的场景但不适合多个监听器各自返回结果。多个监听器结果建议定义成一个事件响应对象监听器写入不同字段发布方统一读取。注意不要拿它做跨线程通信异步监听器里写这个容器发布方不等它执行完就读了。3.5 用集成测试验证同步链路事件是否真的被监听光靠日志不够写一个集成测试更靠谱。测试里直接注入 ApplicationEventPublisher发布一个订单创建事件然后断言监听器是否执行。SpringBootTest class OrderEventApplicationTest { Autowired private ApplicationEventPublisher publisher; Autowired private TestOrderListener listener; Test void orderCreatedEvent_shouldTriggerListener() { publisher.publishEvent(new OrderCreatedEvent(1001L, 202L, 599, Instant.now())); // 监听器里维护一个计数器发布后检查 assertEquals(1, listener.getInvocationCount()); } }监听器里需要一个可观测的字段比如 AtomicIntegerComponent public class TestOrderListener { private final AtomicInteger count new AtomicInteger(); EventListener public void onOrderCreated(OrderCreatedEvent event) { count.incrementAndGet(); System.out.println(收到事件 event.getOrderId()); } public int getInvocationCount() { return count.get(); } }为什么用集成测试而不是直接把 createOrder 调一遍因为事件发布点可能在多个 Service 方法里你直接调业务方法还得准备数据库、事务、DTO验证链路时容易掺杂其他噪音。直接发布事件只看事件机制是否工作出了问题时定位更快。这个测试也适合在接入 Spring Event 第一天就跑通之后每次新增监听器都会自动覆盖。4. 让事件异步化与事务联动Async 和事务监听器的正确姿势4.1 异步事件Async EventListener 的组合当你不想让监听器拖慢主流程比如发短信、推送、生成报表可以把监听器方法加上 Async。这样事件发布后publishEvent 不会等监听器完成而是把监听器执行任务丢进线程池发布方立即返回。最小改动Component public class OrderNotificationListener { Async EventListener public void onOrderCreated(OrderCreatedEvent event) { // 这里运行在线程池里和发布方已经不是同一个线程 notificationClient.pushUser(event.getUserId(), 订单已创建); } }注意前提是 Spring 上下文中启用了异步代理。启动类上需要加 EnableAsync否则 Async 不生效也不会有任何提示这是新手最容易遇到的黑匣子。SpringBootApplication EnableAsync public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }加了 EnableAsync 之后Spring 会为带 Async 的 bean 创建代理。事件监听器方法被 ApplicationListenerMethodAdapter 调用时实际上调用的是代理对象于是能切到线程池。这里有个细节如果你在同一个类里给另一个方法加了 Async然后自己调用它代理不会生效这叫“自调用问题”。但事件监听器是 Spring 外部调用的所以不违反。异步后异常处理完全不同监听器抛异常发布方感知不到也不会影响事务。所以你必须做好异步任务失败的兜底最常见的做法是配置自定义 AsyncUncaughtExceptionHandler统一记录错误。package com.example.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; Configuration public class AsyncConfig implements AsyncConfigurer { private static final Logger log LoggerFactory.getLogger(AsyncConfig.class); Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(4); executor.setMaxPoolSize(8); executor.setQueueCapacity(200); executor.setThreadNamePrefix(event-exec-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (throwable, method, params) - log.error(异步事件执行失败method{}, 参数{}, method, params, throwable); } }参数说明corePoolSize4 是核心线程数maxPoolSize8 是最大线程数queueCapacity200 是等待队列容量。线程池策略默认是 AbortPolicy队列满了直接抛异常这个异常在异步模式下会丢失用 CallerRunsPolicy 会让任务退回发布线程执行虽然阻塞了但至少不丢弃消息适合事件通知这种“最好别丢”的场景。你也可以直接从 Spring 配置里注入已有的 ThreadPoolTaskExecutor然后用 Async(executorName) 指定。4.2 事务事件监听器TransactionalEventListener 与事务边界上一章那个 EventListener 在事务内执行的问题Spring 从 4.2 开始提供了 TransactionalEventListener把监听器的执行时机绑定到事务生命周期。默认在事务提交之后执行也就是 AFTER_COMMIT这时监听器读到的数据都是已经提交的不会看到脏状态。Component public class OrderRepositoryListener { TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onOrderCreatedAfterCommit(OrderCreatedEvent event) { // 事务已提交这里做一些后续清理或通知 log.info(订单事务已提交事件信息: {}, event.getOrderId()); eventStatistics.addOrder(event.getOrderId()); } }phase 的五个取值取值执行时机使用场景BEFORE_COMMIT事务提交前需要在事务里补充数据AFTER_COMMIT事务提交后发通知、推送、调度AFTER_ROLLBACK事务回滚后补偿、告警AFTER_COMPLETION事务结束后无论提交或回滚清理资源、统一统计有一个很容易被忽略的坑TransactionalEventListener 默认 fallbackExecution 为 false如果发布事件的方法没有事务监听器根本不会执行。Spring 官方解释是“没有事务就没法确定提交时机”所以事件就直接丢掉。如果你希望没有事务时也触发可以设置 fallbackExecution true在监听器里加个参数TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT, fallbackExecution true) public void onOrderCreatedFallback(OrderCreatedEvent event) { // 无条件执行 }但这个写法会让同一个事件在“有事务”和“无事务”两条路径上的执行语义不一样你最好在发布方统一保证有事务而不是依赖 fallbackExecution。因为 fallbackExecution 只是让监听器在有事务时提交后执行没有事务时就当普通同步事件执行并不是你主动控制的。事务事件监听器还有一个特点是在事务提交后由 Spring 同步调用的线程仍然是提交事务的业务线程但事务上下文已经关闭所以监听器里不能再访问那个事务的数据库连接快照需要自己开新事务或查询最新数据。4.3 自定义线程池与监听器执行顺序让异步事件可管理异步事件最怕的是线程池混用。如果你把 Async 不加指定Spring 会使用 AsyncConfigurer 里配置的全局执行器如果多个业务模块都用 Async所有的异步事件都挤在同一个池一旦某个任务阻塞其他模块的异步逻辑全部遭殃。更细的做法是为事件单独建一个执行器监听器方法上通过 Async(eventTaskExecutor) 指定。Bean(eventTaskExecutor) public Executor eventTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(4); executor.setQueueCapacity(500); executor.setThreadNamePrefix(event-task-); executor.initialize(); return executor; }Async(eventTaskExecutor) EventListener public void onOrderCreated(OrderCreatedEvent event) { }这样做的好处是运维时能根据线程名前缀判断是事件监听线程还是普通业务线程。线程池参数上事件监听任务通常涉及调 HTTP、发短信属于 IO 密集corePoolSize 不宜过大4 到 8 个就够队列容量可以放宽避免线程数猛增。maxPoolSize 是超过队列容量后才扩到上限如果任务长期积压要优先看队列和下游耗时而不是无脑加线程。最后说执行顺序。同步事件用 Order 能排序但异步事件你没法保证执行顺序因为每个监听器提交到线程池的时间点不同。如果你有“必须先 A 后 B”的需求把 A 和 B 合并成一个监听器或者在做成异步之前先按同步跑通。我见过有人指望两个异步监听器用 Order 顺序执行结果线程池一忙顺序完全颠倒最后只能改成同步事件。5. Spring Event 避坑指南线程丢失、监听循环与事务提交的 5 个坑避坑这种事光看源码记不住。下面 5 个坑都是我在实际项目里见过或者自己踩过的每一条都按现象、原因、解决三步写。你要是能把这张清单贴在项目文档里至少能少几个线上问题。5.1 事件在事务提交前执行监听器读不到已提交数据现象订单创建后同步监听器去查订单表查出来是空或者从数据库读出来的订单状态还是旧值。原因EventListener 默认在发布调用点执行而发布调用点在事务方法里此时数据库事务还没有提交。监听器如果有独立数据库连接看到的是事务外未提交的快照自然查不到。解决换成 TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT)让监听器在事务提交后执行如果不想依赖事务就确保发布方在事务外发布但这样就失去了事务边界的一致性。5.2 同步监听器异常导致主业务回滚现象订单已保存但监听器里的短信服务调用失败了前台提示“下单失败”数据库里订单也没了。原因同步监听器异常会直接抛给发布方发布方法处于事务中运行时异常触发回滚。解决如果监听器副作用不影响主数据用 try-catch 在监听器里包住远程调用日志记录失败或者把监听器改成 Async让异常进线程池。如果确实需要主流程感知就不要用异步要设计补偿。5.3 异步监听器异常“静默丢失”现象日志里没有任何错误但异步通知一直没有发出去业务代码也没报错。原因Async 的监听器跑在线程池异常被吞进了线程池的 rejected/runtime 异常机制默认没有输出关键日志。解决实现 AsyncUncaughtExceptionHandler在 getAsyncUncaughtExceptionHandler 里打印方法名和参数并区分是哪类执行器生产环境最好把异步异常写到专门的错误表或告警系统。另一个坑如果用 ThreadPoolTaskExecutor拒绝策略选 AbortPolicy拒绝异常也会被吞掉建议用 CallerRunsPolicy。5.4 事件重复触发或监听器被扫描两次现象发布一次事件监听器打印了两次日志有时候业务数据执行了两次。原因最常见的是容器中有父子上下文比如 Spring Boot 加 Spring Security 可能引入多个 ApplicationContext监听器在父容器和子容器各注册了一次也可能是监听器所在的包被 SpringBootApplication 扫描又被配置类 Import 重复注册导致同一个 bean 有多个代理。解决先检查启动日志里监听器 bean 是否有重复定义减少扫描范围主要业务监听器统一放到一个包不要同时用 scanBasePackages 和 Import 加载同类。如果确实需要两个上下文可以把事件发布加一个应用标识监听器里判断来源避免重复响应。5.5 泛型事件因类型擦除被所有监听器匹配现象定义一个通用事件 DataEvent 监听器方法写 public void handle(DataEvent event)结果 DataEvent 也触发它。原因虽然 Spring 可以用 ResolvableType 解析泛型但如果你在监听器方法参数上用了原始 DataEvent或者监听器是在旧版本 Spring 环境下解析类型变量被擦除成 Object所有 DataEvent 都会匹配。解决为每个业务域定义独立事件类不要用同一个泛型外壳非要用泛型要在 EventListener 的 condition 里加类型判断比如 #event.payload instanceof T。Spring 对泛型的解析细节容易变成玄学建议用类型清晰的领域事件维护成本更低。6. 给事件加业务上下文SpEL 条件监听与事件发布封装6.1 用 SpEL 条件监听让一个监听器只关注特定事件当事件类携带一个枚举或状态字段你可以用 condition 属性过滤省掉监听器里 if 判断。public enum OrderEventType { CREATED, PAID, CANCELED } public class OrderEvent { private final Long orderId; private final OrderEventType type; // constructor, getters... }EventListener(condition #event.type T(com.example.order.event.OrderEventType).PAID) public void onOrderPaid(OrderEvent event) { // 只有 PAID 类型才进来 }说明condition 通过 SpEL 解析事件对象作为 #event 变量访问 getType 方法得到枚举。T(...) 用来引用枚举类。如果事件字段是 boolean可以写 #event.success解析 getSuccess()。注意 condition 表达式是在发布线程里求值的如果太复杂会影响发布性能建议只放简单判断。如果你没有开启编译器 -parameters 参数方法参数名可能没有被编译进去导致 #event 解析不出来这时要么开启 -parameters要么直接改用 instanceof 判断。6.2 事件发布器的封装为以后替换消息队列留后路我通常会在项目里留一个薄接口不让业务代码直接碰 ApplicationEventPublisher。这样以后需要加消息头、消息轨迹、批量发送或者想换成 MQ只改一个实现类。public interface DomainEventPublisher { void publish(Object event); }Component public class SpringDomainEventPublisher implements DomainEventPublisher { private final ApplicationEventPublisher eventPublisher; public SpringDomainEventPublisher(ApplicationEventPublisher eventPublisher) { this.eventPublisher eventPublisher; } Override public void publish(Object event) { eventPublisher.publishEvent(event); } }业务代码注入 DomainEventPublisher发布时只看到“发布一个事件”不关心底层是同步还是将来要异步。测试时也能直接 mock 这个接口不用启动整个 Spring 容器。这是我的习惯所有领域事件的出口收敛到一个对象上既方便加全局日志也能避免直接依赖 Spring 的上下文类。曾经我图省事把 ApplicationEventPublisher 直接塞到了各个 Service后来要加消息头改了十几个类后悔没早做这层封装。如果你已经用了 Spring Event建议从今天开始把事件发布器抽出来抽出来的成本比想象中低。希望帮到你。本文还有配套的精品资源点击获取