官方文档:标准和自定义事件,本章只介绍自定义事件,这里是 单机事件 官方文档笔记 该功能在 spring-framework core 包中,所以一般依赖一个 boot 包,就已经可以使用这个功能了 Spring Event 如果在 SpringCloud 环境中,还可以利用 SpringCloudBus 实现分布式事件驱动,可以看这个源码解析笔记 Spring Cloud Bus 将单机事件变成分布式事件机制(远程事件)例子
这个事件有什么作用?简单说他的功能是:模拟一个生产消息、订阅消息的功能,在程序内实现一些逻辑的解耦。
比如:创建了一个账户,要给这个账户邮箱发送通知,有时候发送邮件比较耗时,可以起一个线程异步发送,那么使用 Spring Event 就能更优雅的解决这个问题。
- 创建账户后,发布一个事件
- 监听这个事件,然后给这个发送邮件(可以同步或则异步,异步就类似使用一个新的线程来发送,同步则像调用方法一样)
1. 实现一个自定义事件 ApplicationEvent
实现 ApplicationEvent 接口,将消息内容封装到该类里面
package cn.mrcode.event;import org.springframework.context.ApplicationEvent;import lombok.Getter;import lombok.ToString;/*** 审批单创建事件*/@ToString@Getterpublic class ApprovalCreateEvent extends ApplicationEvent {private Long id;private ApprovalDataType dataType;private Integer dataId;// 第一个参数:是事件发布器程序,可以理解为,从事件中可以获取到该事件是从哪一个 发布器程序中发布的// 其他参数都是你的业务信息// 这里通过构造方式传入,是不想这个消息在外部被更改,通用这里只提供农了 @Getter,并没有提供 setter 方法public ApprovalCreateEvent(Object source, Long approvalId, ApprovalDataType dataType, Integer dataId) {super(source);this.id = approvalId;this.dataType = dataType;this.dataId = dataId;}// 还可以定义自定义的方法public boolean isSame(ApprovalDataType dataType) {return dataType == this.dataType;}}
2. 实现事件发布器 ApplicationEventPublisherAware
事件发布器,就类似与 MQ 的消息投递
实现 ApplicationEventPublisherAware 接口,获得 Spring 注入的事件发布对象 ApplicationEventPublisher,然后通过该对象将 事件发布出去
package cn.mrcode..event;import org.springframework.context.ApplicationEventPublisher;import org.springframework.context.ApplicationEventPublisherAware;import org.springframework.stereotype.Component;/*** @author mrcode*/@Componentpublic class ApprovalEventPublisher implements ApplicationEventPublisherAware {// 持有这个事件发布对象private ApplicationEventPublisher publisher;@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.publisher = applicationEventPublisher;}/*** 审批单创建事件** @param approvalId* @param dataId*/public void publishCreateEvent(Long approvalId, ApprovalDataType dataType, Integer dataId) {publisher.publishEvent(new ApprovalCreateEvent(this, approvalId, dataType, dataId));}}
在发布器里面编写了一个 publishCreateEvent 发布创建事件的方法,里面又委托 事件发布对象,将 ApplicationEvent 发布了出去
3. 监听事件 - EventListener
事件监听就类似与 MQ 的消息消费
这里会使用 @EventListener 注解来标识我们对哪一个事件感兴趣
package cn.mrcode.service.lister;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.event.EventListener;import org.springframework.scheduling.annotation.Async;import org.springframework.stereotype.Service;import cn.hutool.core.util.StrUtil;import lombok.extern.slf4j.Slf4j;/*** @author mrcode*/@Slf4j@Servicepublic class PerformanceEventProcess {/*** 审批发起后,立马通知审批人审批** @param event*/@EventListener@Asyncpublic void initiateApprovalNotifyProcess(ApprovalCreateEvent event) {if (!event.isSame(ApprovalDataType.PERFORMANCE)) {return;}// 获取事件内容final Long id = event.getId();// 然后做自己的业务逻辑}}
@EventListener:标识的方法,方法中的入参就是 哪一个具体的事件对象,标识你对这个事件感兴趣@Async:异步执行,利用 spring scheduling 功能,一个全局的线程池,将你的事件消费逻辑包裹在一个线程中执行,如果没有这个注解的话,就相当于是同步执行。 ```javascript package cn.mrcode.event;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication // @EnableScheduling @EnableAsync // 想要异步生效,还需要加该注解开启异步功能 public class CrmBelApplication {
public static void main(String[] args) {SpringApplication.run(CrmBelApplication.class, args);}
}
<a name="AAkq7"></a>## 使用场景在任何需要解耦的地方都可以使用,只不过这个是 JVM 级别的解耦,不是系统级别的解耦。- 一些复杂的业务逻辑中- 一些依赖一个数据的创建,有好几个模块 都需要做一些关联操作的时候比如:创建一个账户1. 积分模块,发放新人积分2. 需要单独为这个账户 创建一张表之类的业务场景- 需要临时新增一个线程来执行逻辑的时候,使用异步,也达到了逻辑解耦<a name="n8sNS"></a>## 注意事项在一些场景中,会有一些问题,比如:<a name="mrE6e"></a>### AOP 代理事物未提交,就异步消费<br />比如上图的代码,在处理这个事件的时候,如果你用异步监听```java@EventListener@Asyncpublic void xxx(Event e){consumerDataMapper.selectById(e.getId())}
那么这个代码就可能会出现: changeStatus 方法上的 AOP 事务代理还未提交,xxx 的异步监听事件中就去获取这事务里面修改的数据,就会导致获取不到正确的数据信息。这个问题还不是每次都会出现,有一定的时间差,所以比较难以发现。
问题根本原因:AOP 代理事务在方法调用完成后,才会提交。
解决问题方案:
- 不使用异步:如果不使用 @Async,那么就等同于同步执行,这个时候还在事物内,就不会有问题
- 在消费时,先休眠几百毫秒:正常情况下几百毫秒足以提交事物了,不过还是有一定概率出现几百毫秒内事务提交不了的情况
- 使用手动提交事务的方式,也就是 Spring 中的编程事务(这里有一个 例子)
事务操作完成后,再发布事件,比如改成如下这样
@Overridepublic void changeStatus(Integer id, ConsumerDataStatus status, String message) {consumerDataServiceImpl.doChangeStatus(id, status, message);consumerDataEventPublisher.publishConsumerDataStatusChangeEvent(id, status);}@Transactional(propagation = Propagation.REQUIRED)public void doChangeStatus(Integer id, ConsumerDataStatus status, String message) {final ConsumerData record = new ConsumerData();record.setId(id);record.setStatus(status.getValue());if (message != null && message.length() > 1024) {message = message.substring(0, 1024);}record.setErrInfo(message);consumerDataMapper.updateByPrimaryKeySelective(record);}
此方法是推荐做法:调用处不用修改任何代码,消费处也不用做额外的处理。
