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 事件发布 的相关文章

随机推荐

  • 【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