Spring 事件机制,比我想的好用
起因
下单成功后要干几件事:发通知、加积分、写操作日志。一开始全塞在 createOrder 方法里,一个方法两百多行,改一处怕碰坏另一处。后来试了试 Spring 的事件机制,把这些拆出去,主流程清爽了不少。
不是所有解耦都得靠 MQ。进程内的事情,Spring 事件够用了,还不用多维护一个中间件。
基本用法
三个东西:事件、发布者、监听者。
事件就是个普通类,继承 ApplicationEvent 或者干脆不继承(Spring 4.2 之后支持任意对象):
public class OrderCreatedEvent {
private final Long orderId;
private final Long userId;
public OrderCreatedEvent(Long orderId, Long userId) {
this.orderId = orderId;
this.userId = userId;
}
// getter
}
发布用 ApplicationEventPublisher:
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Transactional
public void createOrder(OrderReq req) {
Order order = orderDao.insert(req);
publisher.publishEvent(new OrderCreatedEvent(order.getId(), req.getUserId()));
}
}
监听:
@Component
public class OrderEventListener {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 发通知、加积分、写日志
}
}
一个事件可以被多个监听器接,各干各的,互不影响。加积分和发通知拆成两个监听器,以后想删哪个改哪个,动一处不碰另一处。
默认是同步的
publishEvent 默认同步执行,监听器跑完才返回。也就是说,createOrder 里发完事件,得等发通知、加积分全跑完,接口才返回。
这个在有些场景下不能接受。发通知要走 HTTP 调第三方,慢的时候好几秒,用户下单等不起。这时候在监听器上加 @Async,配合 @EnableAsync:
@Async
@EventListener
public void onOrderCreated(OrderCreatedEvent event) { }
变成异步之后,主流程发完事件立刻返回,监听器在另一个线程里跑。代价是异常不好处理,监听器里抛了异常,主流程是感知不到的,得自己在监听器里 try-catch 记日志。
事务边界,这个得注意
最开始的写法有问题。createOrder 标了 @Transactional,事件在事务提交之前就发出去了。监听器里查订单,如果用的是同一个事务,能查到;如果监听器开了新事务或者走了异步,可能查不到——事务还没提交呢。
后来换成了 @TransactionalEventListener:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) { }
它会在事务提交之后才触发。phase 有几个值:BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK、AFTER_COMPLETION。常用的就是 AFTER_COMMIT,保证数据已经落库了再干后续的事。
注意一点:@TransactionalEventListener 只有在有事务的情况下才会触发。如果发布事件的方法没标 @Transactional,监听器永远不执行。这个坑我踩过,方法上事务注解被误删了,监听器静默失效,排查了半天。要兜底的话可以加 fallbackExecution = true,没事务的时候也执行。
顺序问题
多个监听器监听同一个事件,默认顺序不确定。需要控制顺序的话加 @Order:
@Order(1)
@EventListener
public void addPoints(OrderCreatedEvent event) { }
@Order(2)
@EventListener
public void sendNotify(OrderCreatedEvent event) { }
数字小的先执行。不过我一般尽量避免让监听器之间有依赖,有依赖就说明它们不该拆成两个监听器。顺序这东西,加了之后以后加新监听器还得重新想一遍排第几,容易乱。
和 MQ 怎么选
进程内、不需要跨服务、丢了能接受(或者有补偿)的,用 Spring 事件。跨服务、需要持久化、需要重试的,上 MQ。
之前有同事拿 Spring 事件替代 MQ,服务重启的时候正在处理的事件全丢了,对账对不上。事件机制本身不持久化,进程挂了就没了。这个边界得清楚。
小结
Spring 事件解决的是“一个动作触发多个后续操作”的解耦问题,进程内用着很顺手。几个关键点:默认同步、事务场景用 @TransactionalEventListener 且注意它依赖事务、异步监听器得自己处理异常、别拿它替代 MQ。
先记这些。
- 点赞
- 收藏
- 关注作者
评论(0)