Spring 事件发布
前言
事件发布是 Spring 框架中最容易被忽视的功能之一,但实际上它是一个很有用的功能。使用事件机制可以将同一个应用系统内互相耦合的代码进行解耦,并且可以将事件与 Spring 事务结合起来,实现我们工作中的一些业务需求。和 Spring 的事务类似,Spring 事务有编程式事务、声明式事务,Spring 事件也分为编程式事件和声明式事件,本篇主要讲解使用注解实现声明式事件发布和监听。
Spring 内置事件
稍微熟悉 Spring 的话应该不陌生 Spring 的内置事件,Spring 提供了以下几种内置事件。
Event | Explanation |
---|---|
ContextRefreshedEvent | 在 ApplicationContext 初始化或刷新时发布 |
ContextStartedEvent | 使用 ConfigurableApplicationContext 接口上的 start() 方法启动 ApplicationContext 时发布 |
ContextStoppedEvent | 在通过使用 ConfigurableApplicationContext 接口上的 stop() 方法停止 ApplicationContext 时发布 |
ContextClosedEvent | 在通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM 关闭挂钩关闭 ApplicationContext 时发布 |
RequestHandledEvent | 一个特定于 Web 的事件,告诉所有 Bean 已为 HTTP 请求提供服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序。 |
ServletRequestHandledEvent | RequestHandledEvent 的子类,用于添加特定于 Servlet 的上下文信息 |
说实话上面是我从官网抄的,因为我本人对于 Spring 源码并不熟悉,我尝试过去看但是......没有坚持下去。略过这些,下面我们将继承 ApplicationEvent
实现自定义事件。
事件三要素
大家应该都很熟悉网页点击按钮吧,当我们用鼠标点击某一个按钮,弹出一个提示框,这就是一个完整的事件。这个过程包含了三个要素,通俗的来说就是:
- 事件源:谁触发了这个事件?(鼠标)
- 事件:发生了什么?(鼠标点击)
- 事件监听器:事件发生后要做什么?(弹出一个对话框)
了解了事件的三个要素,下面我们具体来看怎样在 Spring
中使用事件。
同步事件
所谓同步事件就是对于发布的事件并不会新开线程去处理,而是在调用方原来的线程基础上执行业务。比如我们现在有个需求是用户提交订单后插入一条该用户的购买日志。这里事件的三个要素可以理解为:
- 事件源:
OrderService
业务类 - 事件:下单
- 监听器:监听下单成功后写日志
在 OrderService
类中
@Service
@Slf4j
public class OrderService {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
/**
* 订单提交
*/
@Transactional
public void submit() {
//...
log.info("提交订单前....");
applicationEventPublisher.publishEvent(new OrderCreateEvent(this, "测试对象"));//发布事件
log.info("提交订单前后....");
}
}
这里的 publishEvent
方法需要传一个继承 ApplicationEvent
的事件类对象
public class OrderCreateEvent extends ApplicationEvent {
@Getter
public Object log;
/**
* @param log 需要传递的参数
* @param source 事件源对象
*/
public OrderCreateEvent(Object source, Object log) {
super(source);
this.log = log;
}
}
我们可以把这个事件需要传递的参数信息封装在事件类 OrderCreateEvent
的成员变量里,这里简单写一个 log 属性。
接下来就是要去监听这个事件了,在 OrderLogService
中使用 @EventListener
标注监听方法,默认情况下,它会监听方法形参对象 Class 类型的事件,假如你的监听方法没有形参,那么你应该用 @EventListener
的 classes 属性去指定要监听的事件类型。
@Service
@Slf4j
public class OrderLogService {
@EventListener
public void listen(OrderCreateEvent event) {
log.info("接受到订单创建事件:{}", event.getLog());
try {
Thread.sleep(10000);//验证事件的同步
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里在监听方法中让线程睡 10s 是为了验证事件的同步性(当然你也可以打印出当前线程的 id 来验证)
提交订单前....
接受到订单创建事件:测试对象
提交订单前后....
观察控制台日志可以得出结论该事件是同步的,原来的业务必须等待事件处理完才会继续往下执行业务。
异步事件
那么你可能已经发现了上述的实现方案其实不太好,因为创建日志的行为其实不属于订单的提交,也就是说每次订单提交的结果还要等发布的事件执行完才能响应,而发布的事件和本次订单提交的业务没有直接的必须同时成功或者同时失败。假如后续随着业务扩大,再增加订单创建后发短信给用户等其他事件,这样会导致接口响应时间变长,吞吐量下降,这无疑是很影响用户体验的。所以我们可以考虑使用异步事件。
实现异步事件非常简单,只需要在 SpringBoot
启动类上添加 @EnableAsync
启用异步功能,然后监听器方法上添加 @Async
注解即可。
@EventListener
@Async
public void listen(OrderCreateEvent event) {
//...
}
这样监听方法的执行就是异步的,不会影响原来订单提交接口的吞吐量。
将事件绑定事务
那么你可能已经发现了,上述异步事件解决的接口吞吐量问题,但是又带来一个问题。假如订单提交的业务失败了怎么办?因为异步事件监听器抛出 Exception
,是不会将其传播到调用方的。也就是说上述订单提交业务如果报错,那么我们的记录日志事件、发短信事件还是会执行,那么这个问题比接口吞吐量问题要严重的多,所以 Spring
允许我们将事件和事务进行绑定。
使用起来也非常简单,只需要将原来监听器的注解 @EventListener
换成 @TransactionalEventListener
即可。
@TransactionalEventListener
public void listen(OrderCreateEvent event) {
//...
}
我们可以通过该注解的 phase 属性来决定监听器要在原调用方事务的哪个阶段开始执行。总共有四种值
- TransactionPhase.BEFORE_COMMIT —— 事务提交前
- TransactionPhase.AFTER_COMMIT —— 事务提交后(默认值)
- TransactionPhase.AFTER_ROLLBACK —— 事务回滚后
- TransactionPhase.AFTER_COMOLETION —— 事务完成后(包括事务提交和回滚)
值得注意的是使用 @TransactionalEventListener
的监听器,其事件调用方必须要有事务,否则将不会被执行。 这意味着光有 @Transactional
注解也不行,必须要有数据库相关整合,因为 @Transactional
其实也是通过修改数据库连接的 auto_commit
属性 为 false
来实现事务不自动提交的。
条件事件
@EventListener
和 @TransactionalEventListener
都有 condition 属性,可以用来判断事件的参数满足一定条件的时候执行监听事件。例如:
@EventListener(condition = "event.log == '测试对象2'")
public void listen(OrderCreateEvent event) {
//...
}
如果说发布事件传递的参数值不是该条件中指定的值,那么该监听器也不会执行。
顺序事件
我们可以通过 @Order
注解来控制监听器的执行顺序,该注解的值越小,执行的顺序越靠前。不过在异步事件中不建议使用它来控制顺序,因为那样意义不大。
@EventListener
@Order(1) //此监听器将会第一个执行
public void listen(OrderCreateEvent event) {
//...
}
@EventListener
@Order(2) //此监听器将会等待上一个执行完才会执行,
public void listen2(OrderCreateEvent event) {
//...
}
不要在事件监听器中再发布事件
这是一个善意的避免采坑忠告,在已存在的事件监听器(尤其是和事务相绑定的)中发布事件,然后再用监听器监听,可能会导致整个链路的事务出现不符合逾期的结果。比如该回滚的没回滚,改变了事务的传播行为却不生效等问题。这是我曾经踩过的坑,不过话又说回来了,我也不知道我当初怎么会在一个事件监听器中再发布一个事件......
事件和消息队列
也许你已经发现了,Spring 的事件发布和消息队列有很多相似的地方。那么我们来对比下两者的异同
相同点
- 解耦:将代码中耦合的地方解耦分离
- 异步:都可以异步执行某一项不属于当前方法业务的事情,提高系统吞吐量
不同点
- 使用范围:事件只能在应用内使用,无法跨系统,而消息队列可以跨多个系统。
- 削峰能力:事件的削峰是很局限的,相当于开启多个线程,这样并不好线程越来越多会消耗服务器资源,。消息队列的削峰是引入了第三方中间件,能够有效进行流量削峰,承载高并发请求量。
- 同样的,事件由于使用范围局限,带来的问题也少,消息队列由于使用范围广,引入的问题也会比较多,要保证消息队列的高可用,解决消息可靠性、幂等性等问题。
总结
Spring
事件发布适用于小型项目,对于高并发流量的业务还是需要专业的消息中间件来支撑。不过通常我们会将 Spring
事件结合消息队列一起用。
结语
如果这篇文章对你有帮助,记得点赞加关注。你的支持就是我继续创作的动力!
转载自:https://juejin.cn/post/6995085073684447239