官方文档:标准和自定义事件,本章只介绍自定义事件,这里是 单机事件 官方文档笔记 该功能在 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
@Getter
public 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
*/
@Component
public class ApprovalEventPublisher implements ApplicationEventPublisherAware {
// 持有这个事件发布对象
private ApplicationEventPublisher publisher;
@Override
public 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
@Service
public class PerformanceEventProcess {
/**
* 审批发起后,立马通知审批人审批
*
* @param event
*/
@EventListener
@Async
public 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 代理事物未提交,就异步消费
![image.png](https://cdn.nlark.com/yuque/0/2022/png/651749/1649400925207-013d4693-e247-40fd-9c82-7de78bd62a37.png#clientId=uf123a28c-58dd-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=388&id=u551cac65&margin=%5Bobject%20Object%5D&name=image.png&originHeight=388&originWidth=1047&originalType=binary&ratio=1&rotation=0&showTitle=false&size=67860&status=done&style=none&taskId=ua8da776d-cef7-4aab-a36e-09095393e09&title=&width=1047)<br />比如上图的代码,在处理这个事件的时候,如果你用异步监听
```java
@EventListener
@Async
public void xxx(Event e){
consumerDataMapper.selectById(e.getId())
}
那么这个代码就可能会出现: changeStatus 方法上的 AOP 事务代理还未提交,xxx 的异步监听事件中就去获取这事务里面修改的数据,就会导致获取不到正确的数据信息。这个问题还不是每次都会出现,有一定的时间差,所以比较难以发现。
问题根本原因:AOP 代理事务在方法调用完成后,才会提交。
解决问题方案:
- 不使用异步:如果不使用 @Async,那么就等同于同步执行,这个时候还在事物内,就不会有问题
- 在消费时,先休眠几百毫秒:正常情况下几百毫秒足以提交事物了,不过还是有一定概率出现几百毫秒内事务提交不了的情况
- 使用手动提交事务的方式,也就是 Spring 中的编程事务(这里有一个 例子)
事务操作完成后,再发布事件,比如改成如下这样
@Override
public 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);
}
此方法是推荐做法:调用处不用修改任何代码,消费处也不用做额外的处理。