Spring Boot Event可以实现组件之间的解耦,这是一种典型的观察者设计模式。
基本使用
使用Spring Boot Event 使用步骤如下:
- 自定义一个事件源并继承
ApplicationEvent - 创建事件监听器监听事件源
- 使用
ApplicationEventPublisher推送事件
其中监听器两种实现方式:
- 实现
ApplicationListener<E extends ApplicationEvent>接口 - 使用
@EventListener及其派生注解
使用方式如下:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class EventApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(EventApplication.class)
.web(WebApplicationType.NONE)
.run(args);
context.addApplicationListener(new SmsApplicationLister());
MyService myService = context.getBean(MyService.class);
myService.doBusiness();
}
@Getter
static class MyEvent extends ApplicationEvent {
private final Integer point;
public MyEvent(Object source, Integer point) {
super(source);
this.point = point;
} }
@Slf4j
static class SmsApplicationLister implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
log.info("发送短信...");
} }
@Slf4j
@Component static class EmailApplicationLister implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
log.info("发送邮件...");
} }
@Component
@Slf4j static class PointApplicationLister {
@EventListener
public void point(MyEvent event) {
log.info("积分+{}", event.getPoint());
} }
@Component
@Slf4j static class MyService {
@Resource
private ApplicationEventPublisher publisher;
public void doBusiness() {
log.info("主线业务");
MyEvent myEvent = new MyEvent(this, 10);
publisher.publishEvent(myEvent);
}
}
}
注意上述代码中,SmsApplicationLister是通过addApplicationListener方法添加进去的,这与EmailApplicationLister使用@Compont注解方式添加到容器中有所不同。下面会有所解释
执行了之后的结果如图:
可以看出它们是以自然顺序排序的:可以在相关Listener上添加@Order注解或者实现Ordered接口来改变它们在同一个线程的时候的执行顺序,如:
@Component
@Slf4j
static class PointApplicationLister {
@EventListener
@Order(Ordered.HIGHEST_PRECEDENCE)
public void point(MyEvent event) {
log.info("积分+{}", event.getPoint());
}}
针对@EventListener标注的方法,我有以下的思考:
- 标注的方法一定要
public这样的公有方法才可以使用吗? - 标注的方法的入参一定要继承了
ApplicationEvent的参数吗?可以有多个参数吗 - 标注的方法可以有返回值吗,有返回值的话,那可以用了做什么?
那么这里的答案是:
- 标注的方法可以不是
public修饰的公有方法,甚至可以是private方法 - 标注的方法的入参可以
String等其他参数,但是只能有一个入参,多个参数会解析失败 - 标注的方法可以有返回值,该返回值如果不是
ApplicationEvent的子类,将会被包装成其子类PayloadApplicationEvent进行事件的传递,也就是说可以不用在PointApplicationLister类中注入ApplicationEventPublisher进行事件的发布推送。
源码解析
查阅源码可使用的快捷键
ctrl+N:查找类ctrl+F12: 在指定类中查找方法、属性等ctrl+shift+F7: 查找文件是否存在指定字符串alt+F7: 查找类在其他类出现的文件
前言
环境:
Spring Boot 3.3.2JDK17
首先,我们可以了解以下类的相关关系:
| 类 | 描述 |
|---|---|
ApplicationEventPublisher | 封装装事件发布功能的接口 |
ApplicationEventMulticaster | 可以管理多个ApplicationListener对象并向其发布事件的对象实现的接口,一个典型的例子就是,ApplciationContext实现了ApplicationEventPublisher接口,并在其内部实现中,使用了ApplicationEventMulticaster作为实际发布事件的发布者。 |
SimpleApplicationEventMulticaster | 实现了ApplicationEventMulticaster接口,它是Spring Event 体系的实际发布者 |
ApplicationListener | : Application Event listeners监听器接口,在Spring 6.1之后添加了supportsAsyncExecution方法,来控制该监听器是否支持异步执行,默认是true,支持异步执行。 |
PayloadApplicationEvent | 是ApplicationEvent的子类,它对非ApplicationEvent的类进行了封装,使得可以在各个监听器中进行事件传播。 |
SmartInitializingSingleton | 在单例实例化阶段结束之后触发的接口,此接口可以由单例bean实现,以便在常规单例实例化算法之后执行一些初始化。 |
EventListenerMethodProcessor | BeanFactoryPostProcessor后置处理器,将 EventListener方法注册为单独的ApplicationListener |
EventListenerFactory | 将EventListener方法创建成ApplicationListener实例的策略工厂接口 |
ApplicationListenerMethodAdapter | GenericApplicationListener适配器,将事件的处理委托给@EventListener注释的方法。同时可以处理其返回值的,将非ApplicationEvent子类的返回值包装成PayloadApplicationEvent,允许方法入参为非ApplicationEvent子类 |
@TransactionalEventListener | 根据TransactionPhase调用的EventListener,添加事务控制,如果发布事件的方法没有添加添加事务,默认不会执行Listener中代码 |
Event的执行顺序如下:
在AbstractApplicationContext#refresh方法中有以下代码:
- 首先先初始化
ApplicationEventMulticaster - 接着将实现了
ApplicationListener接口的Bean注册到ApplicationEventMulticaster中,并发布早期的事件。 - 然后在
finishBeanFactoryInitialization方法中的beanFactory.preInstantiateSingletons()代码执行afterSingletonsInstantiated方法,而EventListenerMethodProcessor实现了EventListenerMethodProcessor接口,所以在这里进行@TransactionalEventListener和@EventListener注解的解析
ApplicationEventMulticaster的初始化
在org.springframework.context.support.AbstractApplicationContext#initApplicationEventMulticaster中方法的代码如图:
在该方法中,可以看出源码首先会从beanFactory容器中查找是否存在一个名称APPLICATION_EVENT_MULTICASTER_BEAN_NAME即applicationEventMulticaster的Bean
- 如果有,那么将其赋值到
this.applicationEventMulticaster中 - 否则,则创建一个
SimpleApplicationEventMulticaster将赋值,同时将它注册到容器中,Bean的名称为applicationEventMulticaster
从这里可以看出,想要覆盖默认创建的SimpleApplicationEventMulticaster一种方式是需要创建一个名称为applicationEventMulticaster并且实现了ApplicationEventMulticaster的Bean
类似以下代码:
@Bean
public ApplicationEventMulticaster applicationEventMulticaster(@Qualifier("taskExecutor") Executor executor) {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.setTaskExecutor(executor);
return multicaster;
}
SimpleApplicationEventMulticaster实现类发布事件
SimpleApplicationEventMulticaster是Spring事件发布默认的一个实现类。发送事件部分源码如下:
这里getApplcationListeners方法会找到对应的listener列表进行循环,每一个listener都会判断是否存在线程池,同时调用其supportsAsyncExecution判断该listener是否支持异步执行。
在Spring 6.1之前是没有supportsAsyncExecution,所以只要有executor,那么所有的listener都会异步执行。而这个executor来源是SimpleApplicationEventMulticaster一个taskExecutor属性,在上述的ApplicationEventMulticaster的初始化中可以看出,Spring默认没有给SimpleApplicationEventMulticaster添加Executor,如果在这里给予线程池,那么会导致异步Listener关于事务功能失效
关于异步的相关功能在 异步事件 与 @Async 中介绍
EventListenerMethodProcessor解析@EventListener注解
EventListenerMethodProcessor的afterSingletonsInstantiated方法中执行了processBean方法,部分源码如下:
这里找到了有@EventListener标注的方法进行轮询,找到对应支持该方法的工厂类,执行factory.createApplicationListener代码创建对应的listener并添加到当前的SimpleApplicationEventMulticaster对象中。然后退出当前循环。
这里的有一点EventListenerFactory的顺序问题如何确定,可以看EventListenerMethodProcessor#postProcessBeanFactory中的AnnotationAwareOrderComparator.sort(factories)进行了排序
相关工厂的实现类都实现了Ordered接口,其中DefaultEventListenerFactory的顺序最小,排到最后,该工厂类,支持所有方法。而TransactionalEventListenerFactory则支持标注了@TransactionalEventListener的方法。
也就是说:如果factoriesz只有DefaultEventListenerFactory工厂时,@TransactionalEventListener标注的方法也会跟@EventListener的方法走相同的逻辑,就是普通的监听器方法。
接下来看DefaultEventListenerFactory的createApplicationListener方法,该方法创建了一个实现了ApplicationListener接口的类:ApplicationListenerMethodAdapter,通过名字可以判断这是将标注了@EventListener方法转成ApplicationListener接口的适配器类。
该类创建如下:
将method存储了下来,用于反射调用。
重写了onApplicationEvent方法,但是没有重写supportsAsyncExecution方法,这也意味着一旦SimpleApplicationEventMulticaster配置了线程池,那么@EventListener标注的方法都会是异步,这会导致有事务的listener事务失效
非public方法、入参和返回值的源码解析
由上可知,监听器的功能在processEvent方法中
依次通过上述三张图可以看出,适配器是会处理带有返回值的方法的,而handleResult方法则会执行到AbstractApplicationContext#publishEvent方法,将不是ApplicationEvent子类的结果参数包装成PayloadApplicationEvent类进行事件发布。
图二中的ReflectionUtils.makeAccessible(this.method)代码设置方法是可访问的,然后反射调用,那么无论是不是AppplicationEvent的子类都可以正常调用。
所以,我们使用以下代码进行测试
@Component
@Slf4j
public class NonApplicationEventListener {
@EventListener
private String test(MyEvent myEvent) {
log.info("测试private,并返回字符串: {}",myEvent);
return "发送字符串";
}
@EventListener
public void handleString(String string) {
log.info(string);
}
}
异步事件 与 @Async
在Spring Boot中,有个TaskExecutionAutoConfiguration类,当不存在ThreadPoolTaskExecutor类时候自动配置默认ThreadPoolTaskExecutorBean,名称为applicationTaskExecutor别名为taskExecutor。
可以通过以下代码看到效果
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class NewEventApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(NewEventApplication.class)
.web(WebApplicationType.NONE)
.run(args);
System.out.println(Arrays.toString(context.getAliases("applicationTaskExecutor")));
System.out.println(context.getBean("applicationTaskExecutor").getClass());
}
}
由于Spring默认是不带线程池的,可以通过自定义Bean注入,根据线程的自动配置,我们可以这样配置
@Bean
public ApplicationEventMulticaster applicationEventMulticaster(ThreadPoolTaskExecutor executor) {
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
multicaster.setTaskExecutor(executor);
return multicaster;
}
这样,事件会变成异步,那么就会导致异步的事件失效,当然,在Spring 6.1,对于通过实现了ApplicationListener接口的类,可以通过实现supportsAsyncExecution方法来阻止该监听器的异步, 如下:
static class SmsApplicationLister implements ApplicationListener<MyEvent> {
@Override
public void onApplicationEvent(MyEvent event) {
log.info("发送短信...");
}
@Override
public boolean supportsAsyncExecution() {
return false;
}}
然而,对于@EventListener标注的方法,由于ApplicationListenerMethodAdapter类没有重写supportsAsyncExecution,所以一旦配置线程池,该类事件事务都会失效,可以猜测之后@EventListener可能也会添加supportsAsyncExecution属性,用于配置是否异步。
@Async
在使用@EnableAsync之前,我们可以通过断点,再看一下之前源码,我们的Listener长什么样子
可以看出现在的Listener不是一个代理类,接下来有以下代码:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableAsync
public class NewEventApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(NewEventApplication.class)
.web(WebApplicationType.NONE)
.run(args);
context.addApplicationListener(new SmsApplicationLister());
MyService myService = context.getBean(MyService.class);
myService.doBusiness();
}
@Getter
static class MyEvent extends ApplicationEvent {
private final Integer point;
public MyEvent(Object source, Integer point) {
super(source);
this.point = point;
} }
@Slf4j
@Order(1)
static class SmsApplicationLister implements ApplicationListener<MyEvent> {
@Override
@Async
public void onApplicationEvent(MyEvent event) {
log.info("发送短信...");
}
@Override
public boolean supportsAsyncExecution() {
return false;
}
}
@Slf4j
@Component static class EmailApplicationLister implements ApplicationListener<MyEvent> {
@Override
@Async
public void onApplicationEvent(MyEvent event) {
log.info("发送邮件...");
}
@Override
public boolean supportsAsyncExecution() {
return false;
}
}
@Component
@Slf4j
static class MyService {
@Resource
private ApplicationEventPublisher publisher;
public void doBusiness() {
log.info("主线业务");
MyEvent myEvent = new MyEvent(this, 10);
publisher.publishEvent(myEvent);
}
}
}
注意:SmsApplicationLister是通过addApplicationListener方式添加进去的,而EmailApplicationLister类标注了@Component,通过断点可以发现EmailApplicationLister变成了由JdkDynamicAopProxy增强的代理,而SmsListener还是个原始类
可以看出只有一个实现了异步,那么为什么会这样子呢,我们继续进入代码getApplicationListeners方法,F7和F8继续断点继续阅读源码。
在retrieveApplicationListeners,也就是listener有两套机制,其中listenerBeans会查找容器中的Bean对象,EmailApplicationLister的代理对象也就是从这里获得的,所以在方法调用的时候,代理对象会解析@Async注解的内容。
也就是说,如果SimpleApplicationEventMulticaster配置了线程池,同时ApplicationListener实例的onApplicationEvent方法标注了@Async注解,那么就会是 异步的异步。
同时,在这个方法也找到了对Listener进行排序的代码AnnotationAwareOrderComparator.sort(allListeners);
上面的是ApplcationListener实例,是JDK动态代理,而@EventListener标注的方法则GBlib代理,依旧是从org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke方法断点得出
@Async中配置线程池
通过断点可以发现,在AnnotationAsyncExecutionInterceptor#getExecutorQualifier方法中会解析@Async中的方法以获取对应的线程池对象。
在AsyncExecutionInterceptor#getDefaultExecutor会获取默认的线程对象,如果没有,则创建一个SimpleAsyncTaskExecutor对象
进入父类AsyncExecutionAspectSupport#getDefaultExecutor方法可以了解到,Executor的获取流程
通过上面的源码可以得出,如果配置了多个TaskExecutor的Bean,并且没有一个Bean的名称叫taskExeutor那么该方法就会返回null,由于返回了null,所以最终默认的
是SimpleAsyncTaskExecutor对象。
制造出这种情况,可以使用在代码中配置两个TaskExecutor的Bean,这会覆盖掉TaskExecutionAutoConfiguration中自动配置,从而没有taskExecutor该名称的TaskExecutorBean
@Bean
public ThreadPoolTaskExecutor silver() {
return new ThreadPoolTaskExecutorBuilder()
.corePoolSize(3)
.maxPoolSize(10)
.queueCapacity(100)
.threadNamePrefix("silver")
.build();
}
@Bean
public TaskExecutor gravel() {
return new ThreadPoolTaskExecutorBuilder()
.corePoolSize(3)
.maxPoolSize(10)
.queueCapacity(100)
.threadNamePrefix("gravel")
.build();
}
这三个类的关系如下:AsyncExecutionAspectSupport -> AsyncExecutionInterceptor-> ``AnnotationAsyncExecutionInterceptor`
在AsyncExecutionAspectSupport#determineAsyncExecutor方法中可以看到相关逻辑,@Async注解上线程池的名称,如果不存在,会走其他方式获取一个线程池。
事务、异步、@TranscationEventListener
我们知道,Spring 的事务在某些条件下,是会失效的,其中一种情况是异步,如
@Slf4j
public class OrderService {
@Resource
private OrderRepository orderRepository;
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Transactional(rollbackFor = Exception.class)
public void save(OrderPO orderPO) {
orderRepository.save(orderPO);
UserPointEvent event = new UserPointEvent(this, orderPO.getUserId(), orderPO.getId());
applicationEventPublisher.publishEvent(event);
log.info("order end");
}}
@Component
@Slf4j
public class UserPointListener {
@EventListener
@Async
public void handleUserPoint(UserPointEvent event) {
log.info("event execute");
// 可能是别的有关数据库操作的代码
throw new RuntimeException("报错");
}}
这里orderRepository#save保存是不会回滚的。如果我们注释@Async,那么方法就会回滚,此时我们注意一下日志,代码不会输出order end这条日志。
在学习@EventListener注解中,会发现它有一个派生注解@TranscationEventListener,它比@EventListener多出了两个值:ransactionPhase phase() default TransactionPhase.AFTER_COMMIT和boolean fallbackExecution() default false
首先解释一下fallbackExecution,它表示如果发布事件的方法没有事务是否依旧执行,默认是false不执行。即:OrderService#save方法上没有事务,那么该listener不执行功能
@Component
@Slf4j
public class UserPointListener {
@TransactionalEventListener
// @Async
public void handleUserPoint(UserPointEvent event) {
log.info("event execute");
throw new RuntimeException("报错");
}}
将OrderService#save方法标记事务,看一下日志:
在有事务的情况下,会先OrderService#save中的内容,再回调handleUserPoint方法中内容。这与@EventListener有着这样的一个执行顺序区别。如果没有事务,而是fallbackExecution值为true,则与@EventListener执行顺序无区别。
但是@TranscationEventListener还有着一个phase属性。TransactionPhase枚举有四个值:BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK以及AFTER_COMPLETION。phase的默认值是AFTER_COMMIT。
这里有什么意义呢,我们将UserPointListener代码改造成这样:
@Component
@Slf4j
public class UserPointListener {
@Resource
private UserPointService userPointService;
private final ThreadLocalRandom random = ThreadLocalRandom.current();
@TransactionalEventListener
// @Async
public void handleUserPoint(UserPointEvent event) {
log.info("event execute");
UserPointPO userPointPO = new UserPointPO();
userPointPO.setPoint(random.nextInt(800,16767));
userPointPO.setId(1);
userPointPO.setUserId(1);
userPointService.save(userPointPO);
userPointService.query(event.getUserId());
}}
我们在这个事件中加上了对数据库的查询,使用的JPA,会发生这样的问题: no transaction is in progress
这是因为phase默认值为AFTER_COMMIT,所以listener在是事务提交之后才执行的,就算同一个线程,后续的操作,这个线程也是没有了事务,所以会报错。同理,AFTER_ROLLBACK,AFTER_COMPLETION也是如此。
解决这个问题方法是在这个事务,在执行数据库操作的方法添加@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class),在没有事务的时候开启一个新的事务。
@TransactionalEventListener
@Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = Exception.class)
@Async
public void handleUserPoint(UserPointEvent event) {
log.info("event execute");
UserPointPO userPointPO = new UserPointPO();
userPointPO.setPoint(random.nextInt(800,16767));
userPointPO.setId(1);
userPointPO.setUserId(1);
userPointService.save(userPointPO);
userPointService.query(event.getUserId());
}
也可以是@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
// @Async
public void handleUserPoint(UserPointEvent event) {
log.info("event execute");
UserPointPO userPointPO = new UserPointPO();
userPointPO.setPoint(random.nextInt(800,16767));
userPointPO.setId(1);
userPointPO.setUserId(1);
userPointService.save(userPointPO);
userPointService.query(event.getUserId());
}
注意:第二段代码是和发布事件的方法处于同一个事务中,后续报错,orderRepository.save方法会回滚,但是如果加上@Async,那么事务就会失效,也就不会回滚。
第一段代码后续的事件是开启一个新事务了,与前面的事务没有关系了,所以使用不使用@Async都一样,后续代码报错,orderRepository.save方法也不会回滚。
通过伪代码的描述大致如下:
public void test(){
try{
// orderRepository.save
save();
// 事务提交
commit();
}
catch(Exception e){
rollback();
}
// 开启一个新事务
try{
handleUserPoint();
// 事务提交
commit();
}catch(Exception e){
rollback();
}
}
public void test(){
try{
// orderRepository.save
save();
// @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
handleUserPoint();
// 事务提交
commit();
}
catch(Exception e){
rollback();
}
}
代码
关于@TransactionalEventListener的部分源码
java-skill-learn/event-spring-boot-study at main · DawnSilverGravel/java-skill-learn (github.com)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
参考链接
167-第卌八讲-事件监听器1_哔哩哔哩_bilibili 通过这里的视频,再看我这里的总结,可能更容易理解一些,在该老师的讲解下,进行了拓展和延伸。