前言一. 容器的基本实现
1.1 资源的读取
1.1.1 Resource资源 1.2 资源的加载和解析
1.2.1 获取XML的验证模式
DTDXSD 1.2.2 获取document
EntityResolver 1.2.3 解析和注册BeanDefinition 1.3 总结 二. 简单的小案例
前言什么是容器?
- 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:
1.1 资源的读取Definition是什么意思?其单词本意是:定义。
而BeanDefinition像是对Bean的一个抽象模板,定义了Bean的一些行为、属性等。
那么自然而然的,BeanDefinitionRegistry就是该模板的注册器了。
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进行文件解析。
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数组)。
在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 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文件的正确性。常用的验证模式有两种:
DTDdocument Type Definition:文档类型定义,一种XML约束模式语言,是XML文件的验证机制。 属于XML文件组成的一部分。可以通过比较XML文档和DTD文件来判断文档是否符合规范。一个DTD文档包含:
- 元素的定义规则。元素间关系的定义规则。元素可使用的属性。可使用的实体或者符号规则。
DTD案例:注意DOCTYPE
XSD
XML Schemas Definition:XML Schema语言就是XSD。描述了XML文档的结构。XML Schema 本身就是XML文档,符合其语法结构,可以用通用的XML解析器来解析。一个XSD包括:
- 文档中出现的元素。文档中出现的属性、子元素。子元素的数量和顺序。元素是否为空。元素和属性的数据类型。元素或属性的默认和固定值。
XSD案例:
使用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:nullsystemId:http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
若读取的是DTD文件:
publiId:-//SPRING//DTD BEAN 2.0//ENsystemId: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对象。
上文的代码里,只剩下这行代码没有讲解了,也就是在将文件转化为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属性,用于在配置文件中指定开发环境,这样可以方便的进行切换开发、部署环境。常用的是更换不同的数据库。
如同一个配置文件中:
xx xx
那么集成到Web环境中,则在web.xml中加入以下代码:
Spring.profiles.active dev
接下来再看看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处理。其中核心的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文件:
测试类:
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方法。下篇文章则从标签的解析来做具体的展开介绍。



