使用Spring事件监听机制实现业务解耦
- 一.业务场景
- 二.Spring事件监听机制是什么?
- 1.相关概念
- 2.解耦
- 三.使用步骤
- 1.定义事件
- 2.发布事件
- 3.侦听事件的对象
- 4.测试结果
- 5.有没有小伙伴们发现上面这种发布订阅有什么问题?
- 代码改造:
- 1. 自定义线程池:
- 2. 侦听器改造
- 3. ok,改造完毕
- 4.核心源码分析
- 四.观察者模式介绍
- 1.基本概念:
- 2.结合生活场景加深理解
- 五.总结及拓展
- 1.初始化操作、缓存填充
- 2.结合策略模式使用
一.业务场景
假如咱们项目里面有这样一个场景,处理业务逻辑完需要发送一个邮件。
我们平常做法可以通过线程池提交一个异步任务或者通过CompletableFuture开启一个异步任务调用发送邮件方法,这里简单介绍一个新的方式来了解与使用Spring的事件发布与监听,了解这种机制有诸多方便!
二.Spring事件监听机制是什么?
1.相关概念
事件监听机制也是设计模式中观察者模式、发布-订阅模式的一种实现。
Spring的事件机制是A调用一个发布事件的方法,就能自动调用监听这个事件的方法B,这个过程有3个对象:
- 事件本身,其实就是一个对象,这个对象对应的类要继承Spring的ApplicationEvent,这样才能被Spring识别。
- 发布事件的对象,一般都是使用ApplicationContext的publishEvent方法来发布;
- 接收(或者叫监听)事件的对象,实现ApplicationListener 接口或者使用@EventListener注解的方法;
2.解耦
通常我们利用消息队列来实现不同系统之间的解耦,如用户注册完成后,可以向消息队列发布一条消息,然后订阅了此topic的子系统(如邮件服务,积分服务)收到发布的消息之后,就会做相应的处理。这样做的好处是避免了在注册服务里耦合其他服务的代码,并且,执行子系统的业务将会异步执行,互不影响, 使用事件发布与监听机制也可以做业务解耦。
三.使用步骤
1.定义事件
定义邮件事件本身,只管邮件发送
public class MailEvent extends ApplicationEvent {public MailEvent(Object source) {super(source);}public void sendMail() {log.info("邮件发送中...");}
}
2.发布事件
模拟处理业务逻辑,然后通过我们的Spring容器发布这个邮件事件,其内部就是通过Spring的事件发布器发布的事件。
@Autowiredprivate ApplicationContext applicationContext;@GetMapping("/pub-event")public void pub() {log.info("业务逻辑处理中....");applicationContext.publishEvent(new MailEvent(this));}
3.侦听事件的对象
- 了解vue的小伙伴们应该知道watch侦听函数,侦听到对应的变量改变则触发侦听函数执行侦听器逻辑,一样的道理,这里是侦听器侦听到对应的事件则执行侦听器的逻辑。
- 通过实现ApplicationListener接口或者使用@EventListener注解来定义侦听器,这里定义了4个侦听器,一个使用实现ApplicationListener
接口的方式,另外3个使用@EventListener注解来定义侦听器的方式。 - 用@EventListener注解定义的侦听器可以使用 @Order来指定侦听器的执行顺序,Order的值越小,优先级越高
- 注意:实现ApplicationListener接口的侦听器不能使用@Order注解指定侦听器顺序。在Spring框架中,虽然@Order注解常用于指定Bean的优先级或排序,但在事件监听器的上下文中,@Order注解并不适用于ApplicationListener接口的实现类。这是因为ApplicationListener接口本身并不支持通过@Order注解来指定侦听器的顺序
@Component
@Slf4j
public class MailEventListener implements ApplicationListener<MailEvent> {// @Order(2) 不能这样使用,不生效的@Overridepublic void onApplicationEvent(MailEvent mailEvent) {log.info("侦听器2侦听到到邮件事件啦...");mailEvent.sendMail();}@Order(1)@EventListenerpublic void listener1(MailEvent mailEvent) {log.info("侦听器1侦听到邮件事件啦...");mailEvent.sendMail();}@Order(3)@EventListenerpublic void listener3(MailEvent mailEvent) {log.info("侦听器3侦听到邮件事件啦...");mailEvent.sendMail();}@Order(2)@EventListenerpublic void listener2(MailEvent mailEvent) {log.info("侦听器2侦听到邮件事件啦...当前执行线程:{}",Thread.currentThread().getName());mailEvent.sendMail();}
}
4.测试结果
5.有没有小伙伴们发现上面这种发布订阅有什么问题?
没错,咱们上面的发布订阅是同步机制,也就是前面一个事件出错了,后面的事件不会执行了,程序异常中断了。
正常咱们的发布订阅,例如消息队列这些,生产端发布了消息,消费端失败了关我生产者什么事?像我们举例的业务场景处理完业务再发邮件,发送邮件失败了肯定不能够影响主业务进行的。所以发邮件这种肯定得异步处理,ok,咱们引入侦听器端异步处理。
代码改造:
1. 自定义线程池:
关于核心线程数、最大线程数,任务队列(阻塞队列)选择,后续有时间可以出一期文章,设置tomcat最大线程数结合压测来设置最合适的核心线程数、最大线程数,这里先随便设置一下。
@Configuration
@EnableAsync
public class ThreadPoolConfiguration {@Bean("commonPool")public ExecutorService commonThreadPoolExecutor(){return new TulingMallThreadPoolExecutor("测试公共线程池",16,50).getLhrmsThreadPoolExecutor();}
}
2. 侦听器改造
使用异步,都使用异步的方式了,就让执行各个侦听器的线程竞争好了,@Order注解完全没意义了,无非是画蛇添足,在同步的情况下,限定侦听器执行顺序才有意义,个人理解~
我看百度AI说@Async 和 @Order注解一起使用,@Order注解还能生效,我测了其实是不行的,也没见人有这样一起使用的。
@Component
@Slf4j
public class MailEventListener {@Autowiredprivate ThreadPoolTaskExecutor commonPool;// commonPool是我自定义线程池命名的bean名称@Async("commonPool")@EventListenerpublic void listener1(MailEvent mailEvent) {log.info("侦听器1侦听到邮件事件啦...当前执行线程:{}",Thread.currentThread().getName());mailEvent.sendMail();throw new RuntimeException("侦听器1异常啦....");}@Async("commonPool")@EventListenerpublic void listener3(MailEvent mailEvent) {log.info("侦听器3侦听到邮件事件啦...当前执行线程:{}",Thread.currentThread().getName());mailEvent.sendMail();}@Async("commonPool")@EventListenerpublic void listener2(MailEvent mailEvent) {log.info("侦听器2侦听到邮件事件啦...当前执行线程:{}",Thread.currentThread().getName());mailEvent.sendMail();}
}
3. ok,改造完毕
再一次测试,可以看到线程名称都是自定义线程池里面的,然后咱们listener1抛出异常,不影响程序继续运行,其他侦听器照样发邮件,这时候咱们在业务层只需要考虑下消费失败场景如何处理等等,这种就是咱们业务上需要考虑的补偿方案了。
4.核心源码分析
EventListenerMethodProcessor类的processBean方法
侦听器集合是在AbstractApplicationContext中定义的:private final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();
其实不就是咱们学习设计模式时候的观察者模式嘛! 看了以上思路,咱们就算脱离Spring实现一套事件侦听机制是不是也有思路了呢?
四.观察者模式介绍
1.基本概念:
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。在这种模式中,一个目标对象(被观察者)管理所有依赖于它的观察者对象,并在其状态改变时主动通知这些观察者, 观察者模式又叫发布-订阅(Publish/Subscribe)模式。
2.结合生活场景加深理解
万事不离其宗,设计来源于生活,正如生活场景:老师有电话号码,学生需要知道老师的电话号码以便于在需要的时候拨打,在这样的组合中,老师就是一个被观察者(Subject),学生就是需要知道信息的观察者,老师需要管理所有的学生对象,当老师的电话号码发生改变时,学生得到通知,并更新相应的电话记录。 是不是跟咱们的Spring事件侦听机制如出一辙呢?
五.总结及拓展
使用Spring事件发布及事件侦听,可以很好的做业务解耦等等,还有很多好用的场景:
1.初始化操作、缓存填充
例如可以利用Spring的容器刷新事件(ContextRefreshedEvent)是在容器完全刷新后发布的。这个事件可以被侦听器侦听,以便执行一些操作,如数据库的初始化、缓存的填充等
2.结合策略模式使用
- 我们可以侦听这个事件,拿到里面的我们自定义注解bean,结合我们的策略再调用具体的实现等等,我在我当前公司项目里面也使用了Spring的事件侦听机制结合策略 +模板方法的设计模式来实现需求,对于拓展上诸多便利,同事都夸牛逼,只能说优雅~ 之后有时间我会出一篇文章供大家参考设计思路。
好啦,相信小伙伴们对Spring的事件侦听机制有简单了解了,晚安。