跳转到内容

用 Spring Event 解耦业务:从贫血模型到 DDD 的第一步

Spring Event 把“订单已创建”发布给关心它的模块,减少订单服务对通知等模块的直接依赖。 默认监听同步执行,并不自动变成后台消息。需要提交后触发时,用 @TransactionalEventListener;不能丢的交付要另加持久化与重试。

先区分业务主干与通知。订单、库存、优惠券若必须一起成功,就不能仅为解耦把后两者改成异步事件;邮件等提交后动作可以独立处理。以下是模块结构片段,实体、DTO、依赖注入和异步配置由项目提供。

定义事实:

public record OrderCreatedEvent(String orderId, Long userId, BigDecimal amount) {
}

订单服务保存后发布:

@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void createOrder(OrderDTO dto) {
// 1. 业务主干:仅仅保存订单,干净利落
Order order = orderRepository.save(new Order(dto));
// 2. 发布领域事件
eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), order.getUserId(), order.getAmount()));
}
}

事件发布发生在事务方法内,不能将它理解成“数据库已经提交”。监听器采用哪种方式决定了执行时机。Spring 事件机制

同步、异步和提交后有什么差别

Section titled “同步、异步和提交后有什么差别”
监听方式时机与影响注意什么
@EventListener 默认同步调用,通常在发布者线程与事务内阻塞会拖慢主流程;同一事务通常可读自己的未提交写入
@Async + @EventListener交给执行器,新线程可能早于提交运行不继承发布者线程事务,查库可能看不到订单
@TransactionalEventListener(AFTER_COMMIT)成功提交后触发主事务不能被后来异常回滚;没有自动持久化重发
提交后监听再加 @Async提交后交给后台线程配置 @EnableAsync 和执行器,定义失败记录与处理

需要邮件不早于提交时:

@Service
@Slf4j
public class NotificationService {
// 重点:只有当发布者的事务成功 Commit 之后,才会触发这个监听器!
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void sendEmailAfterCommit(OrderCreatedEvent event) {
log.info("订单事务已落盘,安全地发送通知邮件给用户 {}", event.userId());
// 发送邮件...
}
}

发布时没有事务,事务事件监听默认不执行;只有明确启用 fallbackExecution 才处理该情况。Spring 事务事件

故障结果与处理
数据库提交后,进程在监听前退出内存事件可能丢失;重要交付用同事务 Outbox
监听器发信失败订单已提交,另行记录失败并重试或人工补发
同步提交后监听继续写库原资源可能仍可访问,但新修改不再随原事务提交;需要独立事务,例如单独 Bean 的 REQUIRES_NEW
异步监听写库新线程自己建立所需事务,不能假定继承原事务

提交后的数据访问限制见 TransactionalEventListener Javadoc。

单体内用 ApplicationEvent 组织模块依赖;跨服务时把事件合同、持久化、投递、消费者幂等与恢复一起设计。Spring 事件不会自动变成 Kafka 或 RabbitMQ,也不保证发送与业务提交原子完成。

完整 SQL 与恢复链路见Outbox、幂等与重放和分布式事务选型。验收时分别测试提交、回滚、监听失败、重复消费和进程中断,而不只检查收到日志。