文章目录
- 1 注解@EventListener
- 1.1 示例Demo
- 1.1.1 简单例子
- 1.1.2 解耦
- 1.1.3 Spring事件
- 1.2 深入@EventListener
- 1.2.1 debug调试
- 1.2.2 问题一: Spring是怎么知道要去触发这个方法
- 1.2.3 问题二:ApplicationListenerMethodAdapter
- 1.2.4 问题三:SimpleApplicationEventMulticaster
- 1.2.5 问题四:调试问题
- 1.2.6 问题六:如何获取到自定义监听
- 1.2.7 问题七:如何获取自定义事件
- 1.3 进一步深究
- 1.3.1 引入
- 1.3.2 ApplicationListenerMethodAdapter
- 1.3.3 与SpringBoot结合
- 1.4 细节
- 1.4.1 单线程执行事件
- 1.4.2 线程池执行事件
- 1.4.3 @EventListener注解参数
1 注解@EventListener
点击了解 Spring中的事件讲解(Application Event)
1.1 示例Demo
1.1.1 简单例子
假设现在的需求是用户注册成功之后给他发个短信,通知他一下。
正常来说,伪代码很简单:
boolean success = userRegister(user);
if(success){sendMsg("...........test.............");
}
这代码能用,完全没有任何问题。但是,你仔细想,发短信通知这个动作按理来说,不应该和用户注册的行为“耦合”在一起,难道你短信发送的时候失败了,用户就不算注册成功吗?
上面的代码就是一个耦合性很强的代码。
1.1.2 解耦
应该是在用户注册成功之后,发布一个有用户注册成功了
的事件:
boolean success = userRegister(user);
if(success){publicRegisterSuccessEvent(user);
}
然后有地方去监听这个事件,在监听事件的地方触发短信发送
的动作。
这样的好处是后续假设不发短信了,要求发邮件,或者短信、邮件都要发送,诸如此类的需求变化,我们的用户注册流程的代码不需要进行任何变化,仅仅是在事件监听的地方搞事情就完事了。
这样就算是完成了两个动作的“解耦”。
1.1.3 Spring事件
我们可以基于 Spring
提供的 ApplicationListener
去做这个时间。
这次的 Demo 也非常的简单,我们首先需要一个对象来封装事件相关的信息,比如我这里用户注册成功,肯定要关心的是 userName:
@Data
public class RegisterSuccessEvent {private String userName;public RegisterSuccessEvent(String userName) {this.userName = userName;}
}
我这里只是为了做 Demo,对象很简单,实际使用过程中,你需要什么字段就放进去就行。
然后需要一个事件的监听逻辑:
@Slf4j
@Component
public class RegisterEventListener {@EventListenerpublic void handleNotifyEvent(RegisterSuccessEvent event) {log.info("监听到用户注册成功事件:" +"{},测试成功", event.getUserName());}}
接着,通过 Http 接口来进行事件发布:
@Resource
private ApplicationContext applicationContext;
@GetMapping("/publishEvent")
public void publishEvent() {applicationContext.publishEvent(new RegisterSuccessEvent("歪歪"));
}
1.2 深入@EventListener
1.2.1 debug调试
打断点首先选择打事件监听的这个地方:
然后直接就是一个发起调用,拿到调用栈再说:
通过观察调用栈发现,全是 Spring
的 event
包下的方法。
完全不知道应该怎么去看,所以我只有先看第一个涉及到 Spring
源码的地方,也就是这个反射调用的地方:
org.springframework.context.event.ApplicationListenerMethodAdapter#doInvoke
通过观察这三个关键的参数,我们可以断定此时确实是通过反射在调用我们 Demo
里面的 RegisterEventListener
类的 handleNotifyEvent
方法,入参是 RegisterSuccessEvent
对象,其 userName
字段的值是“歪歪”:
1.2.2 问题一: Spring是怎么知道要去触发这个方法
或者换个问法:handleNotifyEvent
这个自己写的方法名称怎么就出现在这里了呢?
然后顺着这个 method
找过去一看:
原来是当前类的一个字段,随便还看到了 beanName
,也是其一个字段,对应着 Demo
的 RegisterEventListener
1.2.3 问题二:ApplicationListenerMethodAdapter
既然关键字段都在当前类里面了,那么这个当前类,也就是 ApplicationListenerMethodAdapter
是什么时候冒出来的呢?
带着这个问题,继续往下查看调用栈,会看到这里的这个 listener
就是我们要找的这个“当前类”:
所以,我们的问题就变成了,这个 listener
是怎么来的?
1.2.4 问题三:SimpleApplicationEventMulticaster
然后你就会来到这个地方,把目光停在这个地方:
org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent
为什么会在这个地方停下来呢?
因为在这个方法里面,就是整个调用链中 listener
第一次出现的地方。
所以,第二个断点的位置,我们也找到了,就是这个地方:
org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent
1.2.5 问题四:调试问题
点击了解 idea 断点调试技巧
但是,当然把断点打在这个地方,重启服务准备调试的时候,你会发现重启的过程中就会停在断点处,而停下来的时候,你去调试会发现根本就不是你所关心的逻辑。
全是 Spring 启动过程中触发的一些框架的监听逻辑。比如应用启动事件,就会在断点处停下:
针对这种情况,有两个办法。
- 第一个是服务启动过程中,把断点停用,启动完成之后再次打开断点,然后触发调用。
idea 也提供了这样的功能,这个图标就是全局的断点启用和停用的图标:
这个方法在我们本次调试的过程中是行之有效的,但是假设如果以后你想要调试的代码,就是要在框架启动过程中调试的代码呢? - 使用条件断点。
通过观察入参,我们可以看到 event 对象里面有个 payload 字段,里面放的就是我们 Demo 中的 RegisterSuccessEvent 对象:
那么,我们可不可以打上断点,然后让 idea 识别到是上述情况的时候,即有 RegisterSuccessEvent
对象的时候,才在断点处停下来呢,当然是可以的,打条件断点就行。
在断点处右键,然后弹出框里面有个 Condition 输入框:
在这里,我们的条件是:event
对象里面的 payload
字段放的是我们 Demo
中的 RegisterSuccessEvent
对象时就停下来。
所以应该是这样的:event instanceof PayloadApplicationEvent && (((PayloadApplicationEvent) event).payload instanceof RegisterSuccessEvent)
1.2.6 问题六:如何获取到自定义监听
当我们观察 getApplicationListeners
方法的时候,会发现这个方法它主要是在对 retrieverCache
这个缓存在搞事情。
这个缓存里面放的就是在项目启动过程中已经触发过的框架自带的 listener
对象:
调用的时候,如果能从缓存中拿到对应的 listener
,则直接返回。而我们 Demo
中的自定义 listener
是第一次触发,所以肯定是没有的。
因此关键逻辑就在 retrieveApplicationListeners
方法里面:
org.springframework.context.event.AbstractApplicationEventMulticaster#retrieveApplicationListeners
这个方法里面的逻辑较多,只说一下这个关键的 for 循环:
这个 for 循环在干啥事呢?就是循环当前所有的 listener
,过滤出能处理当前这个事件的 listener
可以看到当前一共有 20 个 listener,最后一个 listener
就是我们自定义的 registerEventListener
:
每一个 listener
都经过一次 supportsEvent
方法判断:
supportsEvent(listener, eventType, sourceType)
这个方法,就是判断 listener
是否支持给定的事件:
因为我们知道当前的事件是我们发布的 RegisterSuccessEvent
对象。
对应到源码中,这里给定的事件,也就是 eventType
字段,对应的就是我们的 RegisterSuccessEvent
对象。
所以当循环到我们的 registerEventListener
的时候,在 supportsEventType
方法中,用 eventType
和 declaredEventTypes
做了一个对比,如果比上了,就说明当前的 listener
能处理这个 eventType
。
1.2.7 问题七:如何获取自定义事件
前面说了 eventType
是 RegisterSuccessEvent
对象。那么这个 declaredEventTypes
是个啥玩意呢?
declaredEventTypes
字段也在之前就出现过的 ApplicationListenerMethodAdapter
类里面。supportsEventType
方法也是这个类的方法:
而这个 declaredEventTypes
,就是 RegisterSuccessEvent
对象:
这不就呼应上了吗?
所以,这个 for 循环结束之后,里面一定是有 registerEventListener
的,因为它能处理当前的 RegisterSuccessEvent
这个事件。
但是你会发现循环结束之后 list 里面有两个元素,突然冒出来个 DelegatingApplicationListener
是什么?
这个时候怎么办?别去研究它,它不会影响我们的程序运行,所以可以先做个简单的记录,不要分心,要抓住主要线路。
经过前面的一顿分析,我们现在又可以回到这里了。
通过 debug 我们知道这个时候我们拿到的就是我们自定义的 listener 了:
从这个 listener 里面能拿到类名、方法名,从 event
中能拿到请求参数。
后续反射调用的过程,条件齐全,顺理成章的就完成了事件的发布。
1.3 进一步深究
1.3.1 引入
到这里,是不是认为已经调试的差不多了?已经知道了 Spring 自定义 listener 的大致工作原理了?
闭着眼睛想一想也就知道大概是一个什么流程了?
那么问一个问题:回想一下我最最开始定位到反射这个地方的时候是怎么说的?
是不是给了你这一张图,说 beanName、method、declaredEventTypes
啥的都在 ApplicationListenerMethodAdapter
这个类里面,这些属性是什么时候设置到这个类里面的呢?
1.3.2 ApplicationListenerMethodAdapter
现在我们看一下 ApplicationListenerMethodAdapter
这个类是咋来的。
就是想看看 beanName
是啥时候和这个类扯上关系的嘛,很简单,刚刚才提到的条件断点又可以用起来了:
重启之后,在启动的过程中就会在构造方法中停下,于是我们又有一个调用栈了:
可以看到,在这个构造方法里面,就是在构建我们要寻找的 beanName、method、declaredEventTypes
这类字段。
而之所以会触发这个构造方法,是因为 Spring
容器在启动的过程中调用了下面这个方法:
org.springframework.context.event.EventListenerMethodProcessor#afterSingletonsInstantiated
在这个方法里面,会去遍历 beanNames
,然后在 processBean
方法里面找到带有 @EventListener
注解的 bean
:
解释说明:
- 在标号为 ① 地方找到这个
bean
具体是哪些方法标注了@EventListener
- 在标号为 ② 的地方去触发
ApplicationListenerMethodAdapter
类的构造方法,此时就可以把beanName
,代理目标类,代理方法通过参数传递过去。 - 在标号为 ③ 的地方,将这个
listener
加入到 Spring 的上下文中,后续触发的时候直接从这里获取即可。
1.3.3 与SpringBoot结合
那么 afterSingletonsInstantiated
这个方法是什么时候触发的呢?还是看调用栈:
即使再不熟悉 SpringBoot
,至少也听说过容器启动过程中有一个 refresh
的动作吧?
就是这个地方:
这里,refreshContext
,就是整个 SpringBoot
框架启动过程的核心方法中的一步。
就是在这个方法里面中,在服务启动的过程中,ApplicationListenerMethodAdapter
这个类和一个 beanName
为 registerEventListener
的类扯上了关系,为后续的事件发布的动作,埋好了伏笔。
1.4 细节
1.4.1 单线程执行事件
前面了解了关于 Spring 的事件发布机制主干代码的流程之后,相信已经能从容器启动时
和请求发起时
这两个阶段进行了一个粗犷的说明了。
但是,里面其实还有很多细节需要注意的,比如事件发布是一个串行化的过程。假设某个事件监听逻辑处理时间很长,那么势必会导致其他的事件监听出现等待的情况。
比如有两个事件监听逻辑,在其中一个的处理逻辑中睡眠 3s,模拟业务处理时间。发起调用之后,从日志输出时间上可以看出来,确实是串行化,确实是出现了等待的情况:
针对这个问题,我们前面讲源码关于获取到 listener
之后,其实有这样的一个逻辑:
这不就是线程池异步的逻辑吗?只不过默认情况下是没有开启线程池的。
开始之后,日志就变成了这样:
1.4.2 线程池执行事件
@EventListener
注解默认是在发布事件的线程上同步执行监听器方法,即串行化执行
。如果想在事件监听器方法中使用线程池来实现并发执行,可以通过以下方式进行配置:
创建一个线程池 Bean:
@Configuration
public class ThreadPoolConfig {@Beanpublic Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10); // 设置核心线程数executor.setMaxPoolSize(20); // 设置最大线程数executor.setQueueCapacity(100); // 设置队列容量executor.setThreadNamePrefix("event-listener-"); // 设置线程名称前缀executor.initialize();return executor;}
}
在 @EventListener
注解中指定使用的线程池:
@EventListener()
@Async("taskExecutor") // 指定使用的线程池 Bean 名称
public void handleEvent(Event event) {// 处理事件逻辑,会在指定的线程池中并发执行
}
上述示例中,通过 @Async 注解指定了使用名为 taskExecutor 的线程池来执行监听器方法。
1.4.3 @EventListener注解参数
@EventListener
注解里面还有这两个参数,我们是没有使用到的:
@EventListener
注解有两个可选参数:classes
和 condition
。
classes 参数
:用于指定要监听的事件类型。可以指定一个或多个事件类型,以数组形式传递。例如:
@EventListener(classes = {EventA.class, EventB.class})
public void handleEvent(Event event) {// 处理事件逻辑
}
上述示例中,方法 handleEvent()
会监听 EventA 和 EventB 类型的事件。
condition 参数
:用于指定一个SpEL
表达式作为条件,只有当条件满足时,才会执行监听器方法。例如:
@EventListener(condition = "#event.source == 'source'")
public void handleEvent(Event event) {// 处理事件逻辑
}
上述示例中,方法 handleEvent()
只有当事件源为 source
时才会执行。
通过使用这两个参数,可以更加灵活地控制监听器的行为。可以根据具体需求选择要监听的事件类型,并根据条件来过滤需要处理的事件。