赞
踩
这一篇从应用角度来跟大伙讲讲 这个 spring 事件监听机制 ,顺便涉及到那些我认为大家应该一块了解的,我也会展开说说。
文章内容(包括不限于) :
1. 对比观察者模式
2. 应用场景的分析
3. 事件的创建 编码介绍
4. 事件如何 发出
5. 事件如何 被接收处理
6. 同步方式、异步方式 的整合
7. 使用注解方式和不使用注解
8. 多事件监听器执行顺序的设置
9. 源码浅析
10. 一些跑题杂谈
ps:上面列的这些,在本文章里面没有严格的顺序, 我控制不住自己的跑题。
我介绍码字的同时,就当作是跟大伙聊天一样,所以有些话可能比较小白文,不喜勿喷。
第一开头我就想吐槽,
其实文章的标题是有误的, 应该是 Spring的容器的事件发布监听机制。
但是为什么我要这样起标题呢,鉴于我最近一些日子去给一些学弟讲一些也是有关ssm的或者说是spring的一些知识, 他们好像有点抵触这些 他们认为的 ‘老知识’,反而我把PPT标题或者课题改一下,是springboot开头,他们会变得兴趣大一些。
我在想, 要是想让初学者也能玩明白这个事件发布监听机制 ,我认为很有必要先去普及一下 观察者模式,当然我也不会非常深入去介绍观察者模式(可能也没得有很深入的东西)
那么我就画一张图,先给大伙了解下 观察者模式(看起来乱,但是井井有序) :
大家可以结合这段小白文描述去 了解观察者模式 :
1. 观察者 和 被观察者 是 1 对 多 方式 。(小华被 好几个人 盯着)
2. 观察者 需要让 被观察者 知道 自己是参与观察他的人 。 (小华其实提供了登记列表途径, 小蓝、小黄、小橙 其实都有通过这个途径 留下了自己的联系号码 update方法)
3. 被观察者 小华,有台大哥大手机(notify方法 )专门打给 这些观察者 。 (小华的业务方法里面会 调用 这个notify方法)
4. 大哥大 从登记列表 里面 循环一个个地通知了 小蓝、小黄、小橙 , 他们接到通知,就开始调用自己的业务方法 进行处理。
看到这,再看一副更加简单的描述图,这一下子已经从深入到浅出了:
接下来,有了观察者设计模式的一定了解之后,我们直接再看一张浅出图 ,spring事件发布监听机制的图:
其实,大家一直都说的观察者设计 模式, 很多情况就是被叫做 发布-订阅设计模式。
但是你说它们是不是完全一致嘛, 不完全 ,但是实现的模式讲真是一致的。
一个主动变化的人 ,一个 等待通知跟着 做出变化的人 。
而对于 所谓的调度中心,只是把这两者的交流交互给 抽离出来罢了。
甚至很多框架里面对于这个调度中心的设计实现都是隐藏的,我们甚至没有感觉到有这个调度中心的存在 。
因为怎么去处理 这两者的一来一回的微妙依赖关系, 不同的人也许也是有不同的设计思路的。
(甚至我们理解玩观察者模式的流程细节,我们自己也可尝试地去 实现 我们自己的一个观察者模式/发布订阅模式,不基于java提供的观察者相关类 ,不基于spring提供的事件发布订阅 等,自己可以做个小挑战,当然大牛已经设计封装出来的东西,肯定是非常优秀的了,有没有必要自己再重造轮子,大家心里有自己的答案 )。
啰唆了很多, 我们一起从业务示例场景去更近一步 玩一下 spring的 事件发布监听机制 。
这是一个游戏公司,我们这个示例需求 是 有关于充值相关的 增强用户体验的 需求。
玩家给 游戏账号充值买礼包了 , 我们需要在充钱成功后,
通过短信 给玩家的手机发送一个通知, 给他 通知一下,让他知道钱花了。
产品 说 你快去设计一下做吧。
开发小明 找到 原先的 充值方法 ,在后面加了个发短信
写完,提测,上线 :
- public void recharge(){
-
- //充值氪金处理............
-
- //然后
- sendSms(String phone);
- }
不到2天, 玩家开始反馈,短信很多收不到,被手机拦截了,麻烦,但是又想知道充值的信息;
账号是绑定过邮箱的,能不能也发个邮件。
产品 : 加个发邮件 。
开发小明 又去改代码了,找到了 充值方法 里面在发短信后面,加了个发邮件 。
开发写完,提测,上线 :
- public void recharge(){
-
- //充值氪金处理............
-
- //然后
- sendSms(String phone);
- sendEmail(String email);
- }
第二天,产品又说,玩家充钱欲望不够, 我们给他们说了,买不同礼包,赠送的积分不同,越贵的,送的积分多多。 积分后面可以兑换 小礼品。
产品 : 加积分!
开发小明 又去改代码了,找到了 充值方法 里面在发邮件 后面 又加了 个 加积分。
- public void recharge(){
-
- //充值氪金处理............
-
- //然后
- sendSms(String phone);
- sendEmail(String email);
- addScore(Integer accountId);
- }
上面说的场景 是非常常见的, 需求出现一步步叠加是很正常的。
开发因为需求变化,频繁改代码,其实也是很正常的。
但是怎么改, 这一点, 是讲究的, 是根据我们设计的考虑而去决定 改动的方式的 。
上面这个业务场景例子,可以看到, 小明一直去改。
但是改在哪? 改的是充值的方法 。
充值变了吗? 其实没变 。 变的是充值后 的一些 业务。
所以本身 小明在写设计这个充值接口的时候,把充值后的一些额外业务都混在一块,
已经 有违 单一职责的原则了。
而频繁去改 充值接口 ,因为别的业务去改 充值接口方法 ,
那么 也有违 充值接口方法的 开闭原则。
而且,这还好全是小明自己一个人写的代码 ,如果说不单是小明一个人,充值接口是 小花写的。
你改自己的充值后的需求,天天改人家小花的充值接口实现代码。
小花都天天提心吊胆的, 生怕被你 改多了几行,每次上线发版,项目经理说,这次有关的版本上线有关人员需要留下来, 有 负责充值的小花, 负责 充值后提升用户体验的 小明。
你说,小花什么感受,直接就EMO了。
这时候,可能有很多看官已经想到了解决方案了 ,就是 让小明写一个 充值后调用的方法(里面囊括 发短信、发邮件、加积分等等甚至应对以后的扩展), 小花那边 充值之后 只需要调用一下 这个方法即可。
也就是:
- public void recharge(){
- //充值氪金处理............
-
- //然后
- afterRecharge();
- }
-
-
- public void afterRecharge(){
-
- sendSms(String phone);
- sendEmail(String email);
- addScore(Integer accountId);
- }
乍一看好像ok的, 但是其实还是未能完全解耦。
如果业务扩展起来了,
小明认为 发短信、发邮箱 属于调用消息中心的方法, 应该分出一个sendNotice方法;
然后加积分、加经验等游戏账号体系相关的, 应该 分出一个 xxx 方法 。
这两部分已经分开设计了,小花只需要分别调用就可以,没必要在外面套一层。因为这两个方法之间也不存在任何相关连。短信不发,积分也是要加的。
但是小花可能就不同意了,不止小花,可能调用 afterRecharge 方法的人,还有小曼,小路,小玲等等。 这么多人,很难一个个让他们改。
可能例子举的比较极端,放大了问题点。
其实这就是 解耦彻底的问题。 如果我们使用事件发布监听机制, 可以更优美地去应对这些场景。
事件发布监听机制 :
充值完 , 发布一个事件 。
小明自己去监听这个 充值完的事件, 写自己的 订阅者, 不管是写一个还是多个,可以自己拿捏。
单独发短信一个 监听器 A ;
单独发邮件一个 监听器 B;
单独 处理加积分等 一个监听器 C;
甚至把 AB 整合一个监听器, C单独一个 。
可以说是 开发自由,后面产品怎么提这些充值后的业务需求, 小花都不几乎不用参与。
就找小明就行, 小明也舒服 。
因为他 实现了 以增量的设计方式去 应对 这些变化多端的 需求 。
ok,事不宜迟,啰唆了这么多, 我们看看spring事件发布订阅的代码的简单实现:
(如果你是为了想找实现例子的,看到这估计也不容易,大多数应该早就退出了)
1. 创建充值事件,充值成功就发布这个事件 :
RechargeChangeEvent.java
里面包含充值的礼包活动id ;
包含用户游戏账号一些相关的信息(举例就随便写个User类了);
- /**
- * @Author JCccc
- * @Description
- * @Date 2020/10/12 9:08
- */
- public class RechargeChangeEvent extends ApplicationEvent {
-
- private Integer giftActivityId;
-
- private User user;
-
-
- public RechargeChangeEvent(Object source, Integer giftActivityId,User user) {
- super(source);
- this.giftActivityId = giftActivityId;
- this.user = user;
- }
-
- public Integer getGiftActivityId() {
- return giftActivityId;
- }
-
- public void setGiftActivityId(Integer giftActivityId) {
- this.giftActivityId = giftActivityId;
- }
-
- public User getUser() {
- return user;
- }
-
- public void setUser(User user) {
- this.user = user;
- }
- }
稍作讲解:
2. 发短信 监听器 :
SmsListener.java
- /**
- * @Author JCccc
- * @Description
- * @Date 2020/10/12 9:08
- */
- @Component
- public class SmsListener implements SmsService, ApplicationListener<RechargeChangeEvent> {
- @Override
- public void sendSms(String phone) {
- System.out.println("发送短信 成功");
- }
-
- @Override
- public void onApplicationEvent(RechargeChangeEvent event) {
- System.out.println("-------------------------------");
- Integer giftActivityId = event.getGiftActivityId();
-
- System.out.println("参与礼包活动id "+giftActivityId+" ,处理短信相关业务 , 获取短信签名等。。。。");
-
- String phone = event.getUser().getPhone();
- sendSms(phone);
-
- }
- }
稍作讲解:
ps: @Component 记得别忘记了,这实现的,继承的用的都是spring里面的。我们自己的自定义监听器那肯定也得丢里头了。 当然图里面的多实现不是一定要这么写的,如果你的监听业务非常特殊,完全可以单独封装一个方法写在对应监听器里面。
3. 发邮件 监听器
EmailListener.java
- @Component
- public class EmailListener implements EmailService, ApplicationListener<RechargeChangeEvent> {
- @Override
- public void sendEmail(String email) {
- System.out.println("发送邮件 成功");
- }
-
-
- @Override
- public void onApplicationEvent(RechargeChangeEvent event) {
- System.out.println("-------------------------------");
-
- try {
- sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- Integer giftActivityId = event.getGiftActivityId();
- System.out.println("参与礼包活动id "+giftActivityId+" ,处理邮件相关业务 , 进行 A BC等。。。。");
- String email = event.getUser().getEmail();
- sendEmail(email);
-
- }
- }
4. 扩展一些额外的监听器 ,例如加积分、加经验等等 。
上面两个都是采用的 代码实现方式,实现了 ApplicationListener ,那么我们也同时搞点不一样的,我们用注解 @EventListener 玩一下。
可以看到用注解,代码实现非常简洁, 明名上面也不需要强制重写 onApplicationEvent ,
直接自己明名,更加贴切合理,通过事件来给注解标识一下,监听器对应的哪个事件即可。
我支持注解! 从简!
- @Component
- public class XxxxxListener {
-
- @EventListener
- public void doXxxxx(RechargeChangeEvent event) {
- System.out.println("-------------------------------");
- Integer giftActivityId = event.getGiftActivityId();
- User user = event.getUser();
- System.out.println("处理Xxxxx业务");
- System.out.println("Xxxxx 成功");
- }
-
- }
5. 最后写个模拟接口实现方法,也就是充值方法,里面会发布一个 充值事件:
- @Service
- public class RechargeServiceImpl implements RechargeService {
- @Autowired
- private ApplicationContext applicationContext;
-
- @Override
- public void recharge(Integer giftActivityId, User user) {
- System.out.println("给用户"+user.getPhone() +"充值成功;使用的充值礼包系列id :"+giftActivityId);
- // 发布事件通知
- applicationContext.publishEvent(new RechargeChangeEvent(this,giftActivityId,user));
- }
- }
6.我们也模拟一个接口,调用充值方法 :
7. 使用postman调用一下,看看效果:
可以看到打印顺序,监听器里面的相关代码都成功执行 :
好了 ,spring的 事件发布监听机制的一些使用,到这里也是讲完了 。
但是还没完 !
大家注意到我的打印输出的截图没?
1. 发邮件那么久, 还真等这么久。
为什么先发邮件,我能先发短信吗? 我能先加积分么? 大家都是监听器,凭什么你先,我就想要固定哪个先,可以?
2 .再看看我如果在发邮件那里抛个异常:
可以看到,这spring默认的事件发布监听,是同步机制, 一个出错,后面跟着泡汤!
我们把原先自定义继承的监听器 ApplicationListener<RechargeChangeEvent> 换成 SmartApplicationListener :
具体代码 示例 , EmailListener.java :
- @Component
- public class EmailListener implements EmailService, SmartApplicationListener{
- @Override
- public void sendEmail(String email) {
- System.out.println("发送邮件 成功");
- }
- @Override
- public boolean supportsEventType(Class<? extends ApplicationEvent> eventType){
- return eventType == RechargeChangeEvent.class;
- }
-
- @Override
- public boolean supportsSourceType(Class<?> sourceType){
- return true;
- }
- @Override
- public int getOrder(){
- return -1;
- }
-
- @Override
- public void onApplicationEvent(ApplicationEvent event){
- RechargeChangeEvent rechargeChangeEvent= (RechargeChangeEvent) event;
- System.out.println("-------------------------------");
- try {
- sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- Integer giftActivityId = rechargeChangeEvent.getGiftActivityId();
- System.out.println("参与礼包活动id "+giftActivityId+" ,处理邮件相关业务 , 进行 A BC等。。。。");
- String email = rechargeChangeEvent.getUser().getEmail();
- sendEmail(email);
- System.out.println("监听到用户注册,给新用户发送首条站内短消息" + event.toString());
- }
-
- }
可以看到有一个 getOrder 方法 ,邮件监听器我们设置的是 -1 ;
同样 ,SmsListener.java 也改成使用 SmartApplicationListener :
- @Component
- public class SmsListener implements SmsService, SmartApplicationListener {
- @Override
- public void sendSms(String phone) {
- System.out.println("发送短信 成功");
- }
- @Override
- public boolean supportsEventType(Class<? extends ApplicationEvent> eventType){
- return eventType == RechargeChangeEvent.class;
- }
- @Override
- public boolean supportsSourceType(Class<?> sourceType){
- return true;
- }
-
- @Override
- public int getOrder(){
- return -2;
- }
-
- @Override
- public void onApplicationEvent(ApplicationEvent event){
- RechargeChangeEvent rechargeChangeEvent= (RechargeChangeEvent) event;
- System.out.println("-------------------------------");
- Integer giftActivityId = rechargeChangeEvent.getGiftActivityId();
- System.out.println("参与礼包活动id "+giftActivityId+" ,处理短信相关业务 , 获取短信签名等。。。。");
- String phone = rechargeChangeEvent.getUser().getPhone();
- sendSms(phone);
- }
-
- }
getOrder 方法 ,我们设置的是 -2 ;
这个order ,谁数值小就执行顺序排越前面。
ok,我们执行一下, 按照我们设置的顺序,发短信监听器是 -2 ,应该先执行 :
顺序的问题我们已经解决了,但是有没有觉得改动好大,完全换了一个监听器,然后还重写好几个方法,很麻烦 。
所以我们来用第二种方式解决 ,使用注解 @Order() 和 @EventListener :
注解的方式永远是极简的,强烈推荐!
看看极简使用注解是怎么玩的 :
可以看到,邮件监听设置的顺序 是 -10 , 短信是 -5 :
其实非常非常简单,基于注解方式 ,我们想要监听器都是多线程异步执行,我们只需要再叠加使用一个注解 @Async , 就像这样:
ps :对于异步注解使用还没了解过的,可以先看看这篇 ,因为使用上是由一些注意点的SpringBoot 最简单的使用异步线程案例 @Async_默默不代表沉默-CSDN博客
这样我们启动一下:
这时候会出现一个关于代理的错误:
Either pull the method up to an interface or switch to CGLIB proxies by enforcing proxy-target-class mode in your configuration.
这里又要扯远了,其实是因为我们使用注解 @Async 导致的。 其实在使用一些其他注解也会有遇到这种情况。
可以看到我们的 代码写的情况 :
EmailService 接口的实现类是 EmailListener ;
然后deal方法 加了 注解方法 EventListener和Async 注解的方法;
但是其实EmailService 接口里面是没这个方法的。
此时 EmailListener 被spring使用jdk代码方式生成的类,在收集 注解相关的方法时,是去类的接口中寻找deal方法,找不到,所以就报错了。
在此,分析问题先打住 ,有结论,解决思路有3小种 :
1.我们这个监听器就不要实现接口了。
2. 我们把这个被注解标记的方法 deal 也变成接口里面有的 。
3. 监听器这几个类的代理方式变成CGLIB ,这样代理的类里面是能找到方法的。
上面三种方式好像都可以 。
实践:
1.我们这个监听器就不要实现接口了:
可以看看每次初始化的时候,spring帮我们选择的代理方式(用到注解、aop的类):
(这种情况都已经不实现接口,也只能是CGLIB代理了)
这种情形,我们把项目运行起来,调一下模拟接口,可以看到异步调用成功:
所以这种方式也是成功可行的!
2.我们把这个被注解标记的方法 deal 也变成接口里面有的
这样就明目张胆的的符合规矩了,所以运行发现成功了, 我们的三个监听器都是异步的了:
特意在发短信的监听处理方法里面跑错,并不会影响其他监听器方法,异步线程机制执行:
所以这种方式也是成功可行的!
3. 保持实现接口的方式, 直接就按照错误提示,我就用 CGLIB代理方式去生成监听器的代理类,不就解决了么:
其实这个@Async也提供了这种手段,也就是开启 proxyTargetClass, 在被初始化收集时,生成的代理类会采取CGLIB方式。
其实就是因为我们想通过这种方式使用异步,然后@Async 使用时,用到了 @EnableAsync ,
而@EnableAsync 里面的代理方式 proxyTargetClass默认是false ,就是导致代理方式改成了JDK代理,然后就出现我们的报错。
所以我们要去解决,只需要设置为true。
指示是否创建基于子类(CGLIB)的代理,而不是到标准的基于Java接口的代理。
仅当{@link#mode}设置为{@link AdviceMode#PROXY}时适用。
默认值为 false。
注意,将此属性设置为 true 将影响所有 Spring管理的bean需要代理,而不仅仅是那些用@Async标记的bean。
例如,使用Spring的@Transactional注释标记的其他bean将同时升级到子类代理。这种方法没有任何好处在实践中产生负面影响,除非有人明确期望一种类型的代理。
运行项目,可以看到成功使用CGLIB代理方式:
同样,这样也是异步生效的,跑错的方法不会影响其他异步线程的执行:
本来还想继续讲一下 这个spring事件发布监听中,所谓的 调度中心在哪里呈现? 是怎么样实现监听机制的? 监听者是 pull模式 还是 push模式?
该篇篇幅太长了,这几个点需要结合源码一块分析,那就留到下一篇吧。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。