侧边栏壁纸
  • 累计撰写 15 篇文章
  • 累计创建 8 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

SpringEvent事件监听机制

王富贵
2024-04-10 / 0 评论 / 1 点赞 / 58 阅读 / 0 字

1、SpringEvent事件监听机制

参考

CSDN-Spring中事件监听(通知)机制详解与实践【含原理】

掘金-TransactionalEventListener使用场景以及实现原理,最后要躲个大坑

demo项目

hy-springEvent-demo【springEventdemo项目】

数据库在项目目录的 datebase 里面

SpringEvent用于对事件的监听与处理,他是对设计模式之发布订阅(观察者)模式的实现。

当某个事件发布之后,就会被广播出去,当然,与我现在的代码就无关了。除此之外,还会有一个监听器,不断的监听着他所对应的事件,一旦事件被他监听到,他就会接收并进行对应的处理。

显然,这类似于运行在系统内部的消息队列(MQ),这样子的好处就是将代码解耦,让我们不再关心事件发布出去后进行的任何操作。

1.1、三要素

在事件发布中,有三大要素

  1. 事件(事件对应MQ中的消息)
  2. 发布(发布对应MQ中的生产者发送消息)
  3. 监听(监听对应MQ中的消费者接收消息)

1.1.1、事件

springEvent中的事件继承自 EventObject 类,这个类维护了一个对象 Object source ,而我们通常自定义事件应该继承自抽象类 ApplicationEvent 对象。

简单来说,事件等同于消息队列的消息体,而事件采用继承 ApplicationEvent 类来声明。而对象的类型,用于区分不同的事件,也就是说,我们后面创建的监听器是通过监听对象类型来接收具体的事件的。

下面我们可以看到,实际上对 ApplicationEvent 的实现不仅仅是这么多,其他的事件也会被对应的监听器所监听。我们将会了解自定义事件以及事务事件的使用。其他实现类以及事件自行了解。

比如,我们可以自定义事件:

1.我们先来定义事件所需的用户类

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@TableName("user")
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId("id")
    private Long id;

    @TableField("name")
    private String name;
}

2.接着,我们就可以自定义一个事件类了

  1. 继承类 ApplicationEvent对象
  2. 实现所需的构造方法,其中假如我们需要在事件中传递对象,那么你需要创建对象并写在构造函数中
  3. get和set方法,我使用 lombok
@Getter
@Setter
@ToString
public class MyEvent extends ApplicationEvent {
    // 事件内部传递的对象
    private User user;

    public MyEvent(User user) {
        super(user);
        //对象需要在构造函数中赋值
        this.user = user;
    }
}

1.1.2、监听

拥有了对事件的定义之后,我们需要一个消费者,用于监听是否有事件发布,一旦出现事件发布,那么我就接收这个事件,并进行处理。

接口EventListener是一个顶级接口提供给其他监听器接口继承。而针对ApplicationEvent事件而言,spring提供了监听自定义事件 ApplicationEvent 的监听器 ApplicationListener ,如下所示:

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
     // 处理监听到的事件
    void onApplicationEvent(E event);

    static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
        return event -> consumer.accept(event.getPayload());
    }

}

那么,我们有两种方法用于实现 ApplicationListener 用于监听自定义事件

  • 继承 ApplicationListener 进行监听器定义
  • 采用注解 @EventListener 进行监听器定义

1.1.2.1、继承 ApplicationListener

步骤如下

  1. 继承 ApplicationListener,泛型就是你所监听事件的对象类型
  2. 实现 onApplicationEvent 方法,传参就是泛型,用于接收你监听的事件,内部编写逻辑
  3. 不要忘记这个类需要装配进容器,使用 @Component
// 装配进容器
@Component
public class MyExtendsEventListener implements ApplicationListener<MyEvent> /*泛型就是监听的事件(消息)*/{
    //实现onApplicationEvent实现对监听的处理
    @Override
    public void onApplicationEvent(MyEvent event) /*接收的事件(消息)实体,可以使用*/{
        System.out.println("监听到的事件MyEvent的用户名为:"+event.getUser().getName());
    }
}

1.1.2.2、注解 @EventListener

  1. 在需要监听的方法上添加注解 @EventListener
  2. 参数传递为接收的事件
  3. 不要忘记这个类需要装配进容器,使用 @Component

@EventListener 注解如下

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EventListener {
    // 被监听事件的类名,我们重点了解这个
    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    String condition() default "";

    String id() default "";
}

那么如下所示,我们监听我们的事件 MyEvent

@Component
public class MyAnnotationEventListener {
    @EventListener(classes = MyEvent.class)
    public void listenerMyEvent(MyEvent event  /*接收的事件(消息)实体,可以使用*/){
        System.out.println("监听到的事件MyEvent的用户名为:"+event.getUser().getName());
    }
}

1.1.3、事件广播

既然我们定义了事件(消息),定义了监听器(消费者),那么我们需要一个将事件发布出去的动作,广播(生产者)。

而对于广播器而言,发布十分简单

  • 注入广播器 ApplicationEventPublisher 用于广播事件
  • 在服务里面使用广播器的 publishEvent 方法发布事件,请注意事件是你自己定义的类型,对于监听器也是通过对象类型来判断事件类型的。
@Service
public class MyEventServiceImpl implements MyEventService {
    
    // 注入广播器,ApplicationEventPublisher
    @Resource
    private ApplicationEventPublisher applicationEventPublisher;
    
    @Override
    public void TestMyEventService() {
        //使用广播器的publishEvent,发布我们特定的事件
        applicationEventPublisher.publishEvent(new MyEvent(new User(12L,"王富贵")));
        System.out.println("MyEventService publish over");
    }
}

1.2、异步

在三要素中我们了解到了如何解耦进行发布订阅模式的应用,但对于底层而言,他依然是在同一个线程中完成的,也就是说,我们实现了代码解耦但执行顺序依然是依次进行的,因此我们可以开启线程池进行异步使用。

普通事件和异步事件对比:

2

假如我们想要解耦分开执行,请务必使用线程池进行异步。

1.2.1、异步启用

开启监听器异步步骤如下

  1. 使用spring-boot异步配置
    1. 主启动类添加 @EnableAsync
    2. 可以简单配置线程池,这里我们就不配置了,使用默认的
  2. 监听器方法上添加 @Async 异步注解
@Component
public class MyAnnotationEventListener {
    @EventListener(classes = MyEvent.class)
    @Async // 开启异步
    public void listenerMyEvent(MyEvent event){
        System.out.println("监听到的事件MyEvent的用户名为:"+event.getUser().getName());
    }
}

1.3、事务事件监听

我们来试想一个场景,假如用户支付了一个订单,此时后端接收到支付成功的消息之后,就需要把订单支付成功这个消息记录流水表,并进行其他的一些操作,之后会发送或者远程调用其他服务,来告诉他们这个订单支付成功了。此时,我们是必须要使用事务来保证流水表记录成功的。但是,如果把消息发送也耦合在事务当中,如果消息发送失败同样也会回滚流水表的支付,这个时候,业务就不是那么合理,发送支付消息失败并不影响用户支付成功业务,因此流水表是不能够回滚的。

我们再来想象一个场景,在分布式锁的分布式事务场景下,如果出现极端并发,使用声明式事务是否会出现并发安全问题?在分布式锁中,我们使用声明式事务,会出现先解分布式锁,再提交事务的情况,此时解锁意味着其他幂等性线程已经能够进入了,那么在事务还在活跃状态,照成了脏读。这种场景极海大佬是有相关描述的。极海Channel-【架构师】主流幂等设计有大坑?超详细并发事务可见性分析!

那么我们总结一下,上述问题的本质就是耦合在事务中的一些操作,需要在事务提交之后才进行,这个时候他放在了事务提交之前进行,就会出现业务逻辑问题。

常规的解决方案就是使用编程式业务,这个时候就可以编排环绕事务了。除此之外,如果你采用aop编写,那么就需要注意事务aop和环绕的优先级了,一定要把事务aop环绕优先级放在最前面。

除此之外,我们也能够使用spring事务监听器来解决这个问题,spring事务监听器的底层依然是一个优先级比事务环绕低的环绕。这样做的好处就是我们依然可以使用声明式事务来便捷的解决问题。

1.3.1、事务事件监听原理

事务监听器的原理就是对事务进行环绕,并使用一个集合来存储发布的事件,更具配置的事务规则来对事件进行发布

事务规则TransactionPhase有一下几种:

  • AFTER_COMMIT - 默认设置,在事务提交后执行
  • AFTER_ROLLBACK - 在事务回滚后执行
  • AFTER_COMPLETION - 在事务完成后执行(不管是否成功)
  • BEFORE_COMMIT - 在事务提交前执行

1.3.2、使用事务事件监听器

对于事件和广播而言,与普通的事件没有区别,最主要的区别就是对事件监听器的定义。

对事务事件监听器的定义只能由注解 @TransactionalEventListener 定义,注解内容如下:

最主要是为俩个

  1. phase:事务规则,决定了这个事件会在事务出现什么情况下进行监听
  2. classes:监听的事件类型
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {
    // 事务规则,默认为事务提交后执行
   TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

   boolean fallbackExecution() default false;

   @AliasFor(annotation = EventListener.class, attribute = "classes")
   Class<?>[] value() default {};

    // 监听的事件类型
   @AliasFor(annotation = EventListener.class, attribute = "classes")
   Class<?>[] classes() default {};

   @AliasFor(annotation = EventListener.class, attribute = "condition")
   String condition() default "";

   @AliasFor(annotation = EventListener.class, attribute = "id")
   String id() default "";
}

我们简单体验一下定义事务事件监听器,假如我现在想要监听某个事务事件,当他的service的事务提交之后,我才进行监听处理。

@Component
public class TranslationEventListener {
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, classes = TranslationEvent.class)
    public void onUserRegisterEvent(TranslationEvent translationEvent){
        System.out.println(translationEvent.getUser().getName());
    }
}

1.3.3、注意

现在我们做一个总结,如果你遇到这样的业务,操作B需要在操作A事务提交后去执行,那么TransactionalEventListener是一个很好地选择。

异步

一般来说,假如不开启异步,则仅仅是吧业务代码解耦,因此,spring官方是建议把事件广播多线程化的

注意默认的事务传播机制

这里需要特别注意的一个点就是:当B操作有数据改动并持久化时,并希望在A操作的AFTER_COMMIT阶段执行,那么你需要将B事务声明为 PROPAGATION_REQUIRES_NEW 。这是因为A操作的事务提交后,事务资源可能仍然处于激活状态,如果B操作使用默认的 PROPAGATION_REQUIRED 的话,会直接加入到操作A的事务中,但是这时候事务A是不会再提交,结果就是程序写了修改和保存逻辑,但是数据库数据却没有发生变化,解决方案就是要明确的将操作B的事务设为 PROPAGATION_REQUIRES_NEW

默认事务传播行为是存在事务加入一个事务,因此如果我们监听器操作需要操作数据库,请你一定注意他默认的传播行为。

1.4、事件源码分析

我们了解了spring事件监听机制的三要素,就是监听器,事件,和事件广播,

监听器就是用于处理监听到的事件的逻辑,他需要订阅一个事件;事件本质就是一个类,他可以保存和流转一些定义信息;事件广播则将监听器和广播联结起来,他将所有注册到容器中的监听器注册到广播器中,我们使用事件广播将一个事件(类)发布出去时,事件广播会遍历所有的监听器筛选出所有对这个事件感兴趣的监听器并排序(Ordered接口)执行。

性能问题:事件广播本质是侵入性(需要手动发布事件)的增强,由于事件广播会遍历所有的监听器,因此他的性能不如aop。建议编写框架时发布事件提供拓展点,给客户选择性实现事件监听器用于拓展;或者本身我们就是在aop中。

1.5、Spring常见拓展点

Spring 中提供了一些标准事件(内置事件),了解 Spring 中的内置事件,请点击spring官方文档-上下文事件

主要有:

  • 容器刷新事件(ContextRefreshedEvent)
  • 参数准备完毕在容器刷新事件之前(ApplicationPreparedEvent)
  • 容器启动事件(ContextStartedEvent)
  • 容器停止事件(ContextStoppedEvent)
  • 容器关闭事件(ContextClosedEvent)
  • 请求处理事件(RequestHandledEvent)
  • Servlet 请求处理事件(ServletRequestHandledEvent)

我们较常用的为ContextRefreshedEvent事件,触发容器刷新常见为第一次启动初始化容器后,以及程序运行过程中容器bean发生改变。

但改事件仅为springboot事件,在类似于nacos动态配置场景中,并不会触发该事件。我们可以通过监听spring-cloud-context包中的RefreshScopeRefreshedEvent 事件用于处理动态配置后的处理逻辑,注意该逻辑仅在运行时的配置变更触发,容器启动不会触发该事件。

nacos同样使用该事件处理动态配置逻辑,我们可以以此作为拓展点使用。除此之外,你可以搭配ApplicationPreparedEvent事件控制动态配置的完整生命周期。

1

评论区