Spring源码系列:容器的基本实现
前言
什么是容器?
Spring容器
是Application
的一个实例对象。- 容器负责的实例化、配置
Bean
、管理Bean
的生命周期。 Spring容器
将我们代码中的PoJo
类、XML
配置文件转化为一个可用的系统。
那么接下来会以spring-beans
包下的一些核心类展开来讲解容器是怎么实现的。
一. 容器的基本实现
要想了解Spring容器的概念和深入源码,相信一切都得从这个容器的出生开始,那么在此以DefaultListableBeanFactory
为切入点来讲解。
那么DefaultListableBeanFactory
类是干什么的呢?用百度翻译一下源码中的注释,如下:
- 作为
ConfigurableListableBeanFactory
和BeanDefinitionRegistry
接口的默认实现。 - 一个基于
bean
定义元数据的工厂类,用于注册所有的bean
。(可能是PoJo
类、配置文件)。 - 可以操作预先解析的
bean
元数据对象。
那么来看下这个类的关系图:
我们可以重点关注图中蓝色框圈起来的部分,我们可以做个总结,DefaultListableBeanFactory
类对bean
的作用有两个方向:
- 进行监听,对于满足条件的
bean
进行定义和注册。 - 对
bean
进行增删改查等操作(一些动作实现)。
DefaultListableBeanFactory
作为整个bean
加载的核心部分,是Spring注册和加载bean
的一个默认实现。其还有个子类XmlBeanFactory
,主要用于从XML
文件中读取BeanDefinition
。
我个人理解是这样的:
- 一些
PoJo
类,其加载一般交给DefaultListableBeanFactory
来执行。 - 而对于
XML
形式配置的Bean
,则交给XmlBeanFactory
来执行。
Tip:
Definition
是什么意思?其单词本意是:定义。
而BeanDefinition
像是对Bean
的一个抽象模板,定义了Bean
的一些行为、属性等。
那么自然而然的,BeanDefinitionRegistry
就是该模板的注册器了。
1.1 资源的读取
Spring的大部分功能都是以配置作为切入点。而上文提到的,XmlBeanFactory
负责XML
配置形式的Bean
的加载。而从XML
这类资源文件中读取、解析以及注册等流程,则交给XmlBeanDefinitionReader
来完成。
来看下XmlBeanDefinitionReader
的类关系图:
XmlBeanDefinitionReader
类下有这么几个重要的成员:
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
// 定义从资源文件加载到转化为Document的功能
private DocumentLoader documentLoader = new DefaultDocumentLoader();
// 前者负责转化为Document,那么documentReaderClass 就负责读取Document 并注册 BeanDefinition
private Class<? extends BeanDefinitionDocumentReader> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;
}
XmlBeanDefinitionReader
类的父类AbstractBeanDefinitionReader
下又有这么几个重要的成员:
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {
// 定义资源加载器,主要应用于根据给定的资源文件地址,返回对应的Resource。
private ResourceLoader resourceLoader;
}
将上文做个总结,XmlBeanDefinitionReader
主要做的事情就是:
- 利用父类
AbstractBeanDefinitionReader
的ResourceLoader
来将资源文件路径转化为对应的Resource
。 - 将
Resource
进行文件转换,转换为Document
文件。 - 使用
DefaultBeanDefinitionDocumentReader
进行文件解析。
1.1.1 Resource资源
Spring的配置文件是通过ClassPathResource
来封装的,我们来看下他的类关系图:
顶层接口InputStreamSource
只提供了一个方法:提供返回InputStream
流的方法。
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
Resource
接口用于封装底层资源,抽象了所有Spring内部使用到的资源:Flie
、URL
、Classpath
等。提供了3个判断当前资源状态的方法。
- 存在性
exists()
。 - 可读性
isReadable()
。 - 是否处于打开状态
isOpen()
。
同时还提供了不同资源到URL
、URI
、File
类型的转换,Resource
接口的具体实现有:
FileSystemResource
(文件)。ClassPathResource
(ClassPath
资源)。UrlResource
(URL
资源)。InputStreamSource
(InputStream
资源)。ByteArrayResource
(Byte
数组)。
1.2 资源的加载和解析
在Spring将配置文件封装为Resource
类型的实例后,就会由XmlBeanDefinitionReader
来完成资源加载。
我们来直接看其核心方法loadBeanDefinitions()
,上文读取好的资源文件(Resource
实例)则作为其参数传入:
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
}
直观的来看这个方法,我们发现,在做Bean
加载之前,会对Resource
实例对象进行编码。
// 先对Resource资源进行编码封装
new EncodedResource(resource)
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Loading XML bean definitions from " + encodedResource);
}
// 用来记录 已经加载完成的资源 的Set集合
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
// 1.如果发现,该资源已经被加载过,那么抛异常,说明你这个资源重复加载了。
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
// 2.获取每个Resource资源对应的inputStream流
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
InputSource inputSource = new InputSource(inputStream);
// 2.1 设置对应的编码,这是考虑到Resource可能存在编码要求的情况
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 2.2 进行真正的Bean加载
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
// ...
}
再看下核心的doLoadBeanDefinitions(inputSource, encodedResource.getResource())
方法:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
// 以下都是各种catch方法,我们主要关注try语句块中做的事情即可。
}
这个方法主要做三件事情:
- 获取
XML
文件的验证模式。 - 加载
XML
文件,并得到对应的Document
。 - 根据返回的
Document
注册Bean
信息。
而doLoadDocument()
这个方法,则做了前两件事情。我们以这行代码为切入点,来展开。
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
// 这里的documentLoader指的是上文提到的DefaultDocumentLoader,负责将Resource实例转化为Document
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
1.2.1 获取XML的验证模式
XML
的验证模式有什么用?其保证了XML
文件的正确性。常用的验证模式有两种:
DTD
Document Type Definition:文档类型定义,一种XML
约束模式语言,是XML
文件的验证机制。 属于XML
文件组成的一部分。可以通过比较XML
文档和DTD
文件来判断文档是否符合规范。一个DTD
文档包含:
- 元素的定义规则。
- 元素间关系的定义规则。
- 元素可使用的属性。
- 可使用的实体或者符号规则。
DTD案例:注意DOCTYPE
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
</beans>
XSD
XML Schemas Definition:XML Schema
语言就是XSD。描述了XML
文档的结构。XML Schema
本身就是XML
文档,符合其语法结构,可以用通用的XML
解析器来解析。一个XSD
包括:
- 文档中出现的元素。
- 文档中出现的属性、子元素。
- 子元素的数量和顺序。
- 元素是否为空。
- 元素和属性的数据类型。
- 元素或属性的默认和固定值。
XSD案例:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
</beans>
使用XML文档的时候,必须做到几点:
- 声明名称空间
xmlns="http://www.springframework.org/schema/beans"
。 - 指定该名称空间对应的
XML Schema
文档的存储位置xsi:schemaLocation="xxx"
。一部分是名称空间的URI
,另一部分是该名称空间所标识的XML Schema
文件位置或者URL
地址。
其他:
- 声明
XML Schema
实例xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
言归正传,我们回到代码本身,来关注下getValidationModeForResource()
方法:
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
// 如果手动制定了验证模式,则使用指定的验证模式
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
// 如果没有手动指定,那么使用自动检测
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
return VALIDATION_XSD;
}
其实detectValidationMode()
方法并不是很难理解,我只会贴出最最核心的代码:
private static final String DOCTYPE = "DOCTYPE";
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
private boolean hasDoctype(String content) {
return content.contains(DOCTYPE);
}
说白了就是,如果发现文档中包含了DOCTYPE
,该XML模式就是DTD
,否则就是XSD
。(看到这里可以回顾下上文的DTD
案例)
1.2.2 获取Document
在验证完XML
模式的合法性后,会将Resource
实例转化为Document
,再来回顾这行代码:
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
// 这里的documentLoader指的是上文提到的DefaultDocumentLoader,负责将Resource实例转化为Document
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
其本质是通过SAX
解析XML
文档。
SAX
解析:逐行扫描文档,一边扫描一边解析。其工作原理简单地说就是:
对文档进行顺序扫描,当扫描到文档(document)开始与结束、元素(element)开始与结束、文档(document)结束等地方时通知事件处理函数,由事件处理函数做相应动作,然后继续同样的扫描,直至文档结束。
我们先来看下getEntityResolver()
这个方法是干什么的:
protected EntityResolver getEntityResolver() {
if (this.entityResolver == null) {
// Determine default EntityResolver to use.
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader != null) {
this.entityResolver = new ResourceEntityResolver(resourceLoader);
}
else {
this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
}
}
return this.entityResolver;
}
EntityResolver
什么是EntityResolver
?
如果SAX应用程序需要实现自定义的处理外部实体,则必须实现此接口并使用
setEntityResolver
方法向SAX驱动器注册一个实例。
而对于项目本身而言,则可以提供一个寻找DTD声明的方法。
我们来看下EntityResolver
接口:
public interface EntityResolver {
public abstract InputSource resolveEntity (String publicId, String systemId) throws SAXException, IOException;
}
他接收俩参数,publiId
和systemId
以上文的DTD
和XSD
案例为例,若是读取XSD
配置文件,则获得的参数如下:
publiId
:null
systemId
:http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
若读取的是DTD
文件:
publiId
:-//SPRING//DTD BEAN 2.0//EN
systemId
:http://www.springframework.org/dtd/spring-beans-2.0.dtd
为啥会出现不同呢?Spring使用DelegatingEntityResolver
来实现该接口:
public class DelegatingEntityResolver implements EntityResolver {
public static final String DTD_SUFFIX = ".dtd";
public static final String XSD_SUFFIX = ".xsd";
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws SAXException, IOException {
if (systemId != null) {
// 若加载dtd类型,则直接截取systemId最后的xx.dtd,然后去当前路径下寻找
if (systemId.endsWith(DTD_SUFFIX)) {
return this.dtdResolver.resolveEntity(publicId, systemId);
}
// 若加载xsd类型,则默认到META-INF/Spring.schemas文件中找到systemId对应的XSD文件并加载。
else if (systemId.endsWith(XSD_SUFFIX)) {
return this.schemaResolver.resolveEntity(publicId, systemId);
}
}
return null;
}
}
最后,关于如何转换Document
,即loadDocument()
方法的最终实现就简单概括,其由DefaultDocumentLoader
来完成。
public class DefaultDocumentLoader implements DocumentLoader {
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
}
主要做三件事:
- 创建
DocumentBuilderFactory
工厂。 - 工厂创建一个文档构造器
DocumentBuilder
。 - 解析
inputSource
来生成Document
对象。
1.2.3 解析和注册BeanDefinition
上文的代码里,只剩下这行代码没有讲解了,也就是在将文件转化为Document
后,重点做的事情:提取和注册Bean
。
// doc则是1.2.2中获取到的Document
int count = registerBeanDefinitions(doc, resource);
代码展开:
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 1.实例化BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 2.获取之前已经加载好的BeanDefinition个数
int countBefore = getRegistry().getBeanDefinitionCount();
// 3.加载和注册Bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 4.记录本次加载的BeanDefinition个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
}
BeanDefinitionDocumentReader
只是一个接口,应用单一职责的原则,将具体的逻辑registerBeanDefinitions()
方法委托给单一的类去进行处理。具体的实现类为DefaultBeanDefinitionDocumentReader
,我们来看下其具体的实现:
public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}
protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
// 处理profile属性
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
log..
}
return;
}
}
}
// 解析前处理,交给子类实现。父类给子类提供模板,即模板模式的一个体现
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
// 解析后处理,交给子类实现
postProcessXml(root);
this.delegate = parent;
}
}
profile
属性,用于在配置文件中指定开发环境,这样可以方便的进行切换开发、部署环境。常用的是更换不同的数据库。
如同一个配置文件中:
<bean profile="dev">xx</bean>
<bean profile="production">xx</bean>
那么集成到Web环境中,则在web.xml
中加入以下代码:
<context-param>
<param-name>Spring.profiles.active</param-name>
<param-value>dev</param-value>
</context-param>
接下来再看看parseBeanDefinitions
方法:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// bean处理
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element ele) {
// bean处理
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}else {
// bean处理
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
Spring的XML配置中Bean的声明方式有两大类:
- 默认的:
<bean id = "xx" class="xx.xx"/>
- 自定义的:
<tx:annotation-driven/>
上述代码也就是对不同情况进行不同的Bean处理。其中核心的parseDefaultElement
和parseCustomElement
方法则在下文继续讲解。
1.3 总结
在这里,先对上文做个总结,方便大家思考和理解。
DefaultListableBeanFactory
作为Bean
加载的一个核心部分,是Spring注册和加载Bean
的一个默认实现。有两个重要的功能:
- 注册和加载
Bean
。(顶层实现AliasRegistry
) - 对
Bean
进行增删改查等操作。(顶层实现BeanFactory
)
Spring的大部分功能都是以配置作为切入点。XML
这类资源文件的读取、解析和注册都是在XmlBeanDefinitionReader
类中来完成。
资源读取:
- Spring有自己的资源接口
Resource
,用于将不同类型的资源对象抽象成Resource
实例对象。
资源解析:
XmlBeanDefinitionReader
的loadBeanDefinitions()
方法进行Bean
的加载解析。- 对
Resource
实例对象进行编码。 - 获取
XML
文件的验证模式(共两种:DTD
、XSD
)。 - 若验证通过,加载
XML
文件(使用Sax
解析,即一边扫描XML
一边解析),将Resource
实例对象中的InputStream
流转化为对应的Document
对象doc
。
资源注册:
- 根据
doc
来提取和注册Bean
。 - 通过
DefaultBeanDefinitionDocumentReader
的registerBeanDefinitions()
方法先处理profile
属性(用于在配置文件中指定开发环境)。 - 然后再解析标签处理生成
BeanDefinition
。
到这里Spring的Bean
容器(工厂)对资源的处理工作也就做完了,更深层次的,对于Bean
层面的解析和加载则交给后文。
二. 简单的小案例
我是在Spring5.0.x版本源码项目上,创建了自己的Test
,如图:
创建User
类:
public class User {
private int id;
private String name;
// get set
}
在test
目录下的resources
资源文件目录中,创建user.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<bean name="user" class="org.springframework.beans.User">
<property name="id" value="1" />
<property name="name" value="你好" />
</bean>
</beans>
测试类:
package org.springframework.beans;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
public class Test {
@org.junit.jupiter.api.Test
public void test() {
ClassPathResource resource = new ClassPathResource("user.xml");
XmlBeanFactory factory = new XmlBeanFactory(resource);
User user = (User) factory.getBean("user");
System.out.println(user.getId());
System.out.println(user.getName());
}
}
结果如下:
一般读取XML
形式的Bean
,有三步:
- 通过
ClassPathResource
加载对应的xml
文件。 - 通过
resource
实例对象创建出Bean
工厂XmlBeanFactory
。 Bean
工厂通过Name
来获取对应的Bean
。
备注:注意,XmlBeanFactory
对于Spring来说,已经是个过时的类了。不推荐使用。上述代码可以改为(本质一样的):
@org.junit.jupiter.api.Test
public void test() {
BeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
ClassPathResource resource = new ClassPathResource("user.xml");
reader.loadBeanDefinitions(resource);
User user = (User) factory.getBean("user");
System.out.println(user.getId());
System.out.println(user.getName());
}
本篇文章,从外层看,已经介绍了Bean
容器对资源的一个加载和解析流程,而上述的案例中,Spring是如何把XML
配置中的Bean
加载进来的?又是如何得到我们配置的字段值的?答案也就是源码中核心的parseDefaultElement
和parseCustomElement
方法。下篇文章则从标签的解析来做具体的展开介绍。