Spring 事件广播机制详解

news2024/11/24 6:34:48

前言

写这篇文章的初衷源自对 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 框架中强大的事件机制。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2024/4/2 IOday4

使用文件IO 实现父进程向子进程发送信息&#xff0c;并总结中间可能出现的各种问题 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd…

盘点AI编程效率神器合集,代码助手工具大模型、Agent智能体

关注wx公众号:aigc247 进社群加wx号&#xff1a;aigc365 程序员是最擅长革自己命的职业&#xff0c;让我们借助AI的力量一起摸鱼一起卷&#xff01; 据说好用的AI代码助手工具、大模型、Agent智能体 微软的compoliot&#xff1a;AI神器之微软的编码助手Copilot-CSDN博客 阿…

数据库系统概论(超详解!!!) 第三节 关系数据库标准语言SQL(Ⅳ)

1.集合查询 集合操作的种类 并操作UNION 交操作INTERSECT 差操作EXCEPT 参加集合操作的各查询结果的列数必须相同;对应项的数据类型也必须相同 查询计算机科学系的学生及年龄不大于19岁的学生。SELECT *FROM StudentWHERE Sdept CSUNIONSELECT *FROM StudentWHERE Sage&l…

【与C++的邂逅】---- 函数重载与引用

关注小庄 顿顿解馋(▿) 喜欢的小伙伴可以多多支持小庄的文章哦 &#x1f4d2; 数据结构 &#x1f4d2; C 引言 : 上一篇博客我们了解了C入门语法的一部分&#xff0c;今天我们来了解函数重载&#xff0c;引用的技术&#xff0c;请放心食用 ~ 文章目录 一. &#x1f3e0; 函数重…

使用vscode前面几行被定住

当我们使用 vscode 滚动代码文档的时候&#xff0c;发现前面几行被定住了&#xff0c;想 css 的 sticky 一样&#xff0c;可能是之前我们不小心点到了这里&#xff0c;取消就好了

视觉Transformer和Swin Transformer

视觉Transformer概述 ViT的基本结构&#xff1a; ①输入图片首先被切分为固定尺寸的切片&#xff1b; ②对展平的切片进行线性映射&#xff08;通过矩阵乘法对维度进行变换&#xff09;&#xff1b; ③为了保留切片的位置信息&#xff0c;在切片送入Transformer编码器之前&…

做抖音小店,体验分一定要很高吗?多少分才是最佳?

大家好&#xff0c;我是电商花花。 做抖音小店&#xff0c;我们都知道体验分非常重要&#xff0c;如果做抖音小店不重视店铺的体验分&#xff0c;对于我们店铺影响还是很大的&#xff0c;体验分不仅影响我们店铺的销量&#xff0c;更是一个店铺流量的直接开关。 店铺体验分越…

Day28:回溯法 LeedCode 93.复原IP地址 78.子集 90.子集II

93. 复原 IP 地址 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址&#xff0c;但是 …

DETREC数据集标注 VOC格式

经过将DETRAC数据集转换成VOC格式&#xff0c;并使用labelimg软件进行查看&#xff0c;发现该数据集存在很多漏标情况&#xff0c;截图如下所示。

121314饿

c语言中的小小白-CSDN博客c语言中的小小白关注算法,c,c语言,贪心算法,链表,mysql,动态规划,后端,线性回归,数据结构,排序算法领域.https://blog.csdn.net/bhbcdxb123?spm1001.2014.3001.5343 给大家分享一句我很喜欢我话&#xff1a; 知不足而奋进&#xff0c;望远山而前行&am…

vue3 记录页面滚动条的位置,并在切换路由时存储或者取消

需求&#xff0c;当页面内容超出了浏览器可是屏幕的高度时&#xff0c;页面会出现滚动条。当我们滚动到某个位置时&#xff0c;操作了其他事件或者跳转了路由&#xff0c;再次回来时&#xff0c;希望还在当时滚动的位置。那我们就进行一下操作。 我是利用了会话存储 sessionSto…

SpringBoot+ECharts+Html 地图案例详解

1. 技术点 SpringBoot、MyBatis、thymeleaf、MySQL、ECharts 等 此案例使用的地图是在ECharts社区中查找的&#xff1a;makeapie echarts社区图表可视化案例 2. 准备条件 在mysql中创建数据库echartsdb&#xff0c;数据库中创建表t_location_count表&#xff0c;表中设置两个…

蚁剑流量分析

蚁剑流量分析 在靶机上面上传一个一句话木马&#xff0c;并使用蚁剑连接&#xff0c;进行抓包, 一句话木马内容 <?php eval($_POST[1]); defalut编码器 在使用蚁剑连接的时候使用default编码器 连接之后进行的操作行为是查看当前目录(/var/www/html)下的文件&#xff0…

InternLM

任务一 运行1.8B模型&#xff0c;并对话 User >>> 请创作一个 300 字的小故事 在一片茂密的森林里&#xff0c;住着一只小松鼠&#xff0c;它的名字叫做小雪。小雪非常活泼好动&#xff0c;经常在树上跳跃玩耍。有一天&#xff0c;小雪发现了一个神秘的洞穴&#xf…

网络编程详解(select poll epoll reactor)

1. 客户端服务器建立连接过程 1.1 编写一个server的步骤是怎么样的&#xff1f; int main(){int listenfd, connfd;pid_t childpid;socklen_t clilen;struct sockaddr_in cliaddr, servaddr;listenfd socket(AF_INET, SOCK_STREAM, 0);bzero(&servaddr, sizeof(servaddr…

【保姆级讲解下MySQL中的drop、truncate和delete的区别】

&#x1f308;个人主页:程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

4.2学习总结

一.java学习总结 (本次java学习总结,主要总结了抽象类和接口的一些知识,和它们之间的联系和区别) 一.抽象类 1.1定义: 抽象类主要用来抽取子类的通用特性&#xff0c;作为子类的模板&#xff0c;它不能被实例化&#xff0c;只能被用作为子类的超类。 2.概括: 有方法声明&…

在jsp文件内使用jdbc报错

使用idea创建javaweb项目后&#xff0c;在jsp文件内使用jdbc连接数据库错误&#xff0c;显示以下内容&#xff1a; java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriverat org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappCl…

相关滤波跟踪算法-CSK

0. 写在前面 对相关滤波算法综述比较强的文档&#xff1a; NIUBILITY的相关滤波框架详解 - 知乎 (zhihu.com) 1. 概述 相关滤波算法问世之前&#xff0c;跟踪算法饱受运行时间的困扰&#xff0c;直到MOSSE算法出现&#xff0c;直接将算法速度提到了615fps&#xff0c;第一次将…

Makefile:通用部分头文件与条件判断(八)

1、通用部分做头文件 首先举个例子看看为什么需要这个东西&#xff0c;例如在一个文件夹下有两个项目&#xff0c;两个项目都需要编写makefile编译&#xff0c;此时可以使用公共头文件 目录结构如下&#xff1a; 1.1、项目&#xff08;一&#xff09; 有a.cpp、b.cpp、c.cpp…