SpringBoot 3 Event 个人详细总结

基本使用

使用Spring Boot Event 使用步骤如下:

  1. 自定义一个事件源并继承 ApplicationEvent
  2. 创建事件监听器监听事件源
  3. 使用ApplicationEventPublisher推送事件

其中监听器两种实现方式:

  1. 实现 ApplicationListener<E extends ApplicationEvent> 接口
  2. 使用 @EventListener 及其派生注解

使用方式如下:

 

java

代码解读

复制代码

@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接口来改变它们在同一个线程的时候的执行顺序,如:

 

java

代码解读

复制代码

@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进行事件的发布推送。

可以看 非public方法、入参和返回值的源码解析

源码解析

查阅源码可使用的快捷键
  • ctrl+N:查找类
  • ctrl+F12: 在指定类中查找方法、属性等
  • ctrl+shift+F7: 查找文件是否存在指定字符串
  • alt+F7: 查找类在其他类出现的文件
前言

环境:

  • Spring Boot 3.3.2
  • JDK17

首先,我们可以了解以下类的相关关系:

描述
ApplicationEventPublisher封装装事件发布功能的接口
ApplicationEventMulticaster可以管理多个ApplicationListener对象并向其发布事件的对象实现的接口,一个典型的例子就是,ApplciationContext实现了ApplicationEventPublisher接口,并在其内部实现中,使用了ApplicationEventMulticaster作为实际发布事件的发布者。
SimpleApplicationEventMulticaster实现了ApplicationEventMulticaster接口,它是Spring Event 体系的实际发布者
ApplicationListener: Application Event listeners监听器接口,在Spring 6.1之后添加了supportsAsyncExecution方法,来控制该监听器是否支持异步执行,默认是true,支持异步执行。
PayloadApplicationEventApplicationEvent的子类,它对非ApplicationEvent的类进行了封装,使得可以在各个监听器中进行事件传播。
SmartInitializingSingleton在单例实例化阶段结束之后触发的接口,此接口可以由单例bean实现,以便在常规单例实例化算法之后执行一些初始化。
EventListenerMethodProcessorBeanFactoryPostProcessor后置处理器,将 EventListener方法注册为单独的ApplicationListener
EventListenerFactoryEventListener方法创建成ApplicationListener实例的策略工厂接口
ApplicationListenerMethodAdapterGenericApplicationListener适配器,将事件的处理委托给@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_NAMEapplicationEventMulticaster的Bean

  • 如果有,那么将其赋值到this.applicationEventMulticaster
  • 否则,则创建一个SimpleApplicationEventMulticaster将赋值,同时将它注册到容器中,Bean的名称为applicationEventMulticaster

从这里可以看出,想要覆盖默认创建的SimpleApplicationEventMulticaster一种方式是需要创建一个名称为applicationEventMulticaster并且实现了ApplicationEventMulticaster的Bean

类似以下代码:

 

java

代码解读

复制代码

@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注解

EventListenerMethodProcessorafterSingletonsInstantiated方法中执行了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接口的适配器类。

该类创建如下:

image.png

将method存储了下来,用于反射调用。

重写了onApplicationEvent方法,但是没有重写supportsAsyncExecution方法,这也意味着一旦SimpleApplicationEventMulticaster配置了线程池,那么@EventListener标注的方法都会是异步,这会导致有事务的listener事务失效

非public方法、入参和返回值的源码解析

image.png

由上可知,监听器的功能在processEvent方法中

image.png

image.png

image.png

依次通过上述三张图可以看出,适配器是会处理带有返回值的方法的,而handleResult方法则会执行到AbstractApplicationContext#publishEvent方法,将不是ApplicationEvent子类的结果参数包装成PayloadApplicationEvent类进行事件发布。

图二中的ReflectionUtils.makeAccessible(this.method)代码设置方法是可访问的,然后反射调用,那么无论是不是AppplicationEvent的子类都可以正常调用。

所以,我们使用以下代码进行测试

 

java

代码解读

复制代码

@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

image.png

可以通过以下代码看到效果

 

java

代码解读

复制代码

@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注入,根据线程的自动配置,我们可以这样配置

 

java

代码解读

复制代码

@Bean public ApplicationEventMulticaster applicationEventMulticaster(ThreadPoolTaskExecutor executor) { SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster(); multicaster.setTaskExecutor(executor); return multicaster; }

这样,事件会变成异步,那么就会导致异步的事件失效,当然,在Spring 6.1,对于通过实现了ApplicationListener接口的类,可以通过实现supportsAsyncExecution方法来阻止该监听器的异步, 如下:

 

java

代码解读

复制代码

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长什么样子

image.png

可以看出现在的Listener不是一个代理类,接下来有以下代码:

 

java

代码解读

复制代码

@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还是个原始类

image.png

image.png

image.png

可以看出只有一个实现了异步,那么为什么会这样子呢,我们继续进入代码getApplicationListeners方法,F7F8继续断点继续阅读源码。

image.png

retrieveApplicationListeners,也就是listener有两套机制,其中listenerBeans会查找容器中的Bean对象,EmailApplicationLister的代理对象也就是从这里获得的,所以在方法调用的时候,代理对象会解析@Async注解的内容。

也就是说,如果SimpleApplicationEventMulticaster配置了线程池,同时ApplicationListener实例的onApplicationEvent方法标注了@Async注解,那么就会是 异步的异步。

同时,在这个方法也找到了对Listener进行排序的代码AnnotationAwareOrderComparator.sort(allListeners);

image.png

上面的是ApplcationListener实例,是JDK动态代理,而@EventListener标注的方法则GBlib代理,依旧是从org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke方法断点得出

image.png

@Async中配置线程池

通过断点可以发现,在AnnotationAsyncExecutionInterceptor#getExecutorQualifier方法中会解析@Async中的方法以获取对应的线程池对象。

image.png

AsyncExecutionInterceptor#getDefaultExecutor会获取默认的线程对象,如果没有,则创建一个SimpleAsyncTaskExecutor对象

image.png

进入父类AsyncExecutionAspectSupport#getDefaultExecutor方法可以了解到,Executor的获取流程

image.png

通过上面的源码可以得出,如果配置了多个TaskExecutor的Bean,并且没有一个Bean的名称叫taskExeutor那么该方法就会返回null,由于返回了null,所以最终默认的 是SimpleAsyncTaskExecutor对象。

制造出这种情况,可以使用在代码中配置两个TaskExecutor的Bean,这会覆盖掉TaskExecutionAutoConfiguration中自动配置,从而没有taskExecutor该名称的TaskExecutorBean

 

java

代码解读

复制代码

@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`

image.png

AsyncExecutionAspectSupport#determineAsyncExecutor方法中可以看到相关逻辑,@Async注解上线程池的名称,如果不存在,会走其他方式获取一个线程池。

image.png

事务、异步、@TranscationEventListener

我们知道,Spring 的事务在某些条件下,是会失效的,其中一种情况是异步,如

 

java

代码解读

复制代码

@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"); }}

 

java

代码解读

复制代码

@Component @Slf4j public class UserPointListener { @EventListener @Async public void handleUserPoint(UserPointEvent event) { log.info("event execute"); // 可能是别的有关数据库操作的代码 throw new RuntimeException("报错"); }}

这里orderRepository#save保存是不会回滚的。如果我们注释@Async,那么方法就会回滚,此时我们注意一下日志,代码不会输出order end这条日志。

image.png

在学习@EventListener注解中,会发现它有一个派生注解@TranscationEventListener,它比@EventListener多出了两个值:ransactionPhase phase() default TransactionPhase.AFTER_COMMITboolean fallbackExecution() default false

image.png

首先解释一下fallbackExecution,它表示如果发布事件的方法没有事务是否依旧执行,默认是false不执行。即:OrderService#save方法上没有事务,那么该listener不执行功能

image.png

 

java

代码解读

复制代码

@Component @Slf4j public class UserPointListener { @TransactionalEventListener // @Async public void handleUserPoint(UserPointEvent event) { log.info("event execute"); throw new RuntimeException("报错"); }}

OrderService#save方法标记事务,看一下日志:

image.png

在有事务的情况下,会先OrderService#save中的内容,再回调handleUserPoint方法中内容。这与@EventListener有着这样的一个执行顺序区别。如果没有事务,而是fallbackExecution值为true,则与@EventListener执行顺序无区别。

但是@TranscationEventListener还有着一个phase属性。TransactionPhase枚举有四个值:BEFORE_COMMITAFTER_COMMITAFTER_ROLLBACK以及AFTER_COMPLETIONphase的默认值是AFTER_COMMIT

这里有什么意义呢,我们将UserPointListener代码改造成这样:

 

java

代码解读

复制代码

@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()); }}

image.png

我们在这个事件中加上了对数据库的查询,使用的JPA,会发生这样的问题: no transaction is in progress

image.png

这是因为phase默认值为AFTER_COMMIT,所以listener在是事务提交之后才执行的,就算同一个线程,后续的操作,这个线程也是没有了事务,所以会报错。同理,AFTER_ROLLBACK,AFTER_COMPLETION也是如此。

解决这个问题方法是在这个事务,在执行数据库操作的方法添加@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class),在没有事务的时候开启一个新的事务。

 

java

代码解读

复制代码

@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)

 

java

代码解读

复制代码

@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方法也不会回滚。

通过伪代码的描述大致如下:

 

scss

代码解读

复制代码

public void test(){ try{ // orderRepository.save save(); // 事务提交 commit(); } catch(Exception e){ rollback(); } // 开启一个新事务 try{ handleUserPoint(); // 事务提交 commit(); }catch(Exception e){ rollback(); } }

 

scss

代码解读

复制代码

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)

 

xml

代码解读

复制代码

<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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值