Spring 事件发布

2023-11-10

前言

事件发布是 Spring 框架中最容易被忽视的功能之一,但实际上它是一个很有用的功能。使用事件机制可以将同一个应用系统内互相耦合的代码进行解耦,并且可以将事件与 Spring 事务结合起来,实现我们工作中的一些业务需求。和 Spring 的事务类似,Spring 事务有编程式事务、声明式事务,Spring 事件也分为编程式事件和声明式事件,本篇主要讲解使用注解实现声明式事件发布和监听。

Spring 内置事件

稍微熟悉 Spring 的话应该不陌生 Spring 的内置事件,Spring 提供了以下几种内置事件。

Event Explanation
ContextRefreshedEvent 在 ApplicationContext 初始化或刷新时发布
ContextStartedEvent 使用 ConfigurableApplicationContext 接口上的 start() 方法启动 ApplicationContext 时发布
ContextStoppedEvent 在通过使用 ConfigurableApplicationContext 接口上的 stop() 方法停止 ApplicationContext 时发布
ContextClosedEvent 在通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM 关闭挂钩关闭 ApplicationContext 时发布
RequestHandledEvent 一个特定于 Web 的事件,告诉所有 Bean 已为 HTTP 请求提供服务。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序。
ServletRequestHandledEvent RequestHandledEvent 的子类,用于添加特定于 Servlet 的上下文信息

说实话上面是我从官网抄的,因为我本人对于 Spring 源码并不熟悉,我尝试过去看但是......没有坚持下去。略过这些,下面我们将继承 ApplicationEvent实现自定义事件。

事件三要素

大家应该都很熟悉网页点击按钮吧,当我们用鼠标点击某一个按钮,弹出一个提示框,这就是一个完整的事件。这个过程包含了三个要素,通俗的来说就是:

  • 事件源:谁触发了这个事件?(鼠标)
  • 事件:发生了什么?(鼠标点击)
  • 事件监听器:事件发生后要做什么?(弹出一个对话框)

了解了事件的三个要素,下面我们具体来看怎样在 Spring 中使用事件。

同步事件

所谓同步事件就是对于发布的事件并不会新开线程去处理,而是在调用方原来的线程基础上执行业务。比如我们现在有个需求是用户提交订单后插入一条该用户的购买日志。这里事件的三个要素可以理解为:

  • 事件源:OrderService 业务类
  • 事件:下单
  • 监听器:监听下单成功后写日志

OrderService 类中

@Service
@Slf4j
public class OrderService {

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    /**
     * 订单提交
     */
    @Transactional
    public void submit() {
        //...
        log.info("提交订单前....");
        applicationEventPublisher.publishEvent(new OrderCreateEvent(this, "测试对象"));//发布事件
        log.info("提交订单前后....");
    }
}
复制代码

这里的 publishEvent 方法需要传一个继承 ApplicationEvent 的事件类对象

public class OrderCreateEvent extends ApplicationEvent {
    @Getter
    public Object log;

    /**
     * @param log    需要传递的参数
     * @param source 事件源对象
     */
    public OrderCreateEvent(Object source, Object log) {
        super(source);
        this.log = log;
    }
}
复制代码

我们可以把这个事件需要传递的参数信息封装在事件类 OrderCreateEvent 的成员变量里,这里简单写一个 log 属性。

接下来就是要去监听这个事件了,在 OrderLogService 中使用 @EventListener 标注监听方法,默认情况下,它会监听方法形参对象 Class 类型的事件,假如你的监听方法没有形参,那么你应该用 @EventListener 的 classes 属性去指定要监听的事件类型。

@Service
@Slf4j
public class OrderLogService {

    @EventListener
    public void listen(OrderCreateEvent event) {
        log.info("接受到订单创建事件:{}", event.getLog());
        try {
            Thread.sleep(10000);//验证事件的同步
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
复制代码

这里在监听方法中让线程睡 10s 是为了验证事件的同步性(当然你也可以打印出当前线程的 id 来验证)

提交订单前....
接受到订单创建事件:测试对象
提交订单前后....
复制代码

观察控制台日志可以得出结论该事件是同步的,原来的业务必须等待事件处理完才会继续往下执行业务。

异步事件

那么你可能已经发现了上述的实现方案其实不太好,因为创建日志的行为其实不属于订单的提交,也就是说每次订单提交的结果还要等发布的事件执行完才能响应,而发布的事件和本次订单提交的业务没有直接的必须同时成功或者同时失败。假如后续随着业务扩大,再增加订单创建后发短信给用户等其他事件,这样会导致接口响应时间变长,吞吐量下降,这无疑是很影响用户体验的。所以我们可以考虑使用异步事件。

实现异步事件非常简单,只需要在 SpringBoot 启动类上添加 @EnableAsync 启用异步功能,然后监听器方法上添加 @Async 注解即可。

@EventListener
@Async
public void listen(OrderCreateEvent event) {
    //...
}
复制代码

这样监听方法的执行就是异步的,不会影响原来订单提交接口的吞吐量。

将事件绑定事务

那么你可能已经发现了,上述异步事件解决的接口吞吐量问题,但是又带来一个问题。假如订单提交的业务失败了怎么办?因为异步事件监听器抛出 Exception ,是不会将其传播到调用方的。也就是说上述订单提交业务如果报错,那么我们的记录日志事件、发短信事件还是会执行,那么这个问题比接口吞吐量问题要严重的多,所以 Spring 允许我们将事件和事务进行绑定。

使用起来也非常简单,只需要将原来监听器的注解 @EventListener 换成 @TransactionalEventListener 即可。

@TransactionalEventListener
public void listen(OrderCreateEvent event) {
   //...
}
复制代码

我们可以通过该注解的 phase 属性来决定监听器要在原调用方事务的哪个阶段开始执行。总共有四种值

  • TransactionPhase.BEFORE_COMMIT —— 事务提交前
  • TransactionPhase.AFTER_COMMIT —— 事务提交后(默认值)
  • TransactionPhase.AFTER_ROLLBACK —— 事务回滚后
  • TransactionPhase.AFTER_COMOLETION —— 事务完成后(包括事务提交和回滚)

值得注意的是使用 @TransactionalEventListener 的监听器,其事件调用方必须要有事务,否则将不会被执行。 这意味着光有 @Transactional 注解也不行,必须要有数据库相关整合,因为 @Transactional 其实也是通过修改数据库连接的 auto_commit 属性 为 false 来实现事务不自动提交的。

条件事件

@EventListener@TransactionalEventListener 都有 condition 属性,可以用来判断事件的参数满足一定条件的时候执行监听事件。例如:

@EventListener(condition = "event.log == '测试对象2'")
public void listen(OrderCreateEvent event) {
    //...
}
复制代码

如果说发布事件传递的参数值不是该条件中指定的值,那么该监听器也不会执行。

顺序事件

我们可以通过 @Order 注解来控制监听器的执行顺序,该注解的值越小,执行的顺序越靠前。不过在异步事件中不建议使用它来控制顺序,因为那样意义不大。

@EventListener
@Order(1)  //此监听器将会第一个执行
public void listen(OrderCreateEvent event) {
    //...
}

@EventListener
@Order(2) //此监听器将会等待上一个执行完才会执行,
public void listen2(OrderCreateEvent event) {
    //...
}
复制代码

不要在事件监听器中再发布事件

这是一个善意的避免采坑忠告,在已存在的事件监听器(尤其是和事务相绑定的)中发布事件,然后再用监听器监听,可能会导致整个链路的事务出现不符合逾期的结果。比如该回滚的没回滚,改变了事务的传播行为却不生效等问题。这是我曾经踩过的坑,不过话又说回来了,我也不知道我当初怎么会在一个事件监听器中再发布一个事件......

事件和消息队列

也许你已经发现了,Spring 的事件发布和消息队列有很多相似的地方。那么我们来对比下两者的异同

相同点

  • 解耦:将代码中耦合的地方解耦分离
  • 异步:都可以异步执行某一项不属于当前方法业务的事情,提高系统吞吐量

不同点

  • 使用范围:事件只能在应用内使用,无法跨系统,而消息队列可以跨多个系统。
  • 削峰能力:事件的削峰是很局限的,相当于开启多个线程,这样并不好线程越来越多会消耗服务器资源,。消息队列的削峰是引入了第三方中间件,能够有效进行流量削峰,承载高并发请求量。
  • 同样的,事件由于使用范围局限,带来的问题也少,消息队列由于使用范围广,引入的问题也会比较多,要保证消息队列的高可用,解决消息可靠性、幂等性等问题。

总结

Spring 事件发布适用于小型项目,对于高并发流量的业务还是需要专业的消息中间件来支撑。不过通常我们会将 Spring 事件结合消息队列一起用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring 事件发布 的相关文章

  • 过滤两次 Lambda Java

    我有一个清单如下 1 2 3 4 5 6 7 和 预期结果必须是 1 2 3 4 5 6 7 我知道怎么做才能到7点 我的结果 1 2 3 4 5 6 我也想知道如何输入 7 我添加了i gt i objList size 1到我的过滤器
  • jQuery AJAX 调用 Java 方法

    使用 jQuery AJAX 我们可以调用特定的 JAVA 方法 例如从 Action 类 该 Java 方法返回的数据将用于填充一些 HTML 代码 请告诉我是否可以使用 jQuery 轻松完成此操作 就像在 DWR 中一样 此外 对于
  • 将流转换为 IntStream

    我有一种感觉 我在这里错过了一些东西 我发现自己做了以下事情 private static int getHighestValue Map
  • 检测并缩短字符串中的所有网址

    假设我有一条字符串消息 您应该将 file zip 上传到http google com extremelylonglink zip http google com extremelylonglink zip not https stack
  • 帮助将图像从 Servlet 获取到 JSP 页面 [重复]

    这个问题在这里已经有答案了 我目前必须生成一个显示字符串文本的图像 我需要在 Servlet 上制作此图像 然后以某种方式将图像传递到 JSP 页面 以便它可以显示它 我试图避免保存图像 而是以某种方式将图像流式传输到 JSP 自从我开始寻
  • 像 Java 这样的静态类型语言中动态方法解析背后的原因是什么

    我对 Java 中引用变量的动态 静态类型和动态方法解析的概念有点困惑 考虑 public class Types Override public boolean equals Object obj System out println i
  • volatile、final 和synchronized 安全发布的区别

    给定一个带有变量 x 的 A 类 变量 x 在类构造函数中设置 A x 77 我们想将 x 发布到其他线程 考虑以下 3 种变量 x 线程安全 发布的情况 1 x is final 2 x is volatile 3 x 设定为同步块 sy
  • 如何在用户输入数据后重新运行java代码

    嘿 我有一个基本的java 应用程序 显示人们是成年人还是青少年等 我从java开始 在用户输入年龄和字符串后我找不到如何制作它它们被归类为 我希望它重新运行整个过程 以便其他人可以尝试 的节目 我一直在考虑做一个循环 但这对我来说没有用
  • 如何访问JAR文件中的Maven资源? [复制]

    这个问题在这里已经有答案了 我有一个使用 Maven 构建的 Java 应用程序 我有一个资源文件夹com pkg resources 我需要从中访问文件 例如directory txt 我一直在查看各种教程和其他答案 但似乎没有一个对我有
  • Java 和 Python 可以在同一个应用程序中共存吗?

    我需要一个 Java 实例直接从 Python 实例数据存储中获取数据 我不知道这是否可能 数据存储是否透明 唯一 或者每个实例 如果它们确实可以共存 都有其单独的数据存储 总结一下 Java 应用程序如何从 Python 应用程序的数据存
  • logcat 中 mSecurityInputMethodService 为 null

    我写了一点android应显示智能手机当前位置 最后已知位置 的应用程序 尽管我复制了示例代码 并尝试了其他几种解决方案 但似乎每次都有相同的错误 我的应用程序由一个按钮组成 按下按钮应该log经度和纬度 但仅对数 mSecurityInp
  • java for windows 中的文件图标叠加

    我正在尝试像 Tortoise SVN 或 Dropbox 一样在文件和文件夹上实现图标叠加 我在网上查了很多资料 但没有找到Java的解决方案 Can anyone help me with this 很抱歉确认您的担忧 但这无法在 Ja
  • 我如何在java中读取二进制数据文件

    因此 我正在为学校做一个项目 我需要读取二进制数据文件并使用它来生成角色的统计数据 例如力量和智慧 它的设置是让前 8 位组成一个统计数据 我想知道执行此操作的实际语法是什么 是不是就像读文本文件一样 这样 File file new Fi
  • Opencv Java 灰度

    我编写了以下程序 尝试从彩色转换为灰度 Mat newImage Imgcodecs imread q1 jpg Mat image new Mat new Size newImage cols newImage rows CvType C
  • 在java中为组合框分配键

    我想添加一个JComboBox在 Swing 中这很简单 但我想为组合中的每个项目分配值 我有以下代码 JComboBox jc1 new JComboBox jc1 addItem a jc1 addItem b jc1 addItem
  • CamcorderProfile.videoCodec 返回错误值

    根据docs https developer android com reference android media CamcorderProfile html 您可以使用CamcorderProfile获取设备默认视频编解码格式 然后将其
  • 双枢轴快速排序和快速排序有什么区别?

    我以前从未见过双枢轴快速排序 是快速排序的升级版吗 双枢轴快速排序和快速排序有什么区别 我在 Java 文档中找到了这个 排序算法是双枢轴快速排序 作者 弗拉基米尔 雅罗斯拉夫斯基 乔恩 本特利和约书亚 布洛赫 这个算法 在许多数据集上提供
  • 通过浏览器关闭页面时出现 Websocket 错误:“已建立的连接被主机中的软件中止”

    我开发了一个实时通知系统Spring 4 代码可以在 Github 上找到 github com vdenotaris Spring Messaging https github com vdenotaris Spring Messagin
  • 如何防止在Spring Boot单元测试中执行import.sql

    我的类路径中有一个 import sql 文件 其中包含一些 INSERT 语句 当使用 profile devel 运行我的应用程序时 它的数据被加载到 postgres 数据库中 到目前为止一切正常 当使用测试配置文件执行测试时 imp
  • Java中super关键字的范围和使用

    为什么无法使用 super 关键字访问父类变量 使用以下代码 输出为 feline cougar c c class Feline public String type f public Feline System out print fe

随机推荐

  • 【QT学习】01:helloqt

    helloqt OVERVIEW helloqt 一 helloqt 1 使用向导创建 2 手动创建 3 pro文件 4 Qt应用程序框架 二 按钮创建 main cpp mywidget cpp 三 对象模型 1 对象树引入 2 存在的问
  • javascript问答(含答案)

    1 我们可以在下列哪个 HTML 元素中放置 Javascript 代码 您的回答
  • 经典算法问题——稳定匹配(Stable Matching)

    经典算法问题 稳定匹配 Stable Matching 问题起源 在1962年 经济学家 David Gale 和 Lloyd Shapley 提出 能否设计一个高校录取过程 能够自我执行 self enforcing 形成一个最佳的匹配效
  • < element-Ui表格组件:表格多选功能回显勾选时因分页问题,导致无法勾选回显的全部数据 >

    文章目录 前言 一 解决思路 二 实现代码 仅供参考 具体问题具体分析 gt HTML模板 gt Js模板 往期内容 前言 在 Vue elementUi 开发中 elementUI中表格在本身是自带多选功能的 但是在某些情况下 并不能完全
  • some() 方法

    该方法是是数组的Array prototype some array some element index arr 返回值 true 或者false true 至少有一个元素 满足 方法提供的函数判断 false 一个都满足 方法提供的函数
  • 存储器的概述——DRAM动态存储器

    DRAM存储器 1 DRAM存储元的工作原理 SRAM存储器的存储位元是一一个触发器 它具有两个稳定的状态 而DRAM存储器的存储位元是由一个MOS晶体管和电容器组成的记忆电路 如图所示 2 DRAM芯片的逻辑结构 下面我们通过一个例子来看
  • SQLServer 批量修改或插入

    场景 今天在工作中遇到这么一个场景 我需要根据条件对表A做批量更新或插入 因为条件比较复杂 所以我使用了临时表B 先把需要更新或插入的数据查询出来放入临时表 然后更新表A的某字段 更新条件是A id B id 更新效果是若记录存在表A中 则
  • 大规模 Vision-Language 模型预训练的数据增强:Supervision Exists Everywhere

    Supervision Exists Everywhere A Data Efficient Contrastive Language Image Pre training Paradigm 论文地址 代码地址 主要工作 核心思想 具体实现
  • 线程池的声明需要手动进行

    Java 中的 Executors 类定义了一些快捷的工具方法 来帮助我们快速创建线程池 阿里巴巴 Java 开发手册 中提到 禁止使用这些方法来创建线程池 而应该手动 new ThreadPoolExecutor 来创建线程池 这一条规则
  • 行链接和行迁移

    行链接 Row chaining 与行迁移 Row Migration 当一行的数据过长而不能插入一个单个数据块中时 可能发生两种事情 行链接 row chaining 或行迁移 row migration 行链接 当第一次插入行时 由于行
  • fiddler抓包工具入门到入职之如何精准的定位前后端的bug

    Fiddler是一款强大的网络调试工具 可以拦截和分析HTTP请求和响应 帮助开发者定位前后端的问题 下面介绍如何使用Fiddler精准定位前后端的Bug 并使用Python代码进行操作 拦截HTTP请求和响应 打开Fiddler 在 Fi
  • 无法连接虚拟设备 sata0:1,因为主机上没有相对应的设备——解决方案

    其实并不复杂 就两个步骤 你装完虚拟机之后什么也别干 如若开虚拟机或虚拟机里面的系统时 出现标题状况 则可以这样解决 1 点击虚拟机 再点击左侧页面编辑虚拟机设置 2 选择CD DVD IDE 将使用物理驱动器的选项改为下方的使用ISO映像
  • 什么是IDaaS?推荐一款开源的IDaaS产品

    IDaaS是云时代的身份和访问管理 IAM 他们之间的关系 IDaaS SaaS IAM IDaaS是一个云服务平台 客户使用提供IDaaS服务相关的产品 例如单点登录 智能多因素认证 来实现云时代所需的既安全又高效的身份和访问管理功能 一
  • 保持工作稳定情绪与心理健康的八大秘诀

    近期发生的新闻热点再度引发公众对稳定情绪和心理健康的关注 有时候我们遇到的最大的敌人 不是运气也不是能力 而是失控的情绪和口无遮拦的自己 如何在工作中保持稳定的情绪 谈谈你的看法 在工作中保持稳定的情绪对于个人的心理健康和工作效率都至关重要
  • 并行单边jacobi算法 奇偶序列

    单边jacobi算法大家都非常熟悉 就是不停地计算旋转矩阵 简单说就是计算c和s 然后旋转 然而其中做一轮旋转 任何两列都需要旋转一次 需要n n 1 2次单独的旋转 这样的旋转其实是可以并行来实现的 这也就是为何jacobi算法最近比较热
  • SpringDataRedis 使用

    1 SpringDataRedis 特点 2 使用 SpringDataRedis 步骤 3 自定义 RedisTemplate 序列化 4 SpringDataRedis 操作对象 1 SpringDataRedis 特点 提供了对不同
  • C#开发学习~~~Console.WriteLine()

    前言 奥利给 冲冲冲 概述 Console WriteLine 是system名称空间中Console类中的一个方法 用于向控制台写入字符串并换行 其格式项采用如下形式 index alignment formatString index
  • 关于返回值RESULT

    关于返回值RESULT HRESULT Here s the RESULT 值分成32位值 HRESULT值中16到30这15个比特位包含的是设备代码 设备代码标识的是可以返回HRESULT返回代码的操作系统部分 由于Windows操作系统
  • P1089 津津的储蓄计划

    题目描述 津津的零花钱一直都是自己管理 每个月的月初妈妈给津津300300元钱 津津会预算这个月的花销 并且总能做到实际花销和预算的相同 为了让津津学习如何储蓄 妈妈提出 津津可以随时把整百的钱存在她那里 到了年末她会加上20 20 还给津
  • Spring 事件发布

    前言 事件发布是 Spring 框架中最容易被忽视的功能之一 但实际上它是一个很有用的功能 使用事件机制可以将同一个应用系统内互相耦合的代码进行解耦 并且可以将事件与 Spring 事务结合起来 实现我们工作中的一些业务需求 和 Sprin