首页 > 编程语言 >Spring源码分析(十二)ApplicationContext详解(中)

Spring源码分析(十二)ApplicationContext详解(中)

时间:2023-09-05 16:32:50浏览次数:38  
标签:ApplicationContext Spring class event 源码 事件 监听器 println public

上篇文章已经对ApplicationContext的一部分内容做了介绍,ApplicationContext主要具有以下几个核心功能:

  1. 国际化
  2. 借助Environment接口,完成了对 Spring运行环境的抽象,可以返回环境中的属性,并能出现占位符
  3. 借助于Resource系列接口,完成对底层资源的访问和加载
  4. 接触了ApplicationEventPublisher接口,能够进行事件发布监听
  5. 负责创建,配置和管理bean

上篇文章写了1,2,两点,这篇文章继续之前的内容

  1. Spring的资源(Resource)

首先需要说明的是,Spring并没有让ApplicationContext直接继承了Resource接口,就像ApplicationContext接口并没有直接继承Environment接口一样。这也不难理解,采用这种组合的方式会让我们的类更加轻量,也起到了解耦的作用,ApplicationContext和Resoucre相关的接口的继承关系如下:

Spring源码分析(十二)ApplicationContext详解(中)_处理事件

不管是ResourceLoader还是ResourcePatternResolver都是为了获取Resource对象,不过ResourceLoader在ResourcePatternResolver的基础上扩展了一个获取多个Resource的方法,后面在介绍。

接口简介

Resource继承了InputStreamSource

Spring源码分析(十二)ApplicationContext详解(中)_System_02

Spring源码分析(十二)ApplicationContext详解(中)_监听器_03

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_04

UML类图

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_05

因为实现了Resource的接口的类很多,并且一些类我们用不到或者很简单,所以上图中省略了一些不重要的分支,接下来就一个个分析。

抽象基类AbstractResource

实现了Resource接口,是大多数Resource实现类的基类,提供了很多通用的方法。比如exists方法会检查是否一个文件或者输入流能够被打开,isOpen永远返回false,getURL和getFile方法会抛出异常,toString将会返回描述信息。

FileSystemResource

基于Java的文件系统封装成一个资源对象。

AbstractFileResolvingResource

将URL解析成文件引用,即会处理协议为:file的URL,也会处理JBoss的vfs协议。然后相应的解析成对应的文件系统引用。

ByteArrayResource

根据一个给定的字节数组构建的一个资源,同时给出一个对应的输入流。

BeanDefinitionResource

只是对BeanDefinition进行的一次描述性的封装

InputStreamResource

是针对输入流封装的资源,它的构建需要一个输入流,对于getInputStream操作将直接返回该字节流,因此只能读取一次该字节流,即isOpen永远返回true。

UrlResource

UrlResource代表URL资源,用于简化URL资源访问。

UrlResource一般支持如下资源访问:

-http:通过标准的http协议访问web的资源,如new UrlResource("http//地址");

-ftp:通过ftp协议访问资源,如new UrlResource("ftp://地址");

-file:通过file协议访问本地文件系统资源,如new UrlResource("file:d/test.txt");

ClassPathResource

JDK获取资源的两种方式:

  1. 使用Class对象的getResource(String path)获取资源的URL,getResourceAsStream(String path) 获取资源流。参数即可是当前class文件相对路径(以文件夹或文件开头),也可以是当前class文件的绝对路径(以“/”开头,相对于当前classpath根目录)
  2. 使用ClassLoader对象的getResource(String path)获取资源的URL,getResourceAsStream(String path) 获取资源流。参数只能是绝对路径,但不能与“/”开头

ClassPathResource代表classpath路径的资源,将使用给定的Class或ClassLoader进行加载classpath资源,isOpen永远返回false,表示可多次读取资源

ServletContextResource

是针对于ServletContext封装的资源,用于访问ServletContext环境下的资源。ServletContextResource持有一个ServletContext的引用,其底层是通过ServletContext的getResource方法getResourceAsStream方法来获取资源的。

ResourceLoader

接口简介

ResourceLoader接口被设计用来从指定的位置加载一个Resource,其接口定义如下

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_06

UML类图

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_07

对于一些不是很必要的类都忽略了,其核心的类只需要关注DefaultResourceLoader就可以了,因为其与子类(除了GenericApplicationContext)都是直接继承了DefaultResourceLoader的getRe方法,代码如下:

Spring源码分析(十二)ApplicationContext详解(中)_监听器_08

资源路径

ant-style

类似于下面这种含有通配符的路径

/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml
// 如果是以“/”开头,直接返回一个classpathResource

classpath和classpath*

classpath:用来加载类路径(包括jar包)中一个且仅一个资源;

classpath*:用来加载类路径(包括jar包)中所有配置的资源,可使用Ant路径模式。


  1. Spring中的事件监听机制(publish-event)

我们知道,ApplicationContext接口继承了ApplicationContextEventPublisher接口,能够进行事件发布监听,那么什么是事件的发布和监听呢?我们从监听者模式说起

监听者模式

概念

事件源经过事件的封装传给监听器,当事件源触发事件后,监听器接收到事件对象可以回调事件的方法。

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_09

Spring对监听者模式的实践

直接通过一个例子来体会:

public class Test {
      public static void main(String[] args) {
         // 创建一个事件发布器(事件源),为了方便,我这里直接通过传入EventListener.class来将监听器注册到容器中
         ApplicationEventPublisher ac = new AnnotationConfigApplicationContext(EventListener.class);
         // 发布一个事件
         ac.publishEvent(new MyEvent("hello event"));
         // 程序会打印如下:
         // 接收到事件:hello event
         // 处理事件....
      }


      static class MyEvent extends ApplicationEvent {
         public MyEvent(Object source) {
            super(source);
         }
      }


      @Component
      static class EventListener implements ApplicationListener<MyEvent> {
         @Override
         public void onApplicationEvent(MyEvent event) {
            System.out.println("接收到事件:" + event.getSource());
            System.out.println("处理事件....");
         }
      }
}

在上面的例子中,主要涉及到了三个角色,也就是我们之前提到的

  1. 事件源:ApplicationEventPublisher
  2. 事件:MyEvent
  3. 事件监听器:EventListener ,实现了ApplicationListener

我们通过ApplicationEventPublisher发布了一个事件(MyEvent),然后事件监听器监听到了事件,并进行对应的处理。

接口简介

ApplicationEventPublisher

Spring源码分析(十二)ApplicationContext详解(中)_System_10

对于这个接口,只需要关注有哪些子类是实现了publishEvent(Object event)这个方法即可。

搜索发现,我们只需要关注org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object)这个方法即可,关于这个方法在后文的源码分析在详细介绍。

ApplicationEvent

继承关系如下:

Spring源码分析(十二)ApplicationContext详解(中)_System_11

我们主要关注上面4个类(PayloadApplicationEvent在后文源码分析时再介绍),下面几个都是Spring直接在内部使用到的事件,比如ContextClosedEvent,在容器关闭时被创建然后发布

// 这个类是java的util包下的一个类,java本身也具有一套事件机制
public class EventObject implements java.io.Serializable {
    
    // 事件所发生的那个源,比如在java中,我们发起了一个鼠标点击事件,那么这个source就是鼠标
    protected transient Object  source;


    public EventObject(Object source) {
        if (source == null)
            throw new IllegalArgumentException("null source");
        this.source = source;
    }


    public Object getSource() {
        return source;
    }


    public String toString() {
        return getClass().getName() + "[source=" + source + "]";
    }
}

Spring源码分析(十二)ApplicationContext详解(中)_监听器_12

ApplicationListener

Spring源码分析(十二)ApplicationContext详解(中)_监听器_13

注解方式实现事件发布机制

在上面的例子中,我们通过传统的方式实现了事件的发布监听,但是上面的过程是在有点繁琐,我们发布的事件需要实现指定的接口,在进行监听时又需要实现指定的接口。每增加一个发布的事件,代表需要多两个类,这样在项目的迭代过程中,会导致我们关于事件的类越来越大,所以在Spring4.2版本后,新增了一个注解,让我们可以快速的实现对发布的事件监听。示例代码:

@ComponentScan("com.wxx.official.event")
public class Test {
   public static void main(String[] args) {
      ApplicationEventPublisher publisher = new AnnotationConfigApplicationContext(Test.class);
      publisher.publishEvent(new Event("注解事件"));
      // 程序打印:
      // 接收到事件:注解事件
      // 处理事件
   }


   static class Event {
      String name;


      Event(String name) {
         this.name = name;
      }


      @Override
      public String toString() {
         return name;
      }
   }


   @Component
   static class Listener {
      @EventListener
      public void listen(Event event) {
         System.out.println("接收到事件:" + event);
         System.out.println("处理事件");
      }
}

可以看到上面的例子中,我们使用一个@EventListener注解,直接标注了Listen类中的一个方法是一个事件监听器,并且通过方法的参数类型Event指定了这个监听器的事件类型为Event类型。在这个例子中,第一,我们的事件不需要去继承特定的类,第二,我们的监听器也不需要去实现特定的接口,极大的方便我们的开发。

异步的方式实现事件监听

对于上面的例子,只需要按下面这种方式添加两个注解即可完成异步:

@ComponentScan("com.wxx.official.event")
@Configuration
@EnableAsync  // 1.开启异步支持
public class MyEvent {
   public static void main(String[] args) {
      ApplicationEventPublisher publisher = new AnnotationConfigApplicationContext(MyEvent.class);
      publisher.publishEvent(new Event("注解事件"));
      // 程序打印:
      // 接收到事件:注解事件
      // 处理事件
   }


   static class Event {
      String name;


      Event(String name) {
         this.name = name;
      }


      @Override
      public String toString() {
         return name;
      }
   }


   @Component
   static class Listener {
      @EventListener
      @Async
      // 2.标志这个方法需要异步执行
      public void listen(Event event) {
         System.out.println("接收到事件:" + event);
         System.out.println("处理事件");
      }
}

对于上面的两个注解@EnableAsync以及@Async,我会在AOP系列的文章中再做介绍,目前而言,知道能通过这种方式开启异步支持即可。

对监听器进行排序

当我们发布一个事件时,可能会同时被两个监听器监听到,比如在我们上面的例子中如果同时存在两个监听器,如下:

@Component
static class Listener {
   @EventListener
   public void listen1(Event event) {
      System.out.println("接收到事件1:" + event);
      System.out.println("处理事件");
   }


   @EventListener
   public void listen2(Event event) {
      System.out.println("接收到事件2:" + event);
      System.out.println("处理事件");
   }
}

在这种情况下,我们可能希望两个监听器可以按顺序执行,这个时候需要用到另一个注解了:@Order

@Component
static class Listener {
   @EventListener
   @Order(2)
   public void listen1(Event event) {
      System.out.println("接收到事件1:" + event);
      System.out.println("处理事件");
   }


   @EventListener
   @Order(1)
   public void listen2(Event event) {
      System.out.println("接收到事件2:" + event);
      System.out.println("处理事件");
   }
}

注解中的参数越小,代表优先级越高,在上面的例子中,会先执行listen2方法再执行listen1方法

那Spring到底如何实现这一套事件发布机制呢?接下来我们进行源码分析

源码分析(publishEvent方法)

我们需要分析的代码主要是org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType)方法,源码如下:

Spring源码分析(十二)ApplicationContext详解(中)_监听器_14

上面这段代码核心部分就是getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);,我们分成几个部分解析:

  • getApplicationEventMulticaster()
  • multicastEvent(applicationEvent, eventType)

getApplicationEventMulticaster()方法代码如下:

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_15

可以看到,只是简单的获取容器中已经初始化好的一个applicationEventMulticaster,那么现在有几个问题:

  1. applicationEventMulticaster是什么?
  • 接口定义

Spring源码分析(十二)ApplicationContext详解(中)_监听器_16

  • UML类图

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_17

主要涉及到两个类:

  • AbstractApplicationEventMulticaster,这个类对ApplicationEventMulticaster这个接口基础方法做了实现,除了核心方法multicastEvent,这个类最大的作用是获取监听器。稍后在介绍.。
  • SimpleApplicationEventMulticaster,这是Spring默认提供的一个事件分发器,如果我们没有进行特别配置的话,就会采用这个类生成的对象作为容器的事件分发器。
  1. 容器在什么时候对其进行的初始化

回到我们之前的图:

Spring源码分析(十二)ApplicationContext详解(中)_System_18

可以看到,在3-8调用了一个initApplicationEventMulticaster方法,从名字我们就知道,这是对ApplicationEventMulticaster进行的初始化,看看这个方法做了什么。

  • initApplicationEventMulticaster方法

Spring源码分析(十二)ApplicationContext详解(中)_System_19

这段代码的含义就是告诉我们,可以自己配置一个ApplicationEventMulticaster,如果没有进行配置,那么将默认使用一个SimpleApplicationEventMulticaster。

接下来,我们尝试自己配置一个简单的ApplicationEventMulticaster,示例代码如下:

@Component("applicationEventMulticaster")
static class MyEventMulticaster extends AbstractApplicationEventMulticaster {


   @Override
   @SuppressWarnings({"unchecked", "rawtypes"})
   public void multicastEvent(@NonNull ApplicationEvent event) {
      ResolvableType resolvableType = ResolvableType.forInstance(event);
      Collection<ApplicationListener<?>> applicationListeners = getApplicationListeners(event, resolvableType);
      for (ApplicationListener applicationListener : applicationListeners) {
         applicationListener.onApplicationEvent(event);
      }
   }


   @Override
   public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
      System.out.println("进入MyEventMulticaster");
   }

运行程序后会发现“进入MyEventMulticaster”这句话打印了两次,第一次是容器启动时会发布一个ContextStartedEvent事件,也会调用我们配置的事件分发器进行事件发布。

  • multicastEvent(applicationEvent, eventType)

在Spring容器中,只内置了一个这个方法的实现类,就是SimpleApplicationEventMulticaster,实现的逻辑如下:

Spring源码分析(十二)ApplicationContext详解(中)_System_20

上面代码主要的实现逻辑可以分为三步:

  1. 推断事件类型
  2. 根据事件类型获取对应的监听器
  3. 执行监听逻辑

我们一步步分析

  • resolveDefaultEventType(event),推断事件类型

Spring源码分析(十二)ApplicationContext详解(中)_System_21

Spring源码分析(十二)ApplicationContext详解(中)_处理事件_22

上面代码涉及到一个概念就是ResolvableType,对于ResolvableType我们需要了解的是,ResolvableType为所有Java类型提供了统一的数据结构和API,换句话说,一个ResolvableType对象就对应着一种Java类型。可以通过ResolvableType对象获取类型携带的信息(举例如下):

  1. getSuperType():获取直接父类型
  2. getInterfaces():获取接口类型
  3. getGeneric(int...):获取类型携带的泛型类型
  4. resolve():Type对象到Class对象的转换

另外,ResolvableType的构造方法全部为私有,我们不能直接new,只能使用其提供的静态方法案进行类型获取:

  1. forField(Field):获取指定字段的类型
  2. forMethodParameter(Method, int):获取指定方法的指定形参的类型
  3. forMethodReturnType(Method):获取指定方法的返回值的类型
  4. forClass(Class):直接封装指定的类型
  5. ResolvableType.forInsance:获取指定的实例的泛型信息

关于ResolvableType和Java的类型中的关系请关注我的后续文章,限于篇幅原因在本文就不做过多介绍了。


  • getApplicationListeners(event, type),获取对应的事件监听器

事件监听器主要分为两种,一种是我们通过实现接口直接注册到容器的bean,例如下面这种

@Component
static class EventListener implements ApplicationListener<MyEvent> {
   @Override
   public void onApplicationEvent(MyEvent event) {
      System.out.println("接收到事件:" + event.getSource());
      System.out.println("处理事件....");
   }
}

另外一种是通过注解的方式,就是下面这种

@Component
static class Listener {
   @EventListener
   public void listen1(Event event) {
      System.out.println("接收到事件1:" + event);
      System.out.println("处理事件");
   }
}

对于实现接口的方式不用多说,因为实现了这个类本身就会被扫描然后加入到容器中。对于注解这种方式,Spring是通过一个回调方法实现的。大家关注下这个接口org.springframework.beans.factory.SmartInitializingSingleton,同时找到其实现类,org.springframework.context.event.EventListenerMethodProcessor。在这个类中,会先调用afterSingletonsInstantiated方法,然后调用一个processBean方法,在这个方法中会遍历所有容器中的bean,然后遍历bean中 的每一个方法判断方法上是否加了一个@EventListener注解。如果添加了这个注解,会将这个Method方法包装成一个ApplicationListenerMethodAdapter,这个类本身也实现了ApplicationListener接口,之后再添加到监听器的集合中。


  • invokeListener,执行监听逻辑

本身这个方法没什么好说的,就是调用了ApplicationListener中的onApplicationEvent方法,执行我们的业务逻辑。但是值得注意的是,在调用onApplicationEvent方法前,会先进行一个判断

Executor executor = getTaskExecutor();
if (executor != null) {
   executor.execute(() -> invokeListener(listener, event));
}
else {
   invokeListener(listener, event);
}

会先判断是否获取到一个Executor,如果能获取到那么会通过Executor异步执行监听的逻辑。所以基于这段代码,我们可以不通过@Async注解实现对事件的异步监听,而是复写SimpleApplicationEventMulticaster这个类的方法,如下:

@Component("applicationEventMulticaster")
public class MyEventMulticaster extends SimpleApplicationEventMulticaster {
   @Override
   public Executor getTaskExecutor() {
      // 在这里创建自己的执行器
      return executor();
   }
}

相比于通过@Async注解实现对事件的异步监听,我更倾向于通过复写的方式进行实现,主要原因就是如果通过注解实现,那么所有加了这个注解的方法在异步执行都是用的同一个线程池,这些加了注解的方法有些可能并不是进行事件监听的,这样显然是不合理的。而后面这种方式,我们可以确保创建的线程池是针对事件监听的,甚至可以根据不同的事件类型路由到不同的线程池。

总结

在这篇文章中,完成了对ApplicationContext中以下两点内容

  1. 借助于Resource一系列接口,完成对底层资源的访问及加载
  2. 实现事件的发布

对于整个ApplicationContext体系,目前来说还剩一个很大的功能没涉及到。因为我们也知道ApplicationContext继承了一系列的BeanFactory接口,所以它还会负责创建,管理及配置bean。

BeanFactory本身也有一套自己的体系,在下篇文章中,就写BeanFactory相关的内容。虽然这一系列文章是以ApplicationContext命名的,但是其中的内容覆盖面很广,这些东西对于我们看懂Spring很重要

希望大家跟我一起慢慢啃掉Spring,加油!共勉!

标签:ApplicationContext,Spring,class,event,源码,事件,监听器,println,public
From: https://blog.51cto.com/u_15668812/7373957

相关文章

  • 解决代码使用CompletableFuture做异步时spring-cloud-starter-sleuth的日志追踪号为空
    产生问题原因就是异步调用,导致spanId和traceId丢失了@Async注解的异步调用是没问题的前提使用spring-cloud-starter-sleuthjar包版本2.2.8.RELEASE关于追踪号的xml配置为<pattern>%yellow(%date{yyyy-MM-ddHH:mm:ss.SSS})[%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-B......
  • Springboot+Quartz+Dynamic-datasource
    在使用dynamic-datasource多数据源切换场景下,实现Quartz任务持久化配置和API动态调度1.pom依赖暂未找到版本对应关系,若有版本不一致异常,请自行尝试升降版本。<dependencies><!--动态数据源--><dependency><groupId>com.baomidou</groupI......
  • [SpringSecurity5.6.2源码分析二]:SecurityAutoConfiguration
    • SecurityAutoConfiguration是SpringSecurity最重要的一个自动配置类• 像以前版本的教程说要在启动类上配@EnableWebSecurity,现在也是由这个自动配置类负责引入• 分析一 已经介绍了DefaultAuthenticationEventPublisher,所以说重点就只有使用@Import导入的三个类,SpringBo......
  • Spring Bean 的生命周期,如何被管理的
    实例化一个Bean,也就是我们通常说的new按照Spring上下文对实例化的Bean进行配置,也就是IOC注入如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(StringbeanId)方法,此处传递的是Spring配置文件中Bean的ID如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBean......
  • elasticsearch wildcard 慢查询原因分析(深入到源码!!!)
    大家好,我是蓝胖子,前段时间线上elasticsearch集群遇到多次wildcard产生的性能问题,elasticsearchwildcard一直是容易引发elasticsearch容易宕机的一个风险点,但究竟它为何消耗cpu呢?又该如何理解elasticsearchprofileapi的返回结果呢?在探索了部分源码后,我将在这篇文章一一揭......
  • 直播带货源码,vue 身份证校验js及其***显示
    直播带货源码,vue身份证校验js及其***显示校验js  constidCardRule=(rule,value,callback)=>{  letreg=/^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/i  if(value&&!reg.test(value)){   callback(newError('身份证号格式有误'))  }else......
  • springboot加载bean失败:No matching autowired candidates found
    场景:之前在培训轮岗,一直没有干活,最近开始干活遇到xxljob,打算自己学习了解一下。在按照文档配置执行器项目时,发现怎么启动,xxlJobExecutor都没有被加载进来。解决:后来经过查阅,原来是springBoot启动默认扫描的是启动类所在的包以及其子包,而我的文件为:因此bean注入失败。把......
  • 自定义企业培训系统:源码定制与扩展指南
    在现代企业中,持续学习和培训已经成为成功的关键要素之一。企业培训系统的存在已经变得不可或缺,因为它们允许企业为员工提供高质量的培训和教育,以提高他们的技能水平并满足不断变化的业务需求。然而,通用的培训系统可能无法满足每个企业的特定需求,这就是为什么自定义企业培训系统变得......
  • 医学影像(PACS)源码,影像的获取、处理、存储、调阅、检索、管理
    医学影像(PACS)系统主要进行病人信息和影像的获取、处理、存储、调阅、检索、管理,并通过网络向全院提供病人检查影像及诊断报告;各影像科室之间共享不同设备的病人检查影像及诊断报告;在诊断工作站上,调阅HIS中病人的其它信息(如:病人信息、病历信息、医嘱、检验信息等)。系统包括:1、预......
  • Docker 部署 Jenkins 构建 SpringBoot 工程发布镜像
    说明全部都基于Docker服务搭建使用,首先用Docker安装Jenkins环境,Docker安装GitLab版本管理系统,执行Jenkins拉取指定版本tag进行编译构建,在用SonarQube进行代码质量检测,在打包制作镜像,发布到Harbor镜像仓库,最后启动SpringBoot工程并进行访问。系统平台CentOSLinu......