Spring 事件广播机制详解

前言

写这篇文章的初衷源自对 Spring 框架中事件机制的好奇心。在编写观察者模式代码示例时,我突然想起了 Spring 框架中支持多事件监听的特性,例如ContextClosedEvent、ContextRefreshedEvent、ContextStartedEvent等等。传统的观察者模式通常是基于单一事件的,但 Spring 框架却提供了一种更为灵活的机制,可以处理多个不同类型的事件。

因此,我决定深入研究 Spring 框架中多事件监听的实现机制,并将我的学习总结记录下来。通过这篇文章,我希望能够帮助读者更好地理解 Spring 框架中事件机制的工作原理,以及如何利用这一机制来构建灵活、高效的应用程序。我相信这对于对 Spring 框架感兴趣的开发者来说会是一次有益的学习经历。

一、Spring 事件简介

1、Spring Context 模块内嵌的事件

image-20240325234344588

实际上我们,继承 ApplicationEvent 的事件对象很多,他们分布在 Spring 生态的各个模块(spring framework、spring mvc、springboot)中,这里就不一一赘述了,就简单介绍下 spring-context 模块下实现的几个事件,如上图所示,spring-context 模块下内嵌了四种事件容器刷新容器开启容器关闭容器停止

通过 UML 图可以看到,他们有一个共同的父类就是 EventObject,为什么 Spring 没有自立门户而是选择了继承 EventObject 呢?我猜测作者可能有以下的考虑:

  • 遵循标准:Java 标准库提供了 EventObject 类作为事件的基类,这是一种广泛接受和认可的设计模式。Spring 框架遵循这一标准,可以使开发者能够更加容易地理解和使用 Spring 事件机制,而不需要学习新的事件模型。
  • 与 Java 生态的整合:继承自标准库的 EventObject 类使得 Spring 框架的事件机制能够更好地与 Java 生态中其他库和框架整合。这种一致性有助于开发者在不同的项目中使用相似的编程模型,提高了代码的可维护性和可复用性。
  • 减少重复工作:避免造轮子。

2、 Spring 事件监听与广播

我们先来看下关于事件监听和广播相关组件的 UML 图:

image-20240327225231069

可能大家感觉这个图比较绕。是的,乍一看是有点绕,但是我们进行下归类就可以变得清晰,我们分为以下三类:

  • 事件存储器
  • 事件监听器
  • 事件广播器
2.1、事件存储器

在事件存储器方面,我们有两个关键类:

  • ListenerRetriever(监听器检索器):用于事件的存储和检索。该类包含一个 Set<ApplicationEvent<?>> 属性,用于存储注册的事件监听器。
  • ListenerCacheKey(监听器缓存键):此类用于事件缓存,以提高在广播过程中查找与特定事件相关联的监听器的速度。通过使用缓存,可以有效地提升广播性能。
2.2、事件监听器

事件监听器由 ApplicationListener 类扮演:

  • ApplicationListener(应用程序监听器):它继承自 EventListener 接口,并且通过泛型上限限制了监听的事件类型为 ApplicationEvent 或其子类。这个类的实现用于处理特定类型的事件,可以在应用程序中注册多个监听器来响应不同类型的事件。
2.3、事件广播器

最后,让我们关注事件广播器,有两个主要组件:

  • ApplicationEventPublisher(应用事件发布器):定义了事件发布的行为。它允许应用程序通过调用 publishEvent() 方法来发布特定的事件,然后将该事件传递给已注册的监听器。
  • SimpleApplicationEventMulticaster(简单应用事件多播器):此组件实现了多事件监听器的广播。它负责管理事件监听器的注册和通知,并根据特定的事件类型将事件分发给相应的监听器。通过使用多播器,应用程序可以有效地处理并响应各种事件。

二、Spring 事件应用

阅读完上面的内容后,相信大家对 Spring 事件机制有了一定的了解,接下来就趁热打铁,动手实现一个事件监听示例来彻底掌握 Spring 事件的应用。

针对 Spring 应用程序**刷新完成(refresh)**进行监听,打印 Spring 应用中所有的 Bean 示例。

自定义一个监听器实现 ApplicationListener

package com.markus.spring.event.listener;import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;/*** @Author: zhangchenglong06* @Date: 2024/3/25* @Description:*/
public class ContextRefreshedEventListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {ApplicationContext context = (ApplicationContext) event.getSource();String[] beanDefinitionNames = context.getBeanDefinitionNames();System.out.println("=============== 开始打印 Bean Name ===============");for (String beanDefinitionName : beanDefinitionNames) {System.out.println(beanDefinitionName);}System.out.println("=============== 结束打印 Bean Name ===============");}
}

创建 ApplicationContext,并将 ContextRefreshedEventListener 注册进容器中,接着刷新应用上下文,监听到 ContextRefreshedEvent 事件打印 Bean 名称列表

package com.markus.spring.event;import com.markus.spring.event.listener.ContextRefreshedEventListener;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;/*** @Author: zhangchenglong06* @Date: 2024/3/25* @Description:*/
@Configuration
public class ApplicationEventListenerDemo {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();context.register(ApplicationEventListenerDemo.class);context.addApplicationListener(new ContextRefreshedEventListener());context.refresh();context.close();}
}

三、Spring 事件广播原理

本文多次提到 Spring 事件监听机制实现的是多事件,那是如何的多事件的,接下来我们就深入 Spring 框架内部去一探究竟!

1、事件监听器的来源

这里我们先入为主,声明一下事件监听器的来源,通常我们会通过如下两种方式去向 Spring 容器中注册监听器:

  • 将监听器注册为 Spring Bean
  • 通过 API 添加至 Spring 容器中

image-20240327234205348

2、事件发布

2.1、事件监听器召回
// org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners
protected Collection<ApplicationListener<?>> getApplicationListeners(ApplicationEvent event, ResolvableType eventType) {// 获取事件中的对象源Object source = event.getSource();Class<?> sourceType = (source != null ? source.getClass() : null);// 构建 缓存 KeyListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);// Potential new retriever to populateCachedListenerRetriever newRetriever = null;// 快速检查缓存中是否有该事件类型相关的监听器CachedListenerRetriever existingRetriever = this.retrieverCache.get(cacheKey);if (existingRetriever == null) {// 如果没有的话就缓存一个新的 ListenerRetrieverif (this.beanClassLoader == null ||(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {newRetriever = new CachedListenerRetriever();existingRetriever = this.retrieverCache.putIfAbsent(cacheKey, newRetriever);if (existingRetriever != null) {newRetriever = null;  // no need to populate it in retrieveApplicationListeners}}}if (existingRetriever != null) {Collection<ApplicationListener<?>> result = existingRetriever.getApplicationListeners();if (result != null) {return result;}}// 实现监听器集合的召回return retrieveApplicationListeners(eventType, sourceType, newRetriever);
}
private Collection<ApplicationListener<?>> retrieveApplicationListeners(ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable CachedListenerRetriever retriever) {List<ApplicationListener<?>> allListeners = new ArrayList<>();Set<ApplicationListener<?>> filteredListeners = (retriever != null ? new LinkedHashSet<>() : null);Set<String> filteredListenerBeans = (retriever != null ? new LinkedHashSet<>() : null);Set<ApplicationListener<?>> listeners;Set<String> listenerBeans;synchronized (this.defaultRetriever) {// 事件监听器的全集来源:通过 API 添加的 以及 注册为 Bean的。listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);}// 对通过 API 添加的监听器进行筛选,筛选逻辑在 supportsEvent 方法中for (ApplicationListener<?> listener : listeners) {if (supportsEvent(listener, eventType, sourceType)) {if (retriever != null) {filteredListeners.add(listener);}allListeners.add(listener);}}// 对通过注册 Bean 的监听器进行筛选,筛选逻辑在 supportsEvent 方法中if (!listenerBeans.isEmpty()) {ConfigurableBeanFactory beanFactory = getBeanFactory();for (String listenerBeanName : listenerBeans) {try {if (supportsEvent(beanFactory, listenerBeanName, eventType)) {ApplicationListener<?> listener =beanFactory.getBean(listenerBeanName, ApplicationListener.class);if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {if (retriever != null) {if (beanFactory.isSingleton(listenerBeanName)) {filteredListeners.add(listener);}else {filteredListenerBeans.add(listenerBeanName);}}allListeners.add(listener);}}else {// Remove non-matching listeners that originally came from// ApplicationListenerDetector, possibly ruled out by additional// BeanDefinition metadata (e.g. factory method generics) above.Object listener = beanFactory.getSingleton(listenerBeanName);if (retriever != null) {filteredListeners.remove(listener);}allListeners.remove(listener);}}catch (NoSuchBeanDefinitionException ex) {// Singleton listener instance (without backing bean definition) disappeared -// probably in the middle of the destruction phase}}}// 针对所有的监听器进行个排序,这里说明同一事件的不同监听器执行是有顺序的AnnotationAwareOrderComparator.sort(allListeners);if (retriever != null) {// 这里的判断主要是将 单例监听器和非单例监听器区分开来if (filteredListenerBeans.isEmpty()) {retriever.applicationListeners = new LinkedHashSet<>(allListeners);retriever.applicationListenerBeans = filteredListenerBeans;}else {retriever.applicationListeners = filteredListeners;retriever.applicationListenerBeans = filteredListenerBeans;}}// 将符合当前事件的所有的监听器返回return allListeners;
}
2.2、实现多事件监听的关键代码

概括一下这段代码就是:匹配出支持当前事件的监听器。具体实现就是将事件监听器的泛型类型参数和当前时间的类型进行比对,如果能匹配就说明当前监听器是监听当前的事件。

private boolean supportsEvent(ConfigurableBeanFactory beanFactory, String listenerBeanName, ResolvableType eventType) {Class<?> listenerType = beanFactory.getType(listenerBeanName);if (listenerType == null || GenericApplicationListener.class.isAssignableFrom(listenerType) ||SmartApplicationListener.class.isAssignableFrom(listenerType)) {return true;}if (!supportsEvent(listenerType, eventType)) {return false;}try {BeanDefinition bd = beanFactory.getMergedBeanDefinition(listenerBeanName);ResolvableType genericEventType = bd.getResolvableType().as(ApplicationListener.class).getGeneric();return (genericEventType == ResolvableType.NONE || genericEventType.isAssignableFrom(eventType));}catch (NoSuchBeanDefinitionException ex) {// Ignore - no need to check resolvable type for manually registered singletonreturn true;}
}

四、本文总结

好了,小结一下。

通过本文的详细介绍,我们对 Spring 框架中的事件广播机制有了更深入的了解。我们首先探讨了 Spring 框架中的事件模型,了解了在 Spring 生态中各个模块内嵌的事件,以及它们共同继承的父类 EventObject。我们也分析了为什么 Spring 选择继承 EventObject 而不是自行实现一套事件机制,这是因为遵循标准、与 Java 生态的整合以及减少重复工作等考虑。

接着,我们深入研究了 Spring 框架中事件监听和广播相关的组件。我们将这些组件分为事件存储器、事件监听器和事件广播器三个类别,并对每个类别下的关键类进行了详细的介绍。通过这种方式,我们更清晰地理解了 Spring 框架中事件的存储、监听和广播的机制。

最后,我们通过一个实际的示例演示了如何使用 Spring 框架中的事件机制。我们编写了一个简单的示例程序,演示了如何监听 Spring 应用程序刷新完成事件,并在事件发生时打印出所有 Bean 的名称列表。通过这个示例,我们加深了对 Spring 事件机制的理解,并展示了如何在实际项目中应用这一机制。

综上所述,本文对 Spring 框架中事件广播机制进行了全面而深入的探讨,希望能够帮助读者更好地理解和应用 Spring 框架中强大的事件机制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/573072.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JDK8的下载安装与环境变量配置教程

前言 官网下载&#xff1a;Java Archive Downloads - Java SE 8u211 and later 现在应该没人用32位的系统了吧&#xff0c;直接下载Windows x64 Installer jdk-8u391-windows-x64.exe 一、安装JDK 1. 打开jdk-8u391-windows-x64.exe 2. 直接下一步 3. 这个地方不要动他&…

剑指Offer题目笔记19(二分查找)

面试题68&#xff1a; 问题&#xff1a; ​ 输入一个排序的整形数组nums和一个目标值t&#xff0c;如果数组nums中包含t&#xff0c;则返回在数组中的下标&#xff0c;否则返回按照顺序插入到数组的下标。 解决方案&#xff1a; ​ 使用二分查找。每次二分查找都选取位于数组…

Docker数据卷挂载

一、容器与数据耦合的问题: 数据卷是虚拟的&#xff0c;不真实存在的&#xff0c;它指向文件中的文件夹 &#xff0c;属主机文件系统通过数据卷和容器数据进行联系&#xff0c;你改变我也改变。 解决办法&#xff1a; 对宿主机文件系统内的文件进行修改&#xff0c;会立刻反应…

用搜索引擎收集信息-常用方式

1&#xff0c;site csdn.net &#xff08;下图表示只在csdn网站里搜索java&#xff09; 2&#xff0c;filetype:pdf &#xff08;表示只检索某pdf文件类型&#xff09; 表示在浏览器里面查找有关java的pdf文件 3&#xff0c;intitle:花花 &#xff08;表示搜索网页标题里面有花…

VTK 示例 基本的流程-事件交互、球体、

流程可以总结如下&#xff1a; 导入所需的头文件&#xff1a; 首先&#xff0c;导入了一系列 VTK 头文件&#xff0c;这些文件包含了所需的类和函数声明。 创建对象&#xff1a; 创建了两个球体&#xff08;一个较大&#xff0c;一个较小&#xff09;&#xff0c;一个平面&…

Spring实战:采用Spring配置文件管理Bean

文章目录 一、Spring框架概述二、实战&#xff1a;采用Spring配置文件管理Bean&#xff08;一&#xff09;创建Jakarta EE项目&#xff08;二&#xff09;添加Spring依赖&#xff08;三&#xff09;创建杀龙任务类&#xff08;四&#xff09;创建勇敢骑士类&#xff08;五&…

亚信安全荣获2023年度5G创新应用评优活动两项大奖

近日&#xff0c;“关于2023 年度5G 创新应用评优活动评选结果”正式公布&#xff0c;亚信安全凭借在5G安全领域的深厚积累和创新实践&#xff0c;成功荣获“5G技术创新的优秀代表”和“5G应用创新的杰出实践”两项大奖。 面向异构安全能力的5G安全自动化响应系统 作为5G技术创…

205基于matlab的关于多目标跟踪的的滤波程序

基于matlab的关于多目标跟踪的的滤波程序&#xff0c;包括采用联合概率数据互联&#xff08;JPDA&#xff09;算法实现两个个匀速运动目标的点迹与航迹的关联&#xff0c;输出两个目标跟踪的观测位置、估计位置以及估计误差。程序已调通&#xff0c;可直接运行。 205 多目标跟踪…

GEC6818开机自动加载驱动与更改开发板的RTC时钟

GEC6818开机自动加载驱动与更改开发板的RTC时钟 本文主要涉及&#xff1a; 1.GEC6818开机自动加载驱动 2.更改开发板的RTC时钟 文章目录 GEC6818开机自动加载驱动与更改开发板的RTC时钟一、开机自动加载驱动或运行程序**STEP1&#xff1a;** 使用vi打开文件profile.命令如下**S…

分享多种mfc100u.dll丢失的解决方法(一键修复DLL丢失的方法)

在使用电脑过程中&#xff0c;我们经常会遇到一些陌生的DLL文件&#xff0c;例如mfc100u.dll。这些DLL文件是动态链接库&#xff08;Dynamic Link Libraries&#xff09;的缩写&#xff0c;它们包含了可以被多个程序共享的代码和数据。今天&#xff0c;我们将深入探讨mfc100u.d…

Windows下载使用nc(netcat)命令

‘nc’ 不是内部或外部命令&#xff0c;也不是可运行的程序&#xff1f; 点击链接地址&#xff0c;下载压缩包。 完成后解压 使用方式&#xff08;三种&#xff09;&#xff1a; 1、直接双击exe使用 2、把这个exe放到cmd启动的默认路径下 放到默认路径下&#xff0c;使用nc&a…

plantegg-10+倍性能提升全过程–优酷账号绑定淘宝账号的TPS从500到5400的优化历程

原文地址:https://plantegg.github.io/2018/01/23/10%E5%80%8D%E6%80%A7%E8%83%BD%E6%8F%90%E5%8D%87%E5%85%A8%E8%BF%87%E7%A8%8B/ 背景说明 2016年的双11在淘宝上买买买的时候&#xff0c;天猫和优酷土豆一起做了联合促销&#xff0c;在天猫双11当天购物满XXX元就赠送优酷会…