前言
事件监听机制其原理就是观察者模式,而观察者模式又被称为发布-订阅模式。
观察者模式将有依赖关系的对象抽象为了观察者和主题两个不同的角色,多个观察者同时观察一个主题,两者只通过抽象接口保持松耦合状态,这样双方可以相对独立的进行扩展和变化:比如可以很方便的增删观察者,修改观察者中的更新逻辑而不用修改主题中的代码。但是这种解耦进行的并不彻底,这具体体现在以下几个方面:
• ① 抽象主题需要依赖抽象观察者,而这种依赖关系完全可以去除。
• ② 主题需要维护观察者列表,并对外提供动态增删观察者的接口。
• ③ 主题状态改变时需要由自己去通知观察者进行更新。
耦合这个词在平常的开发工作中应该不陌生,简单理解就是代码中各部分关联度过高。举一个大家都遇见过的经典耦合场景:用户注册成功之后需要进行发送短信通知或是邮件通知,用户注册逻辑与发送短信或是邮件通知逻辑放在一块就是一种耦合现象,如果短信或是邮件功能异常,整个用户注册功能就会异常,会带来不好的用户体验,另外的缺点是维护复杂,不便于拓展。将业务逻辑与功能性逻辑进行拆分开就是属于解耦。spring中IOC思想就是解耦的一种体现,毕竟在一个实现类中如果调用另一个实现类不用繁琐的创建对象,只需要把需要用到实现类进行注入就可以了,减少代码量的同时也便于后期的拓展与维护。
可以把主题(Subject)替换成事件(event),把对特定主题进行观察的观察者(Observer)替换成对特定事件进行监听的监听器(EventListener),而把原有主题中负责维护主题与观察者映射关系以及在自身状态改变时通知观察者的职责从中抽出,放入一个新的角色事件发布器(EventPublisher)中,事件监听模式的轮廓就展现在了我们眼前,如下图所示:
常见事件监听机制的主要角色如下:
• 事件及事件源
:对应于观察者模式中的主题。事件源发生某事件是特定事件监听器被触发的原因。
• 事件监听器
:对应于观察者模式中的观察者。监听器监听特定事件,并在内部定义了事件发生后的响应逻辑。
• 事件发布器
:事件监听器的容器,对外提供发布事件和增删事件监听器的接口,维护事件和事件监听器
事件监听机制的应用场景
在程序设计里有哪些场景会用到呢?下面举一些例子: 用户注册或者登陆,或者用户下单成功都需要发个短信或者邮件提示用户,简单抽象一下,用户的很多业务行为都会有要发短信的需求,如果每个业务行为都各自发短信,那么首先业务行为与发短信就耦合了,不利于后续扩展,其次具有相同特征的行为一直重复。使用事件监听机制改造一下,把各个业务行为内的短信内容封装成一个事件,业务行为触发的时候发布这个事件,再把具体发短信的行为封装到监听器里,监听到事件源发布的事件后,触发具体的发短信的行为。
事件监听机制的好处
业务与业务之间解耦,符合依赖倒置原则;
代码复用,更容易维护,也提高了执行效率;
Spring容器对事件监听机制的支持
Spring容器,具体而言是ApplicationContext接口定义的容器提供了一套相对完善的事件发布和监听框架,其遵循了JDK中的事件监听标准,并使用容器来管理相关组件,使得用户不用关心事件发布和监听的具体细节,降低了开发难度也简化了开发流程。下面看看对于事件监听机制中的各主要角色,Spring框架中是如何定义的,以及相关的类体系结构。
事件
Spring为容器内事件定义了一个抽象类ApplicationEvent
,该类继承了JDK中的事件基类EventObject。因而自定义容器内事件除了需要继承ApplicationEvent之外,还要传入事件源作为构造参数。
事件监听器
Spring定义了一个ApplicationListener接口作为事件监听器的抽象,接口定义如下:
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
/**
* Handle an application event.
* @param event the event to respond to
*/
void onApplicationEvent(E event);
}
(1)该接口继承了JDK中表示事件监听器的标记接口EventListener,内部只定义了一个抽象方法onApplicationEvent(evnt),当监听的事件在容器中被发布,该方法将被调用。
(2)同时,该接口是一个泛型接口,其实现类可以通过传入泛型参数指定该事件监听器要对哪些事件进行监听。这样有什么好处?这样所有的事件监听器就可以由一个事件发布器进行管理,并对所有事件进行统一发布,而具体的事件和事件监听器之间的映射关系,则可以通过反射读取泛型参数类型的方式进行匹配,稍后我们会对原理进行讲解。
(3)最后,所有的事件监听器都必须向容器注册,容器能够对其进行识别并委托容器内真正的事件发布器进行管理。
事件发布器
ApplicationContext接口继承了ApplicationEventPublisher接口,从而提供了对外发布事件的能力,如下所示:
那么是否可以说ApplicationContext,即容器本身就担当了事件发布器的角色呢?其实这是不准确的,容器本身仅仅是对外提供了事件发布的接口,真正的工作其实是委托给了具体容器内部一个ApplicationEventMulticaster对象,其定义在AbstractApplicationContext抽象类内部,如下所示:
所以,真正的事件发布器是ApplicationEventMulticaster,这是一个接口,定义了事件发布器需要具备的基本功能:管理事件监听器以及发布事件。
其默认实现类是
SimpleApplicationEventMulticaster,该组件会在容器启动时被自动创建,并以单例的形式存在,管理了所有的事件监听器,并提供针对所有容器内事件的发布功能。
AbstractApplicationContext#refresh方法中的initApplicationEventMulticaster()步骤会判断是否存在,不存在就实例化一个SimpleApplicationEventMulticaster。
具体实现细节看
Spring事件监听源码解析
基于Spring实现对任务执行结果的监听
基于Spring框架来实现对自定义事件的监听需要三步:
• ① 继承spring事件基类ApplicationEvent,完成事件的封装;
• ② 实现ApplicationListener接口或者通过@EventListener注解,把事件监听器注册到spring容器里;
• ③ 业务类要实现ApplicationEventPublisherAware接口或者引用ApplicationContext,把spring的事件发布器注入到业务类里,在触发事件的时候,spring的事件发布器发布事件;
自定任务结束事件
定义一个任务结束事件SignInEvent,该类继承抽象类ApplicationEvent来遵循容器事件规范。
public class SignInEvent extends ApplicationEvent {
private static final long serialVersionUID = -578995207794413821L;
/**
* Create a new {@code ApplicationEvent}.
*
* @param source the object on which the event initially occurred or with
* which the event is associated (never {@code null})
*/
public SignInEvent(Object source) {
super(source);
}
}
自定义短信服务监听器并向容器注册
该类实现了容器事件规范定义的监听器接口,通过泛型参数指定对上面定义的任务结束事件进行监听,通过@Component注解向容器进行注册。
@Slf4j
@Component
public class MesTaskListener1 implements ApplicationListener<SignInEvent> {
@Override
public void onApplicationEvent(SignInEvent event) {
User user = (User) event.getSource();
// 调用具体的发送短信接口
log.info("send message to user : {}", JSON.toJSONString(user));
}
}
也可以通过注解的方式实现
@Slf4j
@Component
public class MesTaskListener2 {
@EventListener
public void doSendMesEvent(SignInEvent event) {
User user = (User) event.getSource();
// 调用具体的发送短信接口
log.info("send message to user : {}", JSON.toJSONString(user));
}
}
发布事件
从上面对Spring事件监听机制的类结构分析可知,发布事件的功能定义在ApplicationEventPublisher接口中,而ApplicationContext继承了该接口,所以最好的方法是通过实现ApplicationContextAware接口获取ApplicationContext实例,然后调用其发布事件方法。如下所示定义了一个发布容器事件的代理类:
@Component
public class SignInServiceImpl implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Boolean signIn(User user) {
// 登录成功
// 发布事件
publishEvent(new SignInEvent(user));
return Boolean.TRUE;
}
// 发布事件
public void publishEvent(ApplicationEvent event) {
applicationContext.publishEvent(event);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
因为ApplicationContext继承了ApplicationEventPublisher接口,也可以直接通过ApplicationContext调用其发布事件方法。
@Component
public class SignInServiceImpl2 {
@Autowired
private ApplicationContext applicationContext;
public Boolean signIn(User user) {
// 登录成功
// 发布事件
applicationContext.publishEvent(new SignInEvent(user));
return Boolean.TRUE;
}
}
在此基础上,还可以自定义一个邮件服务监听器,在任务执行结束时发送邮件通知用户。过程和上面自定义短信服务监听器类似:实现ApplicationListner接口并重写抽象方法,然后通过注解或者xml的方式向容器注册。
@Slf4j
@Component
public class EmailTaskListener {
@EventListener
public void doSendEmailEvent(SignInEvent event) {
User user = (User) event.getSource();
// 调用具体的发送短信接口
log.info("send Email to user : {}", JSON.toJSONString(user));
}
}
测试发布事件
@RunWith(SpringRunner.class)
@SpringBootTest()
public class SpringTest {
@Autowired
private SignInServiceImpl1 signInService;
@Test
public void testEvent() {
signInService.signIn(User.builder().id(123546L).name("小明").phone("15232654678").build());
}
}
测试结果
看到这里是不是有个疑问?
1.如果想要改变自定义监听器的执行顺序该怎么做?
2.如果其中某个自定义事件发生异常,会不会影响其它自定义事件?
我们来看下:
1.执行顺序
注解方式:
使用注解 @Order()
和 @EventListener
:
注意:Order的值越小,优先级越高
2.异常问题
我们自己写一个异常
可以看到一个事件发生异常后,剩余的事件都不能执行,这个时候我们可以使用@Async
注解来解决
启动后发现:
task-scheduler-1发送短信的异常并不影响task-scheduler-2发送邮箱业务