技术派Spring事件监听机制及原理
Spring事件监听机制是Spring框架中的一种重要技术,允许组件之间进行松耦合通信。通过使用事件监听机制,应用程序的各个组件可以在其他组件不直接引用的情况下,相互发送和接受消息。
需求
在技术派中有这样一个需求,当发布文章或者文章下线时,会发布一个事件给SiteMap(站点地图,帮助搜索引擎有效的抓取和索引网站),SiteMap监听到该事件后会进行更新。
事件监听的本质是观察者模式的应用,包括事件、事件监听器、事件发布器等主要组件。
事件:一个实现了ApplicationEvent类的对象,代表了应用程序中某个特定的事件。我们可以根据需要创建自定义事件,只要继承ApplicationEvent类并添相关的属性和方法就可以了。
事件监听器:实现了ApplicationListener<E>接口的对象,其中E表示事件监听器需要处理的事件类型。监听器可以通过onApplicationEvent(E event)方法处理接受到的事件,另外也可以使用@EventListener注解来简化事件监听器的实现,技术派正是采用的这种方式。
事件发布器:事件发布器负责将事件发布给所有关注该事件的监听器,在Spring中,ApplicationEventPublisher接口定义了事件发布的基本功能,而ApplicationEventPublisherAware接口允许组件获取到事件发布器的引用。Spring的核心容器ApplicationContext实现了ApplicationEventPublisher接口,因此在Spring应用中,通常直接使用ApplicationContext作为事件发布器,技术派采用该方式。
实例
第一步(事件)
创建自定义事件ArticleMsgEvent,继承ApplicationEvent。
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = true)
public class ArticleMsgEvent<T> extends ApplicationEvent {private ArticleEventEnum type;private T content;public ArticleMsgEvent(Object source, ArticleEventEnum type, T content) {super(source);this.type = type;this.content = content;}
}
类上的四个注解为lombok提供。两个字段,
type:枚举类型(ArticleEventEnum),代表事件的类型。
表示文章上线或者下线。
content:泛型(T),表示事件的内容,在本例中,我们会传一个文章的ID。
source:在构造方法里卖我们还会传一个Object类型的数据,表示事件的来源,也就是事件的发布者。
ApplicationEvent:是Spring Framework框架中用于定义事件的基类。
第二步(发布事件)
定义SpringUtil工具类,实现了ApplicationContexAware
@Component
public class SpringUtil implements ApplicationContextAware {private static ApplicationContext context;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {SpringUtil.context = applicationContext;}/*** 发布事件消息** @param event*/public static void publishEvent(ApplicationEvent event) {context.publishEvent(event);}
通过实现ApplicationContextAware接口,可以让这个类在Spring容器启动时自动获得ApplicationContext引用。(作为事件发布器)@Component可以让该类被Spring容器自动实例化和管理。
自动装配过程是通过Spring得ApplicationContextAwareProcessor类实现得,它是一个后置处理器。在Spring容器初始化时,他会检查所有得Bean,如果Bean实现了ApplicationContextAware接口。他会调用setApplicationContext方法将ApplicationContext的引用传递给Bean。
第三步(用事件)
通过调用SpringUtil.publishEvent()发布事件。在ArticleSettingServiceImpl类中。
@Override
public void updateArticle(ArticlePostReq req) {
ArticleDO article = articleDao.getById(req.getArticleId());
if (article == null) {return;
}if (StringUtils.isNotBlank(req.getTitle())) {article.setTitle(req.getTitle());
}
article.setShortTitle(req.getShortTitle());ArticleEventEnum operateEvent = null;
if (req.getStatus() != null) {article.setStatus(req.getStatus());if (req.getStatus() == PushStatusEnum.OFFLINE.getCode()) {operateEvent = ArticleEventEnum.OFFLINE;} else if (req.getStatus() == PushStatusEnum.REVIEW.getCode()) {operateEvent = ArticleEventEnum.REVIEW;} else if (req.getStatus() == PushStatusEnum.ONLINE.getCode()) {operateEvent = ArticleEventEnum.ONLINE;}// switch (req.getStatus()){// case 0 :// operateEvent = ArticleEventEnum.OFFLINE;// break;// case 3 :// operateEvent = ArticleEventEnum.REVIEW;// break;// case 2 :// operateEvent = ArticleEventEnum.ONLINE;// break;// default:// break;// }}
articleDao.updateById(article);if (operateEvent != null) {// 发布文章待审核、上线、下线事件SpringUtil.publishEvent(new ArticleMsgEvent<>(this, operateEvent, article.getId()));
}
}
第四步(监听并处理事件)
通过 @EventListener注解来处理事件,在SitemapServiceImpl类中可以看到。
/*** 基于文章的上下线,自动更新站点地图** @param event*/
@EventListener(ArticleMsgEvent.class)
public void autoUpdateSiteMap(ArticleMsgEvent<Long> event) {ArticleEventEnum type = event.getType();if (type == ArticleEventEnum.ONLINE) {addArticle(event.getContent());} else if (type == ArticleEventEnum.OFFLINE || type == ArticleEventEnum.DELETE) {rmArticle(event.getContent());}
}public void addArticle(Long articleId) {
RedisClient.hSet(SITE_MAP_CACHE_KEY, String.valueOf(articleId), System.currentTimeMillis());
}public void rmArticle(Long articleId) {RedisClient.hDel(SITE_MAP_CACHE_KEY, String.valueOf(articleId));
}
当ArticleMsgEvent类型的事件被发布时,此方法自动被触发,在该方法中,首先获得事件的类型(ArticleEventEnum枚举值),然后根据事件类型执行相应的操作,上线时将文章添加到SiteMap,下线时从SiteMap中删除。
测试
这个就时技术派中的事件监听机制了。
启动Redis,启动服务端,启动admin端,在后端找一篇文章下线文章。
就可以在debug模式下看到事件触发了。
原理分析
Spring事件监听机制涉及到s四个主要的类:
事件对象:ApplicationEvent
事件监听器:ApplicationLisener,事件监听器,可以通过@EventListener注解定义事件处理方法,而无需实现ApplicationListener接口
事件发布者:ApplicationEventPublisher,在Spring中可以通过ApplicationEventPublisherAware接口或使用@Autowired注解来注入ApplicationEventPublisher实例,当事件被发布时,Spring会自动调用已注册的ApplicationListener实现类得onApplicationEvent()方法。
事件管理者:ApplicationEventMulticaster,管理监听器和发布事件,通常由SimpleApplicationEventMulticaster类实现。他会遍历所有已经注册的监听器,并调用他们的onApplicationEvent()方法。
ApplicationEvent
ApplicationEvent继承了EventObject对象。
来看看ApplicationEvent的子类关系图
ApplicationEvent 有一个重要的子类 ApplicationContextEvent,而ApplicationContextEvent 又有 4 个重要的子类:
ContextStartedEvent:当 Spring 容器启动时触发该事件。这意味着所有 Bean 都已加载,并且 ApplicationContext 已初始化。
ContextStoppedEvent:当 Spring 容器停止时触发该事件。当容器关闭并停止处理请求时,通常会触发此事件。
ContextRefreshedEvent:当 ApplicationContext 刷新时触发该事件。这表示所有Bean 都已创建,并且已初始化所有单例 Bean(前提是它们在容器初始化时需要初始化)
ContextClosedEvent:当 Spring 容器关闭时触发该事件。这表示所有 Bean 都已销塾Spring 容器已清理资源并停止,
ApplicationListener
ApplicationListener继承EventListener接口,并要求实现onApplicationEvent(E event)方法。
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {void onApplicationEvent(E event);
}
onApplicationEvent(E event)方法:当发布某个事件时,所有注册的ApplicationListener 实例的 onApplicationEvent 方法都会被调用。在这个方法中,可以编写处理特定事件的逻辑。此方法接收一个类型为E的参数,这是 ApplicationEvent 的子类表示触发的事件。
当 Spring 应用启动时,Spring 会扫描所有的 Bean,寻找使用了 @EventListener 注解的方法。一旦找到这样的方法,Spring 会为这些方法创建 ApplicationListener 实例并将其注册到 ApplicationEventMulticaster。
ApplicationEventMulticaster
ApplicationEventMulticaster 是一个接口,负责管理监听器和发布事件,包含了注册监听器、移除监听器以及发布事件的方法。
Spring 容器中通常会有一个默认的实现,如 SimpleApplicationEventMulticaster,继承了AbstractApplicationEventMulticaster.
AbstractApplicationEventMulticaster 主要实现了管理监听器的方法(上面接口的前 5 个方法),比如说 addApplicationListener。
public void addApplicationListener(ApplicationListener<?> listener) {Assert.notNull(listener, "ApplicationListener must not be null");if (this.applicationEventMulticaster != null) {this.applicationEventMulticaster.addApplicationListener(listener);}this.applicationListeners.add(listener);
}
最核心的一句代码: this.defaultRetriever.applicationListeners.add(listener);,其内部类 DefaultListenerRetriever 里面有两个集合,用来记录维护事件监听器
这就和设计模式中的发布订阅模式一样了,维护一个 List,用来管理所有的订阅者,当发布者发布消息时,遍历对应的订阅者列表,执行各自的回调 handler。
再来看 SimpleApplicationEventMulticaster 类实现的广播事件逻辑
multicastEvent 的主要作用是将给定的 ApplicationEvent 广播给所有匹配的监听器
首先,通过检査 eventType 参数是否为 nul 来确定事件类型。如果 eventType 为null,则使用 resolveDefaultEventType(event)方法从事件对象本身解析事件类型
获取 Executor,它是一个可选的任务执行器,用于在异步执行监听器时调用。如果没有配置 Executor,则默认为 nul,表示使用同步执行。
使用 getApplicationListeners(event,type)方法获取所有匹配给定事件类型的监听器。
对于每个匹配的监听器,检查是否有 Executor 配置。如果存在 Executor,则使用executor.execute()方法将监听器的调用封装到一个异步任务中。如果没有配置Executor,则直接同步调用监听器。
使用 invokeListener(listener,event)方法调用监听器的 onApplicationEvent方法,将事件传递给监听器。
通过这个实现,SimpleApplicationEventMulticaster 可以将事件广播给所有关心该事件的监听器,同时支持同步和异步执行模式。
最后调用 istener.onApplicationEvent(event);也就是我们通过实现接口ApplicationListener 的方式来实现监听器的 onApplicationEvent 实现逻辑。
ApplicationEventPublisher
ApplicationEventPublisher 是一个接口,用于将事件发布给所有感兴趣的监听器。
这个接口的实现类通常会将事件委托给
ApplicationEventMulticaster。在 Spring 中ApplicationContext 通常充当事件发布者,它就实现了 ApplicationEventPublisher 接口。
ApplicationContext 的 publishEvent 方法的逻辑实现主要在类AbstractApplicationContext 中:
这段代码的主要逻辑在这:
这段代码的主要作用是在 ApplicationContext 初始化时处理应用程序事件的发布。当ApplicationContext 还没有完全初始化时,例如在refresh()方法中earlyApplicationEvents 列表会被用来保存早期的事件。在这个阶段ApplicationEventMulticaster 还没有完全配置好,因此无法直接发布事件。这些早期的事件将在 ApplicationContext 初始化完成后,ApplicationEventMulticaster 配置好后,通过 finishRefresh()方法中的 publishEvent(new ContextRefreshedEvent(this));发布。
当 ApplicationContext 初始化完成后,earlyApplicationEvents 列表将被设置为 null。此时,事件可以直接通过 getApplicationEventMulticaster().multicastEvent(applicationEvent,eventType)方法发布给所有匹配的监听器,
这个机制确保了在 ApplicationContext 初始化过程中产生的事件不会丢失,而是在ApplicationContext 初始化完成后被正确地发布给所有感兴趣的监听器。
总结
这篇内容通过源码的形式讲解了 Spring 事件监听机制及其原理。
相关文章:

技术派Spring事件监听机制及原理
Spring事件监听机制是Spring框架中的一种重要技术,允许组件之间进行松耦合通信。通过使用事件监听机制,应用程序的各个组件可以在其他组件不直接引用的情况下,相互发送和接受消息。 需求 在技术派中有这样一个需求,当发布文章或…...

秋招突击——设计模式补充——简单工厂模式和策略模式
文章目录 引言正文简单工厂模式策略模式策略模式和工厂模式的结合策略模式解析 总结 引言 一个一个来吧,面试腾讯的时候,问了我单例模式相关的东西,自己这方面的东西,还没有看过。这里需要需要补充一下。但是设计模式有很多&…...

SwiftUI中List的liststyle样式及使用详解添加、移动、删除、自定义滑动
SwiftUI中的List可是个好东西,它用于显示可滚动列表的视图容器,类似于UITableView。在List中可以显示静态或动态的数据,并支持垂直滚动。List是一个数据驱动的视图,当数据发生变化时,列表会自动更新。针对List…...

PostgreSQL的系统视图pg_stats
PostgreSQL的系统视图pg_stats pg_stats 是 PostgreSQL 提供的一种系统视图,用于展示当前数据库中的统计信息。这些统计信息由数据库内部的自动统计过程通过 ANALYZE 命令收集,它们帮助查询规划器做出更好的执行决策,从而优化查询性能。 pg…...

UML2.0-系统架构师(二十四)
1、(重点)系统()在规定时间内和规定条件下能有效实现规定功能的能力。它不仅取决于规定的使用条件等因素,还与设计技术有关。 A可靠性 B可用性 C可测试性 D可理解性 解析: 可靠性:规定时间…...

leetcode 152. 乘积最大子数组「贪心」「动态规划」
152. 乘积最大子数组 题目描述: 给你一个整数数组nums,请你找出数组中乘积最大的非空连续子数组,并返回该子数组所对应的乘积 思路1:贪心 由于 n u m s [ i ] nums[i] nums[i]都是整数,所以多乘一些数肯定不会让绝…...

Android项目目录结构
Android项目目录结构 1. 顶层目录2. 重要的顶层文件和目录3. app模块目录结构4. 重要的**app**模块文件和目录5. 典型的 **build.gradle** 文件内容 典型的Android项目结构的详细介绍。 1. 顶层目录 MyAndroidApp/ ├── .gradle/ ├── .idea/ ├── app/ ├── build/ ├…...

网络安全--计算机网络安全概述
文章目录 网络信息系统安全的目标网络安全的分支举例P2DR模型信息安全模型访问控制的分类多级安全模型 网络信息系统安全的目标 保密性 保证用户信息的保密性,对于非公开的信息,用户无法访问并且无法进行非授权访问,举例子就是:防…...

用requirements.txt配置环境
1. 在anaconda创建环境 创建Python版本为3.8的环境,与yolov5所需的包适配。 2. 在Anaconda Prompt中激活环境 (base) C:\Users\吴伊晴>conda activate yolov5 3. 配置环境 用指定路径中的requirements.txt配置环境。 (yolov5) C:\Users\吴伊晴>pip insta…...

APP渗透-android12夜神模拟器+Burpsuite实现
一、夜神模拟器下载地址:https://www.yeshen.com/ 二、使用openssl转换证书格式 1、首先导出bp证书 2、将cacert.der证书在kali中转换 使用openssl生成pem格式证书,并授予最高权限 openssl x509 -inform der -in cacert.der -out cacert.pem chmod 777 cacert…...

源码扭蛋机开发初探
在软件开发的世界里,创新总是层出不穷。今天,我们将一起探讨一个有趣而富有创意的项目——源码扭蛋机。源码扭蛋机,顾名思义,就是将传统的扭蛋机概念与代码编程相结合,让开发者们在扭动的过程中随机获得各种有趣的、实…...

Patch SCN使用说明---惜分飞
软件说明 该软件是惜分飞(https://www.xifenfei.com)开发,仅用来查看和修改Oracle数据库SCN(System Change Number),主要使用在数据库因为某种原因导致无法正常启动的情况下使用该工具进行解决.特别是Oracle新版本中使用隐含参数,event,orad…...

【微服务架构的守护神】Eureka与服务熔断深度解析
标题:【微服务架构的守护神】Eureka与服务熔断深度解析 在微服务架构中,服务的数量众多,网络请求的复杂性也随之增加,这使得系统的稳定性面临挑战。服务熔断作为一种保护机制,能够在服务出现问题时及时切断请求&#…...

使用label-studio对OCR数据进行预标注
导读 label-studio作为一款数据标注工具相信大家都不陌生,对于需要进行web数据标注协同来说应该是必备工具了,标注的数据类型很全涉及AI的各个任务(图像、语音、NLP、视频等),还支持自定义涉及模版。 然而,我们在标注数据的过程…...

嵌入式linux sqlite3读写demo
以下是一个简单的C语言程序,使用SQLite数据库进行读写操作的示例。请确保您已经安装了SQLite3库。 #include <stdio.h> #include <stdlib.h> #include <sqlite3.h> static int callback(void *NotUsed, int argc, char **argv, char **azColNam…...

vue实现搜索文章关键字,滑到指定位置并且高亮
1、输入搜索条件,点击搜索按钮 2、滑到定位到指定的搜索条件。 <template><div><div class"search_form"><el-inputv-model"searchVal"placeholder"请输入关键字查询"clearablesize"small"style&quo…...

Stable Diffusion与AI艺术:探索人工智能的创造力
引言 随着人工智能(AI)技术的迅猛发展,AI艺术逐渐走进了公众视野。尤其是近年来,Stable Diffusion等技术的出现,显著提升了AI在艺术创作领域的表现力和创造力。这篇文章将深入探讨Stable Diffusion技术的工作原理、应…...

华为HCIP Datacom H12-821 卷26
1.单选题 在VRRP中,同一备份组的设备在进行VRRP报文认证时,以下哪一参数不会影响Master设备和Backup设备认证协商结果 A、认证字 B、优先级 C、认证方式 D、VRRP版本 正确答案: B 解析: 优先级只会影响谁是主谁是备&…...

golang 获取系统的主机 CPU 内存 磁盘等信息
golang 获取系统的主机 CPU 内存 磁盘等信息 要求 需要go1.18或更高版本 官方地址:https://github.com/shirou/gopsutil 使用 #下载包 go get github.com/shirou/gopsutil/v3/cpu go get github.com/shirou/gopsutil/v3/disk go get github.com/shirou/gopsuti…...

Infinitar链游新发展新机遇
区块链游戏市场在近年来经历了显著增长,吸引了大量的投资和关注。随着加密货币和NFT(非同质化代币)概念的普及,越来越多的投资者、游戏开发者和看到了区块链技术在游戏领域的应用潜力,纷纷涌入市场。区块链游戏的用户量…...

Figma 被爆出它剽窃了苹果的设计后撤下了AI工具Make Designs
Figma是一款流行的界面设计工具,最近它推出了一个名为Make Designs的新功能,这个功能利用人工智能帮助用户快速设计应用程序界面。但是,这个工具生成的设计竟然和苹果公司的iOS天气应用非常相似,这让外界怀疑Figma是否剽窃了苹果的…...

ERROR | Web server failed to start. Port 8080 was already in use.
错误提示: *************************** APPLICATION FAILED TO START ***************************Description:Web server failed to start. Port 8080 was already in use.Action:Identify and stop the process thats listening on port 8080 or configure thi…...

C++ 类和对象 构造函数
一 类的6个默认成员函数: 如果一个类中什么成员都没有,简称为空类。 例: #include <iostream> class Empty {// 空类,什么成员都没有 }; 空类中真的什么都没有吗?并不是,任何类在什么都不写时&a…...

纯javascript实现图片批量压缩打包zip下载后端ThinkPHP多国语言切换国际站
最近在做一个多国语言的工具站,需要实现多国语言切换,说到多国语言站,肯定是有2种方式,第一是子域名,第二就是子目录。根据自己的需要来确定。 后台配置如下: 前台显示: 前端纯javascript实现…...

使用ChatGPT写论文,只需四步突破论文写作瓶颈!
欢迎关注,为大家带来最酷最有效的智能AI学术科研写作攻略。关于使用ChatGPT等AI学术科研的相关问题可以和作者七哥(yida985)交流 地表最强大的高级学术AI专业版已经开放,拥有全球领先的GPT学术科研应用,有兴趣的朋友可…...

神领物流项目第一天
文章目录 聚焦快递领域首先第一个是验证码模块流程登录接口权限管家 聚焦快递领域 首先第一个是验证码模块流程 首先生成验证码的流程 可以使用工具类去生成验证码 LineCaptcha lineCaptcha CaptchaUtil.createLineCaptcha(160, 60, 4, 26);// 获取值然后存入redis中 strin…...

[作业]10 枚举-排列类
作业: 已做: #include <iostream> using namespace std; int n; int a[100]; void func(int ,int); int main(){cin>>n;func(0,n);return 0; } void func(int k,int m){if(k>m-1){for(int i0;i<m;i){cout<<a[i];}cout<<en…...

vue2(vue-cli3x[vue.config.js])使用cesium新版(1.117.0)配置过程
看来很多解决方法都没有办法,最后终于。呜呜呜呜 这里我用的是vue-cli去搭建的项目的vue2 项目,其实不建议用vue2搭配cesium。因为目前cesium停止了对vue2的版本更新,现在默认安装都是vue3版本,因此需要控制版本,否则…...

【深度学习】常用命令行指令汇总
这些指令对于管理深度学习环境、监控资源使用、调试程序等方面 查看显卡使用情况 要实时监控NVIDIA显卡的状态,可以使用命令: nvidia-smi -l 1这条命令会每秒刷新一次显卡的使用情况,包括GPU利用率、显存使用情况等。 查看当前Python环境 查看当前使用的Python环境,可…...

谷粒商城学习-11-docker安装redis
文章目录 一,拉取Redis镜像1,搜索Redis的Docker镜像2,拉取Redis镜像3,查看已经拉取的镜像 二,创建、启动Redis容器1,创建redis配置文件2,创建及运行Redis容器3,使用docker ps查看运行…...