1. 前言
在 Spring 框架中,BeanFactory
只负责 Bean 的管理工作,包括实例化、依赖解析、初始化、销毁等。同时我们知道,BeanFactory
主要通过 BeanDefinition
来创建实例,现在的问题是谁来注册 BeanDefinition
。事实上,这项工作是由 ApplicationContext
完成的,BeanDefinitionRegistry
接口充当了中间桥梁的作用。
对于字节码文件来说,想要变成 BeanDefinition
并注册到 Spring 容器中,整个流程分为三步。首先,将字节码文件以输入流或字节数组的形式加载到内存中。其次,读取输入流或字节数组中的内容,解析相关的 Class 信息。第三,创建 BeanDefinition
实例,并注册到 BeanFactory
中。在整个过程中,BeanDefinitionRegistry
接口只解决了最后一步,本节将讨论前两步是如何完成的。
2. 资源
2.1 Resource
Resource
接口是 Spring 对资源的统一抽象,包括本地磁盘数据、网络数据、内存数据等等。Resource
的继承结构比较简单,主要的实现类如下:
InputStreamResource
:表示输入流资源ByteArrayResource
: 表示字节数组资源FileSystemResource
: 表示本地磁盘资源(使用File
描述)PathResource
:表示本地磁盘资源(使用Path
描述)UrlResource
: 表示网络资源(使用URL
描述)ClassPathResource
: 表示类路径下的资源,可以看做特殊的本地磁盘资源,这是 Spring 加载组件的方式
2.2 资源分类
InputStreamSource
作为顶级接口,只定义了一个方法 getInputStream
,作用是获取输入流。一般情况下,Resource
接口的实现类都是可读的,但只有一部分资源是可写的。造成这种差异的原因是什么,我们可以将主要实现类分为以下四种情况。
-
InputStreamResource
和ByteArrayResource
的特点是不代表特定的资源,无法根据类名来推断资源的类型。既然不知道资源的来源,自然是不可写的。 -
PathResource
和FileSystemResource
表示本地磁盘资源,资源所对应的文件路径是已知的,因此是可写的。 -
UrlResource
表示网络资源,一般来说网络资源只能读取,除非提供特定接口才能修改。 -
ClassPathResource
表示类路径下的资源,本质上是磁盘资源,但不可写。这是因为类路径下的资源是程序的敏感数据,必须进行保护,一旦被修改程序就不能正常运行。这类资源包括配置文件、静态资源(比如 js、html、css 文件)、程序代码(class 文件编译后的字节码)等。
注:讨论资源的可读性和可写性不是重点,而是为了引入
ClassPathResource
这一特殊的资源。这是资源加载的核心,我们所关心的是 Spring 对配置文件(静态资源)以及程序代码(字节码)的处理。
3. 资源加载器
3.1 概述
ResourceLoader
接口的继承结构由三部分组成,一是主体部分(蓝色区域),包括 ResourceLoader
接口及其子接口/实现类;二是红色区域,PathMatcher
接口及其实现类提供了路径解析的功能;三是紫色区域,Resource
接口负责对资源进行统一抽象。此外,AbstractApplicationContext
继承了 DefaultResourceLoader
,使得 Spring 容器拥有了加载资源的能力。
从类图中可以看到,加载资源的功能是由 ResourceLoader
实现的,而路径解析的功能是委托给 PathMatcher
完成的。也就是说,加载资源是核心功能,路径解析是外围操作,加载资源不一定会用到路径解析,路径解决更像是一种锦上添花的作用。由此想到了什么?没错,还是装饰模式,我们来分析一下具体的组成:
ResourceLoader
接口是抽象组件角色,定义了getResource
方法DefaultResourceLoader
类是具体组件角色,实现了getResource
方法ResourcePatternResolver
接口是抽象装饰角色,不仅继承ResourceLoader
接口,还定义了getResources
方法PathMatchingResourcePatternResolver
类是具体装饰角色,持有一个具体组件角色的实例,实现了加载资源的基本功能。还持有一个PathMatcher
实例,实现了额外的路径解析功能
3.2 ResourceLoader
ResourceLoader
是资源加载的核心接口,定义了加载资源的行为。在 getResource
方法中,参数 location
表示资源的路径。其中,资源路径有多种表现形式:
-
绝对路径:比如
file:C:/abc.txt
,绝对路径是标准的 URL,由协议名和文件路径组成。 -
类路径:比如
classpath:abc.txt
,类路径是伪 URL,classpath:
不是标准的协议名,需要由 Spring 来进行解释。 -
相对路径:比如
WEB-INF/abc.txt
,这是程序中的某个位置,前缀路径由程序的实际情况决定。
public interface ResourceLoader {
//根据指定路径加载单一资源
Resource getResource(String location);
}
ResourcePatternResolver
接口继承了 ResourceLoader
,作用是解析资源的路径模式。getResources
方法有两个特点,一是 locationPattern
参数可以是一个模式字符串,二是返回类型是一个数组。这是因为模式匹配可能有多个符合条件的结果,比如扫描指定路径下的所有文件。
public interface ResourcePatternResolver extends ResourceLoader {
//对路径模式字符串进行解析,并加载多个资源
Resource[] getResources(String locationPattern) throws IOException;
}
ResourcePatternResolver
本身并没有实现路径模式的解析,而是交给 PathMatcher
来处理。PathMatcher
接口用于解析资源所在的路径,AntPathMatcher
是唯一的实现类,可以解析 Ant 风格的路径。Ant 风格的通配符有以下几种:
-
?:匹配任何单字符
-
*:匹配 0 个或任意多个字符,不包括“/”
-
**:匹配 0 个或任意多个字符,包括“/”(可以是多级目录)
Ant 风格的路径模式匹配是一种非常灵活的定位资源的方式,比如 classpath:/**/*.class
表示类路径下的任意目录下的任意名称的 class 文件。
3.3 Spring 的实现
Spring 对 ResourceLoader
接口的使用体现在两个方面。首先,ApplicationContext
接口继承了 ResourcePatternResolver
接口,AbstractApplicationContext
持有一个 PathMatchingResourcePatternResolver
实例,通过 getResources
方法完成路径解析和资源加载的工作。
public interface ApplicationContext extends BeanFactory, ResourcePatternResolver {}
public abstract class AbstractApplicationContext extends DefaultResourceLoader {
private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(this);
@Override
public Resource[] getResources(String locationPattern) throws IOException {
return this.resourcePatternResolver.getResources(locationPattern);
}
}
其次,AbstractApplicationContext
还继承了 DefaultResourceLoader
,可以调用 getResource
方法完成简单的资源加载工作。
4. 资源解析
4.1 概述
Spring 对资源进行抽象,区分的依据是介质和读写问题。但当进行解析的时候,必须考虑资源的具体类型。我们以三种常见的资源为例来说明资源是如何解析,如下所示:
-
静态文件:比如
.html
、.css
、.js
文件,一般由服务器返回给客户端处理,比如浏览器会将这些静态文件渲染成网页。 -
配置文件:主要类型有
.properties
、.xml
、.yml
等,其中.xml
和.yml
文件有专门的框架负责解析,.properties
文件可以使用 JDK 的集合类Properties
处理。 -
字节码文件:源代码的后缀是
.java
,编译成字节码的后缀为.class
,加载之后变成ClassPathResource
对象。Spring 提供了两种解析方式,一是通过反射来解析类的信息,二是使用 ASM 框架处理。
4.2 元数据
Spring 将类的相关信息称为元数据(metadata),在核心包中定义了相关 API 来描述元数据,以及如何获取这些元数据。Spring 使用两个维度来描述元数据,第一个维度是根据目标的不同分为类和方法,主要是接口层面的划分,如下所示:
-
AnnotatedTypeMetadata
接口表示声明的注解的类或方法 -
MethodMetadata
接口表示方法的元数据 -
AnnotationMetadata
接口继承了ClassMeta
接口,表示类的元数据
第二个维度是根据解析方式分为反射和 AMS 两种方式。这两个维度相互作用,对应四个实现类,如下所示:
StandardMethodMetadata
:表示通过反射的方式解析方法MethodMetadataReadingVisitor
:表示通过 ASM 方式解析方法StandardAnnotationMetadata
:表示通过反射的方式解析类AnnotationMetadataReadingVisitor
:表示通过 ASM 方式解析类
注:这些类名比较长,难以记忆,有个简单的区分方法。ASM 解析是通过访问者模式实现的,因此以 Visitor 结尾的类是 ASM 解析,反之是通过反射解析的。
4.3 元数据的解析
StandardMethodMetadata
和 StandardAnnotationMetadata
是通过反射方式获取元数据,可以直接调用构造函数创建实例。此外,方法是类的一部分,MethodMetadata
接口的实例是可以通过 AnnotationMetadata
接口的实例间接获得,因此我们重点关注 AnnotationMetadataReadingVisitor
是如何创建的。
AnnotationMetadataReadingVisitor
的创建过程较为复杂,Spring 通过工厂模式,隐藏了繁琐的解析过程,只要调用工厂方法,便可以得到解析后的类的元数据。相关 API 的结构比较简单,主要包括两部分,如下所示:
-
MetadataReader
接口封装了解析元数据的具体逻辑,唯一的实现类是SimpleMetadataReader
-
MetadataReaderFactory
接口负责创建MetadataReader
的工厂,一般使用默认的实现类SimpleMetadataReaderFactory
5. 测试
5.1 资源加载
本测试实现了两个目标,一是加载配置文件,二是对配置文件进行解析。首先在 test/resources 目录下创建文件 jdbc.properties
,这是数据库连接的配置文件,内容如下:
url=jdbc:mysql:///spring-wheel
namename=root
password=root
在测试方法中,先加载配置文件,然后转换成 Properties
类型的数据,最后遍历集合打印所有的键值对。需要注意的是,由于 AbstractApplicationContext
继承了 DefaultResourceLoader
,因此直接调用 getResource
方法加载配置文件。我们也可以不使用 Spring 容器,直接创建 DefaultResourceLoader
实例也能达到同样效果。
//测试方法
@Test
public void testResourceLoader() throws IOException {
GenericApplicationContext context = new GenericApplicationContext();
Resource resource = context.getResource("classpath:jdbc.properties");
Properties properties = new Properties();
properties.load(resource.getInputStream());
System.out.println("ResourceLoader 测试");
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
System.out.println(entry.getKey() + " --> " + entry.getValue());
}
}
从测试结果来看,打印的信息和配置文件中的内容一致,只是键值对的顺序发生了改变,这是因为键是按照英文字母的顺序进行排列的。
ResourceLoader 测试
password --> root
url --> jdbc:mysql:///spring-wheel
username --> root
5.2 元数据解析
本测试实现了两个目标,一是加载字节码文件,二是对元数据进行解析。测试方法比较复杂,可以分为三步,如下所示:
-
创建 Spring 容器,加载指定类的资源。需要注意的是,测试代码在执行时,源码被编译成字节码,因此文件的后缀名必须是
.class
。 -
创建
MetadataReaderFactory
实例,通过 ASM 方式对资源进行解析,得到的AnnotationMetadata
包含了类的相关信息。 -
创建
BeanDefinition
对象,指定className
属性。需要注意的是,通过 ASM 方式解析的元数据不能直接得到类的Class
信息,因此只能指定全类名。
//测试方法
@Test
public void testResolveMetadata() throws Exception {
//1. 加载资源
GenericApplicationContext context = new GenericApplicationContext();
Resource resource = context.getResource("classpath:context/basic/User.class");
//2. 使用 ASM 方式解析元数据
SimpleMetadataReaderFactory factory = new SimpleMetadataReaderFactory();
MetadataReader metadataReader = factory.getMetadataReader(resource);
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
//3. 创建BeanDefinition
RootBeanDefinition definition = new RootBeanDefinition();
definition.setBeanClassName(metadataReader.getAnnotationMetadata().getClassName());
context.registerBeanDefinition("user", definition);
context.refresh();
User user = context.getBean("user", User.class);
System.out.println("元数据解析测试:" + user);
}
从测试结果可以看到,User
对象注册到了 Spring 容器中。
元数据解析测试:context.test.basic.User@531be3c5
6. 总结
在得到初步的 ApplicationContext
实现之后,我们首要解决的是加载 Bean 的问题,也就是从字节码文件到注册 BeanDefinition
的过程。这一过程分为三步,首先将文件加载到程序中;其次对文件进行解析,获取类的相关信息;最后创建 BeanDefinition
实例,并注册到容器中。
Spring 将所有文件和数据都视为资源(resource),根据存储介质以及可读写性这两个维度来定义实现类。其中 ClassPathResource
是我们关心的,该类有两个特点,一是路径是固定为项目的类路径,可以方便地访问项目文件;二是资源是只读的,这就保证了代码和其他文件不会被修改。此外,Spring 提供 ResourceLoader
这个组件完成了读取文件并转换成资源的操作。
得到资源之后,接下来需要对资源进行解析。由于源文件的类型不同,解析资源的方式也是不同的。对于字节码文件来说,我们需要获取相关的类信息。Spring 提供了两种解析方式,一是基于 JVM 的类加载机制,二是通过 ASM 框架实现。Spring 对这两种解析方式进行统一地抽象,使用注解元数据来描述。
总的来说,资源加载和资源解析是 Spring 框架提供的通用功能,不仅可以将字节码解析为注解元数据(包含类的信息),也可以处理其他文件。比如配置文件和静态文件等和项目有关的资源,它们也有对应的解析方式,但这不是本节的重点,我们将在后续内容中专门讨论配置文件和静态文件是如何解析的。
7. 项目结构
新增修改一览,新增(1),修改(4)。
context
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring.context
│ ├─ support
│ │ ├─ AbstractApplicationContext.java (*)
│ │ └─ ApplicationContextAwareProcessor.java (*)
│ └─ ApplicationContext.java (*)
└─ test
├─ java
│ └─ context
│ └─ basic
│ └─ BasicTest.java (*)
└─ resources
└─ jdbc.properties (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。