Spring监听器用法与原理详解(带ApplicationListener模型图)(2)

简介: Spring监听器用法与原理详解(带ApplicationListener模型图)

四、Spring监听器原理

1. Spring监听器模型

前面我们讲了观察者模式的模型,它的模型主要是由 观察者实体 和 主题实体 构成,而Spring的监听器模式则结合了Spring本身的特征,也就是容器化。在Spring中,监听器实体全部放在ApplicationContext中,事件也是通过ApplicationContext来进行发布,具体模型如下:

f109b3c5a4dd4d03b436a329816df52a.png

我们不难看到,虽说是通过ApplicationContext发布的事件,但其并不是自己进行事件的发布,而是引入了一个处理器—— EventMulticaster,直译就是事件多播器,它负责在大量的监听器中,针对每一个要广播的事件,找到事件对应的监听器,然后调用该监听器的响应方法,图中就是调用了监听器1、3、6。


PS: 只有在某类事件第一次广播时,EventMulticaster才会去做遍历所有监听器的事,当它针对该类事件广播过一次后,就会把对应监听器保存起来了,最后会形成一个缓存Map,下一次就能直接找到这些监听器

final Map<ListenerCacheKey, ListenerRetriever> retrieverCache = new ConcurrentHashMap<>(64);

2. @EventListener原理

直接实现监听器接口,然后注册成Bean,这种方式比较好理解,因为我们自己写的实现类就是监听器。但是使用 @EventListener 时,监听器又是怎么产生呢?我们以上面的【自定义事件与监听器Demo】为例,来看一下关键代码。


我们知道,在生成流程中,会对每个Bean都使用PostProcessor来进行加工,而其中就有这么一个类EventListenerMethodProcessor,这个类会在Bean实例化后进行一系列操作

(PS: 首先,不了解Bean生成过程的同学,可以先去看看另一篇文章:SpringBean生成流程详解 )

private void processBean(final String beanName, final Class<?> targetType) {
          ......省略前面代码
  // Non-empty set of methods
  ConfigurableApplicationContext context = this.applicationContext;
  Assert.state(context != null, "No ApplicationContext set");
  List<EventListenerFactory> factories = this.eventListenerFactories;
  Assert.state(factories != null, "EventListenerFactory List not initialized");
  // 遍历该Bean中有EventListener注解的方法,此例中即methodA、methodB
  for (Method method : annotatedMethods.keySet()) {
      // 遍历监听器工厂,这类工厂是专门用来创建监听器的,此处起作用的是默认工厂DefaultEventListenerFactory
    for (EventListenerFactory factory : factories) {
        // DefaultEventListenerFactory是永远返回true的
      if (factory.supportsMethod(method)) {
        Method methodToUse = AopUtils.selectInvocableMethod(method, context.getType(beanName));
        // 利用该Bean名、Bean类型、方法来创建监听器
        ApplicationListener<?> applicationListener =
            factory.createApplicationListener(beanName, targetType, methodToUse);
        if (applicationListener instanceof ApplicationListenerMethodAdapter) {
          ((ApplicationListenerMethodAdapter) applicationListener).init(context, this.evaluator);
        }
        // 把监听器存入容器
        context.addApplicationListener(applicationListener);
        break;
      }
    }
  }
  ......省略后面代码
}

如上,遍历Bean每个带@EventListener注解的方法,然后利用DefaultEventListenerFactory开始创建监听器,实际上这些监听器类型都是一个适配器类——ApplicationListenerMethodAdapter,只是因为这些监听器具体的参数不一样,所以可以监听不同的事件,做不同的响应

public class DefaultEventListenerFactory implements EventListenerFactory, Ordered {
  // 省略其余代码
  @Override
  public ApplicationListener<?> createApplicationListener(String beanName, Class<?> type, Method method) {
    // 可以看到,每次都是返回一个新对象,所以我们在MyListener里的两个方法都加了@EventListener,其实就会返回两个监听器
    return new ApplicationListenerMethodAdapter(beanName, type, method);
  }
}

最后效果如图,成功的创建了两个监听器

e3e8270aa91c4d35af2be707fe1ac0b4.png


3. @EventListener错误尝试

知道了@EventListener的原理,我们其实可以做一些猜测,如下:

methodA是正常的用法;

methodB方法的修饰符是private;

methodC则是监听的ContextRefreshedEvent,但下面方法的入参却是ContextClosedEvent;

66458a9efef843d783cb3b57d3c1db89.png

后两者都有问题:

可以看到,编译器直接黄底提示了methodB的@EventListener注解,其实从前面我们已经猜到,因为最后我们的调用是由监听器ApplicationListenerMethodAdapter对象直接调用的方法ABC,所以方法必须可被其他对象调用,即public

d2505aea57dc40b39739bda0474074a2.png

而后者会在执行广播响应事件时报参数非法异常也是意料之中。


五、同步与异步

通过模型,我们不难看出,事件的发布其实由业务线程来发起,那么哪些监听器的触发呢,是仍由业务线程一个个同步地去通知监听器,还是有专门的线程接手,收到事件后,再转手通知监听器们?


1. 默认同步通知

其实,因为spring默认的多播器没有设置执行器,所以默认采用的是第一种情况,即哪个线程发起的事件,则由哪个线程去通知监听器们,关键代码如下所示

  // SimpleApplicationEventMulticaster.java
  public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
    ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
    Executor executor = getTaskExecutor();
    for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
      if (executor != null) {
        executor.execute(() -> invokeListener(listener, event));
      }
      else {
        // 默认走此分支,由发出事件的线程来执行
        invokeListener(listener, event);
      }
    }
  }
  @Nullable
  protected Executor getTaskExecutor() {
    return this.taskExecutor;
  }

我们可以看到,对于每个监听器的调用是同步还是异步,取决于多播器内部是否含有一个执行器,如果有则交给执行器去执行,如果没有,只能让来源线程一一去通知了。


2. 异步通知设置

两种方式,一种是在多播器创建时内置一个线程池,使多播器能够调用自身的线程池去执行事件传播。另一种是不再多播器上做文章,而是在每个监视器的响应方法上标注异步@Async,毫无疑问,第一种才是正道,我们来看看如何做到。其实有多种方法,我们这里说两种。


第一种,直接自定义一个多播器,然后顶替掉Spring自动创建的多播器

@Configuration
public class EventConfig {
    @Bean("taskExecutor")
    public Executor getExecutor() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
                15,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue(2000));
        return executor;
    }
    // 这其实就是spring-boot自动配置的雏形,所谓的自动配置其实就是通过各种配置类,顶替原有的简单配置
    @Bean(AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME)
    public ApplicationEventMulticaster initEventMulticaster(@Qualifier("taskExecutor") Executor taskExecutor) {
        SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = new SimpleApplicationEventMulticaster();
        simpleApplicationEventMulticaster.setTaskExecutor(taskExecutor);
        return simpleApplicationEventMulticaster;
    }
}

第二种,为现成的多播器设置设置一个线程池

@Component
public class WindowsCheck implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SimpleApplicationEventMulticaster caster = (SimpleApplicationEventMulticaster)applicationContext
                .getBean(AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
                15,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue(2000));
        caster.setTaskExecutor(executor);
    }
}

当然,这里推荐第一种,第二种方法会在spring启动初期的一些事件上,仍采用同步的方式。直至被注入一个线程池后,其才能使用线程池来响应事件。而第一种方法则是官方暴露的位置,让我们去构建自己的多播器。


六、总结

我们可以看到,一个Spring监听器内容其实并不少,而且用到了观察者模式,工厂模式(EventListenerFactory),适配器模式(ApplicationListenerMethodAdapter)。除了这些设计模式,还需要对Spring的基础有些了解,比如Bean生成过程(PostProcessor),不过,相信你看完了本篇,已经对Spring监听器用法及原理已经有了相当的理解了,只需要在后续开发实践中,注意相互印证即可。


目录
打赏
0
0
0
0
242
分享
相关文章
RAG 调优指南:Spring AI Alibaba 模块化 RAG 原理与使用
通过遵循以上最佳实践,可以构建一个高效、可靠的 RAG 系统,为用户提供准确和专业的回答。这些实践涵盖了从文档处理到系统配置的各个方面,能够帮助开发者构建更好的 RAG 应用。
584 114
微服务——SpringBoot使用归纳——Spring Boot中使用监听器——Spring Boot中自定义事件监听
本文介绍了在Spring Boot中实现自定义事件监听的完整流程。首先通过继承`ApplicationEvent`创建自定义事件,例如包含用户数据的`MyEvent`。接着,实现`ApplicationListener`接口构建监听器,用于捕获并处理事件。最后,在服务层通过`ApplicationContext`发布事件,触发监听器执行相应逻辑。文章结合微服务场景,展示了如何在微服务A处理完逻辑后通知微服务B,具有很强的实战意义。
55 0
微服务——SpringBoot使用归纳——Spring Boot中使用监听器——监听器介绍和使用
本文介绍了在Spring Boot中使用监听器的方法。首先讲解了Web监听器的概念,即通过监听特定事件(如ServletContext、HttpSession和ServletRequest的创建与销毁)实现监控和处理逻辑。接着详细说明了三种实际应用场景:1) 监听Servlet上下文对象以初始化缓存数据;2) 监听HTTP会话Session对象统计在线用户数;3) 监听客户端请求的Servlet Request对象获取访问信息。每种场景均配有代码示例,帮助开发者理解并应用监听器功能。
51 0
Spring Boot 项目中触发 Circular View Path 错误的原理与解决方案
在Spring Boot开发中,**Circular View Path**错误常因视图解析与Controller路径重名引发。当视图名称(如`login`)与请求路径相同,Spring MVC无法区分,导致无限循环调用。解决方法包括:1) 明确指定视图路径,避免重名;2) 将视图文件移至子目录;3) 确保Spring Security配置与Controller路径一致。通过合理设定视图和路径,可有效避免该问题,确保系统稳定运行。
41 0
Spring Boot中的WebFlux编程模型
Spring WebFlux 是 Spring Framework 5 引入的响应式编程模型,基于 Reactor 框架,支持非阻塞异步编程,适用于高并发和 I/O 密集型应用。本文介绍 WebFlux 的原理、优势及在 Spring Boot 中的应用,包括添加依赖、编写响应式控制器和服务层实现。WebFlux 提供高性能、快速响应和资源节省等优点,适合现代 Web 应用开发。
249 15
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
322 14
Spring高手之路26——全方位掌握事务监听器
本文深入探讨了Spring事务监听器的设计与实现,包括通过TransactionSynchronization接口和@TransactionalEventListener注解实现事务监听器的方法,并通过实例详细展示了如何在事务生命周期的不同阶段执行自定义逻辑,提供了实际应用场景中的最佳实践。
132 2
Spring高手之路26——全方位掌握事务监听器
Spring AOP 底层原理技术分享
Spring AOP(面向切面编程)是Spring框架中一个强大的功能,它允许开发者在不修改业务逻辑代码的情况下,增加额外的功能,如日志记录、事务管理等。本文将深入探讨Spring AOP的底层原理,包括其核心概念、实现方式以及如何与Spring框架协同工作。
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于 xml 的整合
本教程介绍了基于XML的MyBatis整合方式。首先在`application.yml`中配置XML路径,如`classpath:mapper/*.xml`,然后创建`UserMapper.xml`文件定义SQL映射,包括`resultMap`和查询语句。通过设置`namespace`关联Mapper接口,实现如`getUserByName`的方法。Controller层调用Service完成测试,访问`/getUserByName/{name}`即可返回用户信息。为简化Mapper扫描,推荐在Spring Boot启动类用`@MapperScan`注解指定包路径避免逐个添加`@Mapper`
50 0
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 介绍
本课介绍Spring Boot集成Thymeleaf模板引擎。Thymeleaf是一款现代服务器端Java模板引擎,支持Web和独立环境,可实现自然模板开发,便于团队协作。与传统JSP不同,Thymeleaf模板可以直接在浏览器中打开,方便前端人员查看静态原型。通过在HTML标签中添加扩展属性(如`th:text`),Thymeleaf能够在服务运行时动态替换内容,展示数据库中的数据,同时兼容静态页面展示,为开发带来灵活性和便利性。
59 0
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等