如何掌握Spring事件发布和监听机制?

在软件设计和开发过程中,事件驱动是一种非常主流的架构模式,它的基本组成见下图,可以看到存在一个事件中心,而各个服务可以执行事件发布、订阅和消费等基本过程。

   

事件驱动架构代表的是一种架构设计风格,实现方法和工具有很多。今天的内容将围绕Spring框架来介绍它的事件发布和监听机制。事实上,事件处理也是Spring内置的一个功能强大的扩展点,这种扩展点实现机制更加灵活,但也更加复杂。

对于所应该具备的技术组件而言,事件驱动架构与观察者模式比较类似,基本上都采用了如下图所示的组件结构。


作为类比,观察者模式中的主题就相当于是上图的事件(Event),观察者则对应事件监听器(EventListener)。而这里的事件发布器(EventPublisher)用来发布事件,并管理着一组事件监听器。一旦某个事件被发布,那么对它感兴趣的监听器就会监听到该事件,并触发事先定义好的处理逻辑。

Spring框架采用的也是与上图类似的设计方法。接下来,让我们看一下Spring中对整个事件驱动架构的抽象和实现过程。

Spring事件发布和监听机制

用户发布事件的ApplicationEventPublisher和用于监听事件的ApplicationListener是Spring框架中实现事件驱动编程的核心类。我们知道在ApplicationContext的整个生命周期中,每一个阶段或操作的开始和结束都可以触发一种事件,这些事件被封装成ApplicationEvent对象。通过这些接口,可以实现针对ApplicationContext中各种事件的处理。


一旦一个Bean实现了ApplicationListener接口,那么每当ApplicationEventPublisher发布事件时,这个Bean将自动触发监听器处理程序。ApplicationListener接口定义如下所示。

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

void onApplicationEvent(E event);

}

有了事件的监听器,就有事件的发布器。在Spring中,事件发布器由ApplicationEventPublisher接口进行表示,该接口定义如下所示。

public interface ApplicationEventPublisher {

void publishEvent(ApplicationEvent event);

}

请注意,ApplicationContext接口本身就继承了这个ApplicationEventPublisher接口,意味着所有ApplicationContext的实现类都具有发布事件的能力。我们可以通过一个简单的代码示例来实现事件发布和消费的完整流程,如下所示。

public class EmailEvent extends ApplicationEvent{

}

public class SpringTest {

public static void main(String args[]){

ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");

//创建一个ApplicationEvent对象

EmailEvent event = new EmailEvent(…);

//主动触发该事件

context.publishEvent(event);

}

}

public class EmailNotifier implements ApplicationListener{

public void onApplicationEvent(ApplicationEvent event) {

 //监听EmailEvent事件

if (event instanceof EmailEvent) {

//处理EmailEvent事件

EmailEvent emailEvent = (EmailEvent)event;

} else {

}

}

}

在上述代码示例中,我们使用了一个自定义事件EmailEvent。事实上,Spring框架中已经内置了一批常见的事件,其中最常用的就是ContextRefreshedEvent。顾名思义,每当ApplicationContext被刷新时,Spring就会发布ContextRefreshedEvent事件。

我们在AbstractApplicationContext的refresh方法中看到有一个finishRefresh方法,该方法中包含了如下所示的核心代码。

protected void finishRefresh() {

// 发布ContextRefreshedEvent事件

publishEvent(new ContextRefreshedEvent(this));

}

可以看到这里通过这里publishEvent方法语句发布了一个ContextRefreshedEvent。当然,我们也可以直接使用refresh方法来触发该事件。


ContextRefreshedEvent是一个非常常用和重要的事件,我们在后续介绍Dubbo与Spring框架之间的整合过程时还会提到。

另外诸如ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent等事件会在Spring容器的生命周期处于启动、停止以及关闭阶段自动触发。如果我们对这些事件感兴趣,就可以直接实现ApplicationListener接口从而对具体某一个事件进行响应。例如,如果我们想对ContextStoppedEvent事件进行响应,可以使用如下所示的实现方法。

@Component

public class TestApplicationListener implements ApplicationListener<ContextStoppedEvent>{

    @Override

    public void onApplicationEvent(ContextStoppedEvent contextStoppedEvent) {

        System.out.println(contextStoppedEvent);

    }

}

另外,我们还可以使用一个@EventListener注解来对整个事件监听的开发过程进行简化。使用该注解时,我们可以不需要显式实现ApplicationListener接口就可以消费事件。例如,如下代码完成了与前面代码同样的操作。

@Component

public class TestApplicationListener

@EventListener(classes={ ContextStoppedEvent.class})

public void listen(ContextStoppedEvent contextStoppedEvent){

System.out.println(contextStoppedEvent);

}

}

ApplicationListener实现原理

介绍完Spring事件发布和监听的基本流程之后,我们来进一步剖析它的实现原理。前面提到,ApplicationContext接口本身就继承了这个ApplicationEventPublisher接口,因此在AbstractApplicationContext中我们就会发现如下所示的publishEvent方法。

public void publishEvent(ApplicationEvent event) {

getApplicationEventMulticaster().multicastEvent(event);

if (this.parent != null) {

this.parent.publishEvent(event);

}

}

显然,对于一个事件而言,可以存在多个消费者。所以上述代码中首先获取了Spring中的事件多播器ApplicationEventMulticaster,并调用它的事件多播方法multicastEvent实现事件的多播发送。我们不难明白在ApplicationEventMulticaster中维护着一个ApplicationListener列表,并能实现对这些ApplicationListener发送事件。


SimpleApplicationEventMulticaster是ApplicationEventMulticaster的最终实现类,它的multicastEvent方法实现如下所示。

public void multicastEvent(final ApplicationEvent event) {

for (final ApplicationListener listener : getApplicationListeners(event)) {

//获取线程执行器

Executor executor = getTaskExecutor();

if (executor != null) {

//执行监听线程

executor.execute(new Runnable() {

public void run() { listener.onApplicationEvent(event);

}

});

}

else {

listener.onApplicationEvent(event);

}

}

}

这里通过JDK提供的Executor为每个ApplicationListener启动一个独立的线程,并在该线程中回调了onApplicationEvent方法。

我们已经明白了事件发布的独立过程,现在回到Spring容器来获取用于事件发布的ApplicationEventMulticaster以及各个用于事件监听的ApplicationListener的具体方式。让我们再次来到已经多次强调的AbstractApplicationContext中的refresh方法,这其中有两个步骤与事件相关,即initApplicationEventMulticaster和registerListeners。

public void refresh() throws BeansException, IllegalStateException {

try {

              ...

// 初始化自定义事件广播器

initApplicationEventMulticaster();

// 执行刷新

onRefresh();

// 注册监听器

registerListeners();

...

}

}

initApplicationEventMulticaster方法比较简单,先判断容器中是否实现了一个自定义的ApplicationEventMulticaster,如果有就直接使用;反之就会创建一个新的SimpleApplicationEventMulticaster。

另一个registerListeners方法则获取当前上下文中的所有ApplicationListener并添加到ApplicationEventMulticaster中。那么这些ApplicationListener是什么时候添加到当前上下文中的呢?显然,最合理的时机就是在Bean初始化之后。

至此,整个Spring的事件发布和监听机制已经介绍完毕。作为总结,我们可以通过一张图描述整个过程中所涉及的类层关系。


应该说,Spring中的事件处理机制虽然有点复杂,但功能丰富,架构设计也很优雅。上图中所展示的类层结构为开发人员自己实现一套自定义的事件驱动架构提供了很好的参考价值。

Spring事件处理机制在Dubbo框架中的应用

现在,我们已经理解了Spring中如何实现事件发布和监听的过程,接下来让我们看看它在开源框架中的实际应用。今天我们讨论框架仍然是Dubbo。

我们来到Dubbo服务端的启动类ServiceBean,该类的定义如下所示。

public class ServiceBean<T> extends ServiceConfig<T> implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, BeanNameAware

显然,ServiceBean实现了Spring的ApplicationListener等接口,该接口的onApplicationEvent方法如下所示。代码X。

public void onApplicationEvent(ContextRefreshedEvent event) {

        if (isDelay() && !isExported() && !isUnexported()) {           

            export();

        }

}

可以看到Dubbo的ServiceBean类从ApplicationContext监听ContextRefreshedEvent事件。也就是说,只要Spring容器发布了ContextRefreshedEvent事件,Dubbo接收到该事件之后就会执行export方法,这个export方法会完成Dubbo服务的对外发布。

那么,Spring在什么时候会发布ContextRefreshedEvent事件呢?这个问题的答案,实际上在我们前面的讨论中已经明确了。一旦AbstractApplicationContext的refresh方法被调用,ContextRefreshedEvent事件就会被发布。作为总结,我们可以得到如下所示的执行流程图。


可以看到,Dubbo框架正是基于Spring框架所提供的事件发布和监听机制,完成了与Spring框架的无缝整合。

通过本讲内容的学习,我们掌握了Spring中事件处理的实现过程,这也为实现自定义的事件驱动架构提供了很好的基础,很多实现上的细节可供我们参考。在日常开发过程中,事件处理是一个常见的需求。在分布式环境下,我们可以通过消息中间件完成这个操作。而在单体系统内部,基于对本文中ApplicationEventPublisher和ApplicationListener这两个核心接口及其各个实现类的理解,我们可以抽象出更加轻量级的、符合具体业务场景的事件处理机制。

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

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

相关文章

[图解]企业应用架构模式2024新译本讲解05-表模块2

1 00:00:00,970 --> 00:00:03,440 接下来&#xff0c;我们就来看案例了 2 00:00:03,690 --> 00:00:09,260 案例跟上一次事务脚本案例是一样的 3 00:00:11,210 --> 00:00:13,130 也是收入确认这个案例 4 00:00:14,460 --> 00:00:15,580 这是表结构 5 00:00:15,7…

WHAT - Typescript 中 structural-type-system 结构类型系统

目录 一、结构类型系统与名义类型系统二、结构类型系统的基本概念三、泛型和结构类型系统四、类型别名和接口的兼容性五、总结 一、结构类型系统与名义类型系统 TypeScript 中的结构类型系统&#xff08;Structural Type System&#xff09;是 TypeScript 的核心特性之一&…

解决uniapp里的onNavigationBarSearchInputClicked不生效

如何在uniapp里使用onNavigationBarSearchInputClicked。 1、在page.json里配置 "pages": [{"path": "pages/index/index","style": {"navigationBarTitleText": "首页","navigationStyle": "cu…

【Android】手动下载gradle插件包,解决gradle插件包下载不全问题。

问题描述 拉取别人的项目时&#xff0c;因为网络问题gradle插件包一直下载不全&#xff0c;一直build。 解决方案&#xff1a; 打开gradle>wrapper文件下gradle-wrapper.properties&#xff0c;查看需要下载gradle-7.2-bin.zip。 distributionBaseGRADLE_USER_HOME distr…

cmd窗口输出内容乱码问题

出现这样的问题是因为编码格式和解码格式不一样导致的&#xff0c;cmd窗口的默认解码格式为GBK&#xff0c;如想修改cmd默认编码格式可以按照下面步骤操作&#xff1a;打开cmd窗口输入&#xff1a;chcp 65001 65001指的是utf-8编码如果不清楚编码对应的 页面编码是是多少&#…

国内外知名的低代码开发平台下载地址

以下是国内外几款低代码开发平台的列表&#xff0c;包含了下载地址、适应操作系统、是否可以独立部署、优点、缺点以及是否包含流程引擎的信息。 平台名称 下载地址 适应操作系统 是否可以独立部署 优点 缺点 是否包含流程引擎 国内平台 阿里云宜搭 阿里云官网 跨平台…

文件批量重新命名

实用 如果你想忽略原文件的后缀&#xff0c;将所有文件&#xff08;除了 .py 文件&#xff09;都重命名为以 .jpg 结尾&#xff0c;并且使用序号来命名&#xff0c;你可以使用以下脚本&#xff1a; # renamefile.py import os# 定义原始目录和起始序号 original_directory &q…

C++操纵符用法

C中的操纵符&#xff08;Manipulators&#xff09;是用于格式化输入输出的特殊工具。它们可以在输出流中控制各种格式&#xff0c;如设置字段宽度、精度、填充字符等。以下是一些常用的操纵符及其用法&#xff1a; setw(int width): 设置字段宽度为width个字符。 cout <<…

访问github加速方法续集dev-sidecar

访问github加速方法续集dev-sidecar dev-sidecar 重要提醒 ------------------------------重要提醒1--------------------------------- 注意&#xff1a;由于electron无法监听windows的关机事件&#xff0c;开着ds情况下直接重启电脑&#xff0c;会导致无法上网&#xff0c…

【网络层】IP地址基础 与 子网掩码

文章目录 IP地址基础IP地址概念IP地址分类公网地址和私网地址 子网掩码子网掩码作用默认子网掩码网络地址、主机地址、广播地址 IP地址基础 IP地址概念 IP地址&#xff1a;IP Address 在网络中&#xff0c;通信节点都需要有一个IP地址 IP地址以点分十进制表示&#xff0c;有…

建设人工智能平台,主流GPU卡选型分析

国内外主流GPU卡性能分析&#xff01;2024&#xff01; 大模型兴起助推算力需求激增 2024年&#xff0c;深度学习与人工智能技术飞速跃进&#xff0c;Transformer、GPT-3等大模型在自然语言处理、图像识别、语音合成等领域大放异彩&#xff0c;开启AI新纪元。其庞大的参数与数…

Matlab操作Excel筛选指定数据的对应数据

Matlab中在表格中寻找指定汉字&#xff0c;并返回其所在行数&#xff0c; 将该行数的另一列提取出来。 目录 一、前言 二、直接在命令行输出 三、保存筛选数据excel 一、前言 源数据excel&#xff1a; 指定汉子&#xff1a;买&#xff0c;得到下面数据&#xff1a; 二、直接…

微信小程序埋点监听方案

场景&#xff1a;运营小程序&#xff0c;需要根据系统访问情况来做决策时。 后台&#xff1a;开发可配置监听页面路径&#xff0c;参数&#xff0c;事件名称等&#xff0c;类似以下格式&#xff0c;进行json保存。 组装数据接口返回给前端缓存到本地&#xff0c;然后进行校验编…

2024华为OD机试真题-机器人搬砖-C++(C卷D卷)

题目描述 机器人搬砖,一共有N堆砖存放在N个不同的仓库中,第i堆砖中有bricks[i]块砖头, 要求在8小时内搬完。机器人每小时能搬砖的数量取决于有多少能量格, 机器人一个小时中只能在一个仓库中搬砖,机器人的能量格每小时补充一次且能量格只在这一个小时有效,为使得机器人损…

Vue3 自定义Hooks函数的封装

1、如何理解vue3中的自定义hooks 在Vue 3中&#xff0c;自定义hooks允许开发者封装和重用逻辑代码。自定义hooks是使用Composition API时创建的函数&#xff0c;这些函数可以包含任意的组合逻辑&#xff0c;并且可以在多个组件之间共享。 自定义hooks通常遵循这样的命名约定&…

MyBatis延迟加载缓存分页逆向工程

文章目录 延迟加载概述步骤 缓存一级缓存介绍原理 二级缓存介绍 设置缓存对象策略原理开启步骤属性解释是否使用一级缓存 分页插件使用步骤 逆向工程介绍搭建使用增删修改查 延迟加载 概述 延迟加载本身是依赖于多表查询的 延迟加载中返回值要选择resultMap返回的结果一定是D…

数学建模 —— 插值与拟合(1)

一、matlab画图 1.1 plot&#xff08;二维图形&#xff09; plot(x) —— 缺省自变量绘图格式 plot(x,y) —— 基本格式&#xff0c;以y(x)的函数关系作出直角坐标图&#xff0c;如果y为nm的矩阵&#xff0c;则以x为自变量&#xff0c;作出m条曲线 plot(x1,y1,x2,y2,…,xn,…

python循环引用和解决方法

目录 1. 延迟导入 2. 使用 importlib 3. 重构代码 4. 使用类型提示的前向引用 在Python中&#xff0c;两个文件循环引用的问题通常发生在模块相互依赖导致的导入循环。这种情况下&#xff0c;解决循环引用的方法有几种&#xff0c;以下是一些常见的解决方案&#xff1a; 1…

神经网络算法详解与前沿探索

神经网络算法详解与前沿探索 随着人工智能技术的迅猛发展&#xff0c;神经网络成为机器学习领域的重要组成部分&#xff0c;广泛应用于图像识别、自然语言处理和推荐系统等。本文将详细探讨神经网络的基本原理、结构、训练过程及其应用实例&#xff0c;并扩展至更多相关领域和…

基于标准库的STM32的外部中断EXTI

毕设已经告一段落了&#xff0c;接下来准备开始整理一下毕设中用到的知识与技术细节&#xff0c;今天整理的是STM32从编码器获取数据的方式-----外部中断&#xff08;EXTI&#xff09;&#xff1a; 外部中断分为四个硬件相关外设&#xff0c;GPIO/AFIO/EXTI/NVIC&#xff08;E…