SpringEvent优雅解耦时连续两个bug的解决方案
程序员拾山 人气:01,基本原理
日常开发中,我们有时会使用SpringEvent对业务解耦,使我们的代码更加高内聚低耦合,不过如果对其运行原理不清楚,那么在使用的过程中,一不留神就会出现一些bug。
今天我们回顾一下SpringEvent使用的基本原理,需要优化的点,以及非常常见的两种错误。
Spring的事件模式其实很简单,我们创建一个Event事件,当Event发生时,广播器对事件进行发布,然后对应的Listener进行处理即可。
Spring的事件一共有三个组件:
1,Event:用于定于我们的事件,比如ApplicationEvent或者通过继承ApplicationEvent定义我们自己的事件。
2,广播器Multicaster:当事件发生时,将事件广播出去。
3,监听器Listener:监听和处理广播器广播的事件。
2,基本用法
第一步,首先定义一个Event事件,
@Getter @Setter public class MessageEvent extends ApplicationEvent { private String content; public MessageEvent(String content) { super(new Object()); this.content = content; } }
第二步,定义一个Listener对事件进行监听,
@Component public class MessageListener { @EventListener public void listen(MessageEvent messageEvent) { System.out.println("收到消息:" + messageEvent.getContent()); } }
最后在我们的业务逻辑需要的地方,就可以发布事件了。
@RestController @RequestMapping(value = "/demo") public class DemoController { @Resource private ApplicationContext applicationContext; @PostMapping(value = "/send") public ResponseEntity sendMessage() { //..... //处理一些业务逻辑之后,发送通知消息 MessageEvent messageEvent = new MessageEvent("发布一条测试消息"); this.applicationContext.publishEvent(messageEvent); return ResponseEntity.ok().build(); } }
3,需要注意的点
一,对于同一个Event,我们可以定义多个Listener,多个Listener之间可以通过@Order来指定顺序,order的Value值越小,执行的优先级就越高。
二,我们可以使用@EventListener轻松标记一个方法作为监听器,但是默认情况下,它是同步执行的,所以如果发布事件的方法处于事务中,那么事务会在监听器方法执行完毕之后才提交。
有些情况下,我们希望事件发布之后就由监听器去处理,而不要影响原有的事务,也就是说希望事务及时提交。
这时我们可以使用@TransactionalEventListener来定义一个监听器。
@Component public class MessageListener { //上层事务执行完毕之后再执行 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void listen(MessageEvent messageEvent) { System.out.println(Thread.currentThread().getName()); System.out.println("收到消息:" + messageEvent.getContent()); } }
三,默认情况下,@EventListener定义的方法是同步执行的,如果我们想通过异步的方式执行一个监听器的方法,可以在方法上加上@Async注解(记得在启动类上加上@EnableAsync开启异步执行配置)。
需要注意的是,使用@Async时,必须为其配置线程池,否则用的还是默认的线程。
如@Async(value = "taskExecutor"),此时Listener就会被分配到taskExecutor的线程池中执行。
使用@Async异步执行的同时,还会带来另外两个问题,需要大家注意:
1,如果Listener执行过程中抛出了异常,由于是异步执行,异常并不会被事件发布方捕获。
2,异步执行时,方法的返回值不能用来发布后续事件,如果需要处理结果去发布另一个事件,需要我们手动去发布。
4,常见错误一:错误的监听一个并不会抛出的事件
有时我们希望监听Spring的启动事件,做一些初始化操作。于是有的同学可能定义了这样一个Listener:
@Component public class MessageListener { @EventListener public void listen2(ContextStartedEvent event) { System.out.println("Spring启动了," + event.toString()); } }
不过,虽然名字看起来似乎是一个上下文启动时的事件,但是Spring启动时并不会发布这个事件,我们启动项目看下控制台是否会打印日志:
可以看到,Spring项目启动后,并没有打印任何日志。
其实Spring项目启动后发布的真正Event是ContextRefreshedEvent,我们修改下代码再看一下结果:
这时,控制台打印出了我们想要的日志。
在创建监听事件时,一定要确保监听的Event是正确的,否则就会监听不到对应的事件。
5,常见错误二:由于异常导致的事件传播丢失
即使我们保证事件会被监听器真正的捕获,但是某些情况下,事件会因为异常导致传播丢失。
如上图所示,我们定义了两个Listener,原本期望按照order定义的顺序,将消息传播依次传播。然而因为一些原因,Listener1中的方法抛出了异常,导致Listener2无法接收到消息了。
这是因为:默认情况下处理器的执行是顺序执行的,在执行过程中,如果一个监听器执行抛出了异常,则后续监听器就得不到被执行的机会了。
我们可以通过SimpleApplicationEventMulticaster中的multicastEvent方法看一下事件是如何传播的,
通过上图可以看出,如果在没有定义线程池的情况下,在invokeListener方法中会调用doInvokeListener方法去执行真正的逻辑,在doInvokeListener方法中,如果抛出了异常,会导致后的Listener失效。
针对异常这种情况又该如何处理我们的代码呢?
有三种方法:
1,使用try catch捕获异常,只要Listener方法不抛出异常,自然每个Listener都可以收到广播的消息。
2,使用@Async异步执行,通过上面的源码可以看到,如果定义的线程池,那么每一个Listener都会在一个线程中执行,每个线程之后是相互独立的,自然不会影响别人。
3,通过ErrorHandler处理掉异常,保证后面的Listener不受影响。
总结
本文主要讲解了SpringEvent基本的使用方法,和平常开发中可能会遇到的一些问题。总的来说,Spring为了让大家用的更轻松,考虑了各种可能发生的情况,但是如果大家不了解背后的实现原理,就可能发生一些本不该出现的bug。
加载全部内容