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> {

    @Override
    public 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);
  // 构建 缓存 Key
  ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

  // Potential new retriever to populate
  CachedListenerRetriever newRetriever = null;

  // 快速检查缓存中是否有该事件类型相关的监听器
  CachedListenerRetriever existingRetriever = this.retrieverCache.get(cacheKey);
  if (existingRetriever == null) {
    // 如果没有的话就缓存一个新的 ListenerRetriever
    if (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 singleton
    return true;
  }
}
protected boolean supportsEvent(ApplicationListener<?> listener,
		Class<? extends ApplicationEvent> eventType, Class<?> sourceType) {

	SmartApplicationListener smartListener = (listener instanceof SmartApplicationListener ?
			(SmartApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
	return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}

四、本文总结

好了,小结一下。

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

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于springboot+vue实现的驾校信息管理系统

作者主页&#xff1a;Java码库 主营内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app等设计与开发。 收藏点赞不迷路 关注作者有好处 文末获取源码 技术选型 【后端】&#xff1a;Java 【框架】&#xff1a;spring…

YOLOv9改进策略 :IoU优化| Inner-IoU基于辅助边框的IoU损失,高效结合新型边界框相似度度量(MPDIoU)| 二次创新

💡💡💡本文独家改进:Inner-IoU引入尺度因子 ratio 控制辅助边框的尺度大小用于计算损失,新型边界框相似度度量(MPDIoU)MPDIoU损失进行有效结合 💡💡💡适用场景:小目标数据集,进一步提升检测精度,强烈推荐 《YOLOv9魔术师专栏》将从以下各个方向进行创新: …

【Java八股面试系列】Arraylist和HashMap的底层原理

文章目录 ArrayList源码总&#xff1a;构造方法扩容机制remove HashMap总&#xff1a;构造方法细节问题putVal()方法resize()方法Hash值 HashMap常见问题 ConcurrentHashMap总&#xff1a;putVal()方法自己的测试 为什么重写HashCode和equals ArrayList源码 总&#xff1a; *…

代码随想录算法训练营三刷day41 | 动态规划之 343. 整数拆分 96.不同的二叉搜索树

三刷day41 343. 整数拆分确定dp数组&#xff08;dp table&#xff09;以及下标的含义确定递推公式dp的初始化确定遍历顺序举例推导dp数组 96.不同的二叉搜索树确定dp数组&#xff08;dp table&#xff09;以及下标的含义确定递推公式dp数组如何初始化确定遍历顺序举例推导dp数组…

2024年,我写了一个视频去水印的微信小程序

花了两天时间&#xff0c;写了一个简单的微信小程序&#xff0c;主要为了学习一下微信小程序相关的知识。 目录 一、功能介绍 二、页面展示 三、简单总结 四、在线体验 一、功能介绍 小程序的主要功能是对目前的主流视频平台的视频进行去水印处理。 项目难点在于收集各个平…

关于多物理场耦合仿真的相关思考

关于多物理场耦合仿真&#xff0c;写点自己的思考。 1 核心本质 多物理场耦合仿真&#xff0c;听起来是个挺高大上的名词。不少人被各种名词创新搞得云里雾里&#xff0c;不知所谓。 实际上&#xff0c;多物理场耦合仿真理解起来并不算复杂。搞清楚了本质&#xff0c;做多物理…

LeetCode-热题100:160. 相交链表

给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果后&…

“美国债务螺旋上升,每百天膨胀万亿”!华尔街:投入比特币是明智之举,美元早晚垮台?

​ 前不久&#xff0c;黄金和比特币价格的双双逼近历史高位&#xff0c;再度吸引了不少金融市场参与者的关注。虽然这两类资产大涨的背后&#xff0c;有着诸如比特币减半临近、地缘局势引发避险等各自的原因&#xff0c;但也有一些业内人士提到了美国政府债务规模激增等无法回…

day_2FreeRTOS使用PWM+ADC光敏电阻完成光控灯实验

主要代码&#xff1a; int adc_val0;//保存ADC采集到的数值 float volt0;//保存电压值HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);//打开定时器的PWM通道3 TIM3->CCR30;//改变CCR的值&#xff0c;范围0——999&#xff0c;不能超过ARRwhile (1){ HAL_ADC_Start(&had…

小米SU7又“赢麻了”对标雷军的爽文人生:天选成功人士

会议之眼 快讯 2024年3月28日&#xff0c;小米SU7汽车盛大发布&#xff0c;吸引了众多关注者。SU7标准版售价21.59万元&#xff0c;Pro版24.59万元&#xff0c;Max版本29.99万元&#xff0c;全部控制在30万元以内。发布会场面火爆&#xff0c;各大车企领导齐聚&#xff0c;雷军…

OMP压缩感知仿真(MATLAB)

clc; clearvars; close all;% 读文件 Ximread(mandrill256.bmp); tic; Xdouble(X); [m,n]size(X);% % 小波变换矩阵生成 [LL1, LH1, HL1, HH1] dwt2(X, haar); [LL2, LH2, HL2, HH2] dwt2(LL1, haar); % [LL3, LH3, HL3, HH3] dwt2(LL2, haar); % [LL4, LH4, HL4, HH4] d…

ObjectiveC-07-OOP面向对象程序设计基础

OOP&#xff08;面向对象程序设计&#xff09;是一个简单又复杂的课题&#xff0c;之所以简单是因为其概念清晰&#xff0c;内容简单&#xff0c;之所以复杂是因为没有固定的模式可寻&#xff0c;正所谓千人千面。 从本节开始&#xff0c;笔者大概会用5篇左右不同的专题来讲解O…

虚拟机ip不停地变每次使用ssh不好登录?有手就行!

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 虚拟机ip不停地变每次使用ssh不好登录&#xff1f;有手就行&#xff01; 桥接模式下固定ip&#xff1f;NoAvahi服务&#xff0c;你值得拥有Avahi解决方案虚拟机中配置Avahi服务配置成功展示测试成功 桥…

MySQL故障排查与生产环境优化

一、MySQL逻辑架构图 客户端和连接服务核心服务功能存储引擎层数据存储层 二、MySQL故障排查 1、MySQL单实例故障排查 故障一 故障现象&#xff1a; ERROR 2002 (HY000): Cant connect to local MySQL server through socket /data/mysql/mysql.sock (2)问题分析&#xff…

使用Pollard_rho算法分解质因数

分解质因数的朴素算法 最简单的算法即为从 [2, sqrt&#xff08;N&#xff09;] 进行遍历。 vector<int> breakdown(int N) {vector<int> result;for (int i 2; i * i < N; i) {if (N % i 0) { // 如果 i 能够整除 N&#xff0c;说明 i 为 N 的一个质因子。…

求组合背包II(acwing)

题目描述&#xff1a; 给定n组循问&#xff0c;每组询问给定两个整数a&#xff0c;b&#xff0c;请你输出Ca^b mod (1e9 7)的值&#xff0c;。 输入格式&#xff1a; 第一行包含整数n。 接下来2行&#xff0c;每行包含一组a和b。 输出格式&#xff1a; …

Vscode下使用markdown入门

1.安装vscode插件 1. **Markdown All in One** ——提供丰富的Markdown相关的快捷键、自动补全功能&#xff0c;提高md文档编写生产力 2. **Markdown Preview Ehanced** ——用于渲染当前编写文档的效果同步预览 3. **Paste Image** ——用于快速引用图片至Markdown文…

视频素材库有哪些网站?八大平台视频素材库创作推荐

视频创作的小达人们&#xff0c;是不是经常在想&#xff0c;视频素材库有哪些网站能提供高质量的素材呢&#xff1f;别担心&#xff0c;今天我要为你们揭秘八个超棒的视频素材网站&#xff0c;让你的视频制作更加轻松在创作的路上如鱼得水&#xff01; 蛙学网&#xff1a;海量…

【tensorflow框架神经网络实现鸢尾花分类_Keras】

文章目录 1、前言2、鸢尾花分类3、结果打印 1、前言 【tensorflow框架神经网络实现鸢尾花分类】一文中使用自定义的方式&#xff0c;实现了鸢尾花数据集的分类工作。在这里使用tensorflow中的keras模块快速、极简实现鸢尾花分类任务。 2、鸢尾花分类 import tensorflow as t…

.DevicData-P-XXXXXXXX勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复

导言&#xff1a; 随着信息技术的飞速发展&#xff0c;网络安全问题日益突出&#xff0c;其中勒索病毒成为了一种日益严重的威胁。.DevicData-P-XXXXXXXX勒索病毒就是其中一种典型的恶意软件&#xff0c;它通过加密用户文件并要求赎金来解锁的方式&#xff0c;给企业和个人带来…