spring事件监听机制

1,047 阅读8分钟

这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

介绍

spring事件监听机制实际上就是一个典型的观察者模式,在观察者模式的基础之上进行的抽象和处理。使得开发者可以根据自己的业务特点依附于spring容器进行事件的注册、发布、处理。

简单使用

1.创建一个类继承于顶层事件类ApplicationEvent,主要需要创建一个处理业务参数的属性值和这个属性值的构造函数。

public class SendEvent extends ApplicationEvent {

    private SendVo sendVo;

    public SendEvent(SendVo sendVo) {
        this.sendVo = sendVo;
    }
}

2.监听到对应的事件后的业务处理。可以通过SendNotificationEvent对象获取到发起监听时设置的参数,然后再执行具体的监听器业务逻辑。

@Component
public class EventListenerHandler {

    @EventListener
    public void sendEventListener(SendEvent event) {
        // 业务逻辑
    }
}

3.调用点发送具体的事件。调用点通过调用SpringApplicationContext对象进行事件的发布,从而进入Spring的监听机制。

public static void main(String[] args) {
    SendVo sendVo = SendVo.builder()
            .id("123")
            .name("tom").build();
    SendEvent event = new SendEvent(sendVo);
    applicationContext.publishEvent(event);
}

注意:以上就是最简单的spring事件监听的使用。在具体的应用场景中,并不会这么简单的使用,因为若在业务逻辑上需要解耦,大部分还是希望是异步的方式进行事件的处理,然而在默认的情况下,这种模式是同步机制,也就是说待到具体的事件监听处理完成之后,才会继续执行调用点的业务逻辑。

异步方式

1.广播器异步

​ 在spring的事件监听机制中已经考虑到异步的情况,所以在事件发送器发送事件时,会判断是否存在广播器,当存在广播器时,会将具体的监听执行逻辑转移到广播器对应的线程池中。来跟踪一下源码。实际上只有一个接口publishEvent,默认接口中仅是将事件类型都转换为Object对象,由子类进行具体的实现。

@FunctionalInterface
public interface ApplicationEventPublisher {
		
    default void publishEvent(ApplicationEvent event) {
        publishEvent((Object) event);
    }
    
    // 子类实现接口
    void publishEvent(Object event);
}

publishEvent的具体实现在AbstractApplicationContext中,核心逻辑是获取广播器后发送对应的事件。

protected void publishEvent(Object event, ResolvableType eventType) {
    // 省略代码
    if (this.earlyApplicationEvents != null) {
        this.earlyApplicationEvents.add(applicationEvent);
    }else {
        // 获取广播器, 并调用广播器对应的发送事件处理
        getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
    }
}

默认仅有一个广播器的实现SimpleApplicationEventMulticaster,核心处理在这,先通过扩展接口getTaskExecutor()获取对应的的广播器,再获取这个event对应类型的监听器列表,然后进行监听任务的发布,默认是使用同步的方式,可以通过配置对应的广播器来使用线程池方式进行监听任务的发布。

public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        // 若是配置了线程池, 将监听任务转移到线程池执行
        if (executor != null) {
            executor.execute(() -> invokeListener(listener, event));
        }else {
            // 没有配置线程池, 同步方式执行执行监听任务
            invokeListener(listener, event);
        }
    }
}

通过doInvokeListener执行具体的事件监听逻辑,然后通过listener.onApplicationEvent来进行具体的触发逻辑,所以invokeListener方法最后的逻辑是去调用onApplicationEvent方法。

刚刚上面介绍的最简单的使用方式中采用的EventListener的方式来标记监听器的位置,实际上在初始化这个bean对象时,扫描到EventListener后会将这个对应的方式转化为ApplicationListenerMethodAdapter适配器,该类中包含了对象名称、类名称、监听处理方法名称等等,待到接收到事件时,通过反射调用对应的监听处理方法。

2.@Async注解异步

​ 虽然在事件发送器中内置了广播器线程池,但是若不进行配置,则它还是同步的方式执行,在它同步执行的基础上,若是利用spring的异步机制,也可以达到异步的效果。

@Component
public class EventListenerHandler {
    @Async
    @EventListener
    public void sendEventListener(SendEvent event) {
        // 业务处理
    }
}

在这种情况下,在spring容器初始化时,扫描到这个bean对象并进行初始化时,会为这个bean创建一个代理类,由这个代理类来执行相应的异步逻辑。

事务

以上看似已经解决的异步的问题,但是在实际的使用过程中又发现如果事件发送点存在事务管理,就会导致事件中获取不到事件发送点的某些数据。(由于事件监听处理触发时,事件发送点还未提交事务。)

伪代码, 在事件监听的处理中, 通过id=123可能存在获取不到这条数据的情况

public void send() {
    SendVo sendVo = SendVo.builder()
            .id("123")
            .name("tom").build();
    mapper.insert(sendVo);
    SendnEvent event = new SendEvent(sendVo);
    applicationContext.publishEvent(event);
}

但是、但是、但是这种情况spring也考虑到了,spring监听机制中通过使用TransactionalEventListener来解决这个问题。TransactionalEventListener它的元注解为EventListener,所以本质上也是个EventListener注解。

  • phase:事件触发阶段: 比如事务提交之前、事务提交之后等, 默认是在事务提交之后
  • fallbackExecution: 若调用点无事务管理也触发, 默认情况下若调用点无事务接管, 该监听处理不会触发
TransactionPhase phase();

boolean fallbackExecution();

刚刚上面说到在spring扫描到对应的监听器处理bean时,从beanclass对象中找出含有@EventListener注解的方法, 存在map中,同时@TransactionListener方法也会被匹配, 因为它的元注解是@EventListener,所以是根据方法上标记的注解将监听器转换为对应的处理类。根据不同的两个注解@TransactionalEventListenerEventListener对应两个不同的生成监听类工厂DefaultEventListenerFactoryTransactionalEventListenerFactory,由它们来创建具体的监听处理类。 获取监听工厂, 这里有两个工厂:DefaultEventListenerFactoryTransactionalEventListenerFactory。然后判断这个被标记的方法适配哪个工厂,使用工厂创建对应的监听器对象来处理。

private void processBean(final String beanName, final Class<?> targetType) {
    Map<Method, EventListener> annotatedMethods = null;

    if (CollectionUtils.isEmpty(annotatedMethods)) {
        // 省略代码
    }else {
        ConfigurableApplicationContext context = this.applicationContext;
        List<EventListenerFactory> factories = this.eventListenerFactories;
        for (Method method : annotatedMethods.keySet()) {
            for (EventListenerFactory factory : factories) {
                if (factory.supportsMethod(method)) {
                    Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));
                    ApplicationListener<?> applicationListener = factory.createApplicationListener(beanName, targetType, methodToUse);
                    context.addApplicationListener(applicationListener);
                    break;
                }
            }
        }
    }
}

而这两个工厂生成出来的监听类,实际上是两个适配器,ApplicationListenerMethodAdapterApplicationListenerMethodTransactionalAdapter,由这两个适配器来执行相应的处理逻辑。这里要感叹下spring设计的精妙,一环扣一环,扩展性极强。

这里分析下ApplicationListenerMethodTransactionalAdapter中对应的监听触发方法onApplicationEvent,publish事件时: 创建一个TransactionSynchronization对象, 这个对象持有event,创建TransactionSynchronizationEventAdapter事件适配器,然后注册到事务管理器中。

@Override
public void onApplicationEvent(ApplicationEvent event) {
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
        // 通过事务管理器方式执行
    }else if (this.annotation.fallbackExecution()) {
        // 反射进行事件的处理
    }
}

本以为采用这种方式之后,就解决了对应的异步+调用点事务的问题。在测试中发现:若采用广播器实现异步,极大可能获取不到调用点事务内数据;而采用@Async实现异步百分百可以获取到调用点事务内数据。

简单跟踪发现:

  • 广播器方式实现异步,是将onApplicationEvent方法的触发丢入线程池。
  • @Async方式实现异步,走下方else逻辑,在事件发送器中走同步逻辑,是直接执行onApplicationEvent
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
        if (executor != null) {
            // invokeListener()返回最后的逻辑是去调用ApplicationListener.onApplicationEvent()
            executor.execute(() -> invokeListener(listener, event));
        }
        else {
            invokeListener(listener, event);
        }
    }
}

这里的是否对onApplicationEvent方法执行执行起到了关键性的作用,因为在事务监听处理器适配器中会判断是否是否存在事务。第一种情况,由线程池内线程来执行该方法,这时事务是绑定在原线程上,所以会导致这个判断结果为false。第二种情况,由事件发送线程执行该方法,这时与事务在同一线程,则这个判断的结果为true,将对应的事件处理方法注册到事务管理器中,待到执行改事件监听处理方法时,是异步进行处理的。

总结

​ 整体使用下来,发现其中的道道还是很多的,这需要对所有的组合情况、问题情况、原理都掌握的情况下,否则随意组合,可能在某一场景下能达到需要的效果,但是就像是埋下了定时炸弹。当然了spring的事件监听机制毕竟只是基于内存,若对应的生产环境并没有升级停机钩子处理,或者是金丝雀升级等方式,需停机升级,有可能会导致部分监听未执行的情况,所以建议生产环境还是通过一些mq组件进行发布监听事件的处理。