上篇文章已经对ApplicationContext的一部分内容做了介绍,ApplicationContext主要具有以下几个核心功能:
- 国际化
- 借助Environment接口,完成了对 Spring运行环境的抽象,可以返回环境中的属性,并能出现占位符
- 借助于Resource系列接口,完成对底层资源的访问和加载
- 接触了ApplicationEventPublisher接口,能够进行事件发布监听
- 负责创建,配置和管理bean
上篇文章写了1,2,两点,这篇文章继续之前的内容
- Spring的资源(Resource)
首先需要说明的是,Spring并没有让ApplicationContext直接继承了Resource接口,就像ApplicationContext接口并没有直接继承Environment接口一样。这也不难理解,采用这种组合的方式会让我们的类更加轻量,也起到了解耦的作用,ApplicationContext和Resoucre相关的接口的继承关系如下:
![8c4b4e866024dcc982ccb6c4704ccf7e.png](https://img-blog.csdnimg.cn/img_convert/8c4b4e866024dcc982ccb6c4704ccf7e.png)
不管是ResourceLoader还是ResourcePatternResolver都是为了获取Resource对象,不过ResourceLoader在ResourcePatternResolver的基础上扩展了一个获取多个Resource的方法,后面在介绍。
接口简介
Resource继承了InputStreamSource
![277e1d80945e10bf271d22c13c985988.png](https://img-blog.csdnimg.cn/img_convert/277e1d80945e10bf271d22c13c985988.png)
![586e092f07a237d0261f1c5d9fd9954b.png](https://img-blog.csdnimg.cn/img_convert/586e092f07a237d0261f1c5d9fd9954b.png)
![078ed6aea86a42d6065f13749d8b7f55.png](https://img-blog.csdnimg.cn/img_convert/078ed6aea86a42d6065f13749d8b7f55.png)
UML类图
![432b0b081aba9f38178f684d64fcdb3c.png](https://img-blog.csdnimg.cn/img_convert/432b0b081aba9f38178f684d64fcdb3c.png)
因为实现了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获取资源的两种方式:
- 使用Class对象的getResource(String path)获取资源的URL,getResourceAsStream(String path) 获取资源流。参数即可是当前class文件相对路径(以文件夹或文件开头),也可以是当前class文件的绝对路径(以“/”开头,相对于当前classpath根目录)
- 使用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,其接口定义如下
![40a1b3408c750ccc6e17025ff59160fa.png](https://img-blog.csdnimg.cn/img_convert/40a1b3408c750ccc6e17025ff59160fa.png)
UML类图
![e5ef9b0d7239e884211a127b3b9984d8.png](https://img-blog.csdnimg.cn/img_convert/e5ef9b0d7239e884211a127b3b9984d8.png)
对于一些不是很必要的类都忽略了,其核心的类只需要关注DefaultResourceLoader就可以了,因为其与子类(除了GenericApplicationContext)都是直接继承了DefaultResourceLoader的getRe方法,代码如下:
![187f19d720dbf79baf90a643f46260b6.png](https://img-blog.csdnimg.cn/img_convert/187f19d720dbf79baf90a643f46260b6.png)
资源路径
ant-style
类似于下面这种含有通配符的路径
/
classpath和classpath*
classpath:用来加载类路径(包括jar包)中一个且仅一个资源;
classpath*:用来加载类路径(包括jar包)中所有配置的资源,可使用Ant路径模式。
- Spring中的事件监听机制(publish-event)
我们知道,ApplicationContext接口继承了ApplicationContextEventPublisher接口,能够进行事件发布监听,那么什么是事件的发布和监听呢?我们从监听者模式说起
监听者模式
概念
事件源经过事件的封装传给监听器,当事件源触发事件后,监听器接收到事件对象可以回调事件的方法。
![f844d3d084e22adb54b170236a99ff26.png](https://img-blog.csdnimg.cn/img_convert/f844d3d084e22adb54b170236a99ff26.png)
Spring对监听者模式的实践
直接通过一个例子来体会:
public
在上面的例子中,主要涉及到了三个角色,也就是我们之前提到的
- 事件源:ApplicationEventPublisher
- 事件:MyEvent
- 事件监听器:EventListener ,实现了ApplicationListener
我们通过ApplicationEventPublisher发布了一个事件(MyEvent),然后事件监听器监听到了事件,并进行对应的处理。
接口简介
ApplicationEventPublisher
![677634c8dafd726cba8ea2ee1c1b1381.png](https://img-blog.csdnimg.cn/img_convert/677634c8dafd726cba8ea2ee1c1b1381.png)
对于这个接口,只需要关注有哪些子类是实现了publishEvent(Object event)这个方法即可。
搜索发现,我们只需要关注org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object)这个方法即可,关于这个方法在后文的源码分析在详细介绍。
ApplicationEvent
继承关系如下:
![68f02014d07439ddafd8f8f983a2afea.png](https://img-blog.csdnimg.cn/img_convert/68f02014d07439ddafd8f8f983a2afea.png)
我们主要关注上面4个类(PayloadApplicationEvent在后文源码分析时再介绍),下面几个都是Spring直接在内部使用到的事件,比如ContextClosedEvent,在容器关闭时被创建然后发布
// 这个类是java的util包下的一个类,java本身也具有一套事件机制
![7fee89b1f67f32987e53b46dd17faa98.png](https://img-blog.csdnimg.cn/img_convert/7fee89b1f67f32987e53b46dd17faa98.png)
ApplicationListener
![e8046d7bff70ca3f3be7f9364d87e05e.png](https://img-blog.csdnimg.cn/img_convert/e8046d7bff70ca3f3be7f9364d87e05e.png)
注解方式实现事件发布机制
在上面的例子中,我们通过传统的方式实现了事件的发布监听,但是上面的过程是在有点繁琐,我们发布的事件需要实现指定的接口,在进行监听时又需要实现指定的接口。每增加一个发布的事件,代表需要多两个类,这样在项目的迭代过程中,会导致我们关于事件的类越来越大,所以在Spring4.2版本后,新增了一个注解,让我们可以快速的实现对发布的事件监听。示例代码:
@ComponentScan
可以看到上面的例子中,我们使用一个@EventListener注解,直接标注了Listen类中的一个方法是一个事件监听器,并且通过方法的参数类型Event指定了这个监听器的事件类型为Event类型。在这个例子中,第一,我们的事件不需要去继承特定的类,第二,我们的监听器也不需要去实现特定的接口,极大的方便我们的开发。
异步的方式实现事件监听
对于上面的例子,只需要按下面这种方式添加两个注解即可完成异步:
@ComponentScan
对于上面的两个注解@EnableAsync以及@Async,我会在AOP系列的文章中再做介绍,目前而言,知道能通过这种方式开启异步支持即可。
对监听器进行排序
当我们发布一个事件时,可能会同时被两个监听器监听到,比如在我们上面的例子中如果同时存在两个监听器,如下:
@Component
在这种情况下,我们可能希望两个监听器可以按顺序执行,这个时候需要用到另一个注解了:@Order
@Component
注解中的参数越小,代表优先级越高,在上面的例子中,会先执行listen2方法再执行listen1方法
那Spring到底如何实现这一套事件发布机制呢?接下来我们进行源码分析
源码分析(publishEvent方法)
我们需要分析的代码主要是org.springframework.context.support.AbstractApplicationContext#publishEvent(java.lang.Object, org.springframework.core.ResolvableType)方法,源码如下:
![b06231b1c9c9bbb1ea957ee671d8645e.png](https://img-blog.csdnimg.cn/img_convert/b06231b1c9c9bbb1ea957ee671d8645e.png)
上面这段代码核心部分就是getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);,我们分成几个部分解析:
- getApplicationEventMulticaster()
- multicastEvent(applicationEvent, eventType)
getApplicationEventMulticaster()方法代码如下:
![9de382606569e4d72bc9ca01eb944ebc.png](https://img-blog.csdnimg.cn/img_convert/9de382606569e4d72bc9ca01eb944ebc.png)
可以看到,只是简单的获取容器中已经初始化好的一个applicationEventMulticaster,那么现在有几个问题:
- applicationEventMulticaster是什么?
- 接口定义
![7b80b8ece5e5824b63f192f15ab58077.png](https://img-blog.csdnimg.cn/img_convert/7b80b8ece5e5824b63f192f15ab58077.png)
- UML类图
![cb70c9d058f83934a71d1a215f05116b.png](https://img-blog.csdnimg.cn/img_convert/cb70c9d058f83934a71d1a215f05116b.png)
主要涉及到两个类:
- AbstractApplicationEventMulticaster,这个类对ApplicationEventMulticaster这个接口基础方法做了实现,除了核心方法multicastEvent,这个类最大的作用是获取监听器。稍后在介绍.。
- SimpleApplicationEventMulticaster,这是Spring默认提供的一个事件分发器,如果我们没有进行特别配置的话,就会采用这个类生成的对象作为容器的事件分发器。
- 容器在什么时候对其进行的初始化
回到我们之前的图:
![665d48313956d2934ea1c933c7dce9c0.png](https://img-blog.csdnimg.cn/img_convert/665d48313956d2934ea1c933c7dce9c0.png)
可以看到,在3-8调用了一个initApplicationEventMulticaster方法,从名字我们就知道,这是对ApplicationEventMulticaster进行的初始化,看看这个方法做了什么。
- initApplicationEventMulticaster方法
![5109928d328997ebb42df848e575a244.png](https://img-blog.csdnimg.cn/img_convert/5109928d328997ebb42df848e575a244.png)
这段代码的含义就是告诉我们,可以自己配置一个ApplicationEventMulticaster,如果没有进行配置,那么将默认使用一个SimpleApplicationEventMulticaster。
接下来,我们尝试自己配置一个简单的ApplicationEventMulticaster,示例代码如下:
@Component
运行程序后会发现“进入MyEventMulticaster”这句话打印了两次,第一次是容器启动时会发布一个ContextStartedEvent事件,也会调用我们配置的事件分发器进行事件发布。
- multicastEvent(applicationEvent, eventType)
在Spring容器中,只内置了一个这个方法的实现类,就是SimpleApplicationEventMulticaster,实现的逻辑如下:
![ed668d019a3e55727974202f497af000.png](https://img-blog.csdnimg.cn/img_convert/ed668d019a3e55727974202f497af000.png)
上面代码主要的实现逻辑可以分为三步:
- 推断事件类型
- 根据事件类型获取对应的监听器
- 执行监听逻辑
我们一步步分析
- resolveDefaultEventType(event),推断事件类型
![48742a6b1cdc093383ca19a05ba9905d.png](https://img-blog.csdnimg.cn/img_convert/48742a6b1cdc093383ca19a05ba9905d.png)
![d70fe970bde8ebf442d2fce5f07c618b.png](https://img-blog.csdnimg.cn/img_convert/d70fe970bde8ebf442d2fce5f07c618b.png)
上面代码涉及到一个概念就是ResolvableType,对于ResolvableType我们需要了解的是,ResolvableType为所有Java类型提供了统一的数据结构和API,换句话说,一个ResolvableType对象就对应着一种Java类型。可以通过ResolvableType对象获取类型携带的信息(举例如下):
- getSuperType():获取直接父类型
- getInterfaces():获取接口类型
- getGeneric(int...):获取类型携带的泛型类型
- resolve():Type对象到Class对象的转换
另外,ResolvableType的构造方法全部为私有,我们不能直接new,只能使用其提供的静态方法案进行类型获取:
- forField(Field):获取指定字段的类型
- forMethodParameter(Method, int):获取指定方法的指定形参的类型
- forMethodReturnType(Method):获取指定方法的返回值的类型
- forClass(Class):直接封装指定的类型
- ResolvableType.forInsance:获取指定的实例的泛型信息
关于ResolvableType和Java的类型中的关系请关注我的后续文章,限于篇幅原因在本文就不做过多介绍了。
- getApplicationListeners(event, type),获取对应的事件监听器
事件监听器主要分为两种,一种是我们通过实现接口直接注册到容器的bean,例如下面这种
@Component
另外一种是通过注解的方式,就是下面这种
@Component
对于实现接口的方式不用多说,因为实现了这个类本身就会被扫描然后加入到容器中。对于注解这种方式,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,如果能获取到那么会通过Executor异步执行监听的逻辑。所以基于这段代码,我们可以不通过@Async注解实现对事件的异步监听,而是复写SimpleApplicationEventMulticaster这个类的方法,如下:
@Component
相比于通过@Async注解实现对事件的异步监听,我更倾向于通过复写的方式进行实现,主要原因就是如果通过注解实现,那么所有加了这个注解的方法在异步执行都是用的同一个线程池,这些加了注解的方法有些可能并不是进行事件监听的,这样显然是不合理的。而后面这种方式,我们可以确保创建的线程池是针对事件监听的,甚至可以根据不同的事件类型路由到不同的线程池。
总结
在这篇文章中,完成了对ApplicationContext中以下两点内容
- 借助于Resource一系列接口,完成对底层资源的访问及加载
- 实现事件的发布
对于整个ApplicationContext体系,目前来说还剩一个很大的功能没涉及到。因为我们也知道ApplicationContext继承了一系列的BeanFactory接口,所以它还会负责创建,管理及配置bean。
BeanFactory本身也有一套自己的体系,在下篇文章中,就写BeanFactory相关的内容。虽然这一系列文章是以ApplicationContext命名的,但是其中的内容覆盖面很广,这些东西对于我们看懂Spring很重要
希望大家跟我一起慢慢啃掉Spring,加油!共勉!
不吃竹子的滚滚:Spring源码分析(十四)Spring中的BeanWrapper及类型转换
不吃竹子的滚滚:Spring源码分析(十二)ApplicationContext详解(中)
不吃竹子的滚滚:Spring源码分析(十一)ApplicationContext详细介绍(上)
不吃竹子的滚滚:Spring源码分析(十)Spring中Bean的生命周期(下)
不吃竹子的滚滚:Spring源码分析(九)Spring中Bean的生命周期(上)
不吃竹子的滚滚:Spring源码分析(八)容器的扩展点(BeanPostProcessor)
不吃竹子的滚滚:Spring源码分析(七)容器的扩展点(FactoryBean)
不吃竹子的滚滚:Spring源码分析(六)容器的扩展点(BeanFactoryPostProcessor)
不吃竹子的滚滚:Spring源码分析(五)BeanDefinition(下)
不吃竹子的滚滚:Spring源码分析(四)BeanDefinition(上)
不吃竹子的滚滚:Spring源码分析(三)自动注入与精确注入
不吃竹子的滚滚:Spring源码分析(二)依赖注入及方法注入
不吃竹子的滚滚:Spring源码分析(一)Spring容器及Spring Bean