一、Spring结构组成
了解Spring之前需要先了解Spring中的两个核心类。
- DefaultListableBeanFactory
DefaultListableBeanFactory 是整合Bean加载的核心部分,是Spring注册几家在的Bean的默认实现。XmlBeanFactory继承DefaultListableBeanFactory,对于XmlBeanFactory与DefaultListableBeanFactory不同的地方在于XmlBeanFactory中使用了自定义的XML读取器XmlBeanDefinitionReader,XmlBeanDefinitionReader继承AbstractBeanDefinitionReader实现了个性化的BeanDefinitionReader读取。反过来说DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory并实现了ConfigurableListableBeanFactory以及BeanDefinitionRegistry接口,下图是相关uml图:
从上面的类图以及层次结构中,我们可以很清晰的从全局角度了解DefaultListableBeanFactory的脉络。
1 AliasRegistry:定义堆alias的简单增删改等操作;
2 SimpleAliasRegistry:主要使用Map(ConcurrentHashMap)作为alias的缓存,并对接口AliasRegistry进行实现;
3 SingletonBeanRegistry:定义堆单例的注册及获取;
4 BeanFactory:定义获取bean及bean的属性;
5 DefaultSingletonBeanRegistry:对接口 SingletonBeanRegistry 各函数的实现 对SingletonBeanRegistry实现类继承;
6 HierarchicalBeanFactory:继承了BeanFactory并在此基础上增加了对ParentBeanFactory的支持;
7 BeanDefinitionRegistry:定义了对BeanDefinition的增删改操作;
8 BeanDefinitionRegistry:在DefaultSingletonBeanRegistry的基础上增加了对FactoryBean的处理;
9 ConfigurableBeanFactory:提供了对factory的各种方法;
10 ListableBeanFactory:根据各种条件获取bean的配置清单;
11 AbstractBeanFactory:综合了FactoryBeanRegistrySupport及ConfigurableBeanFactory功能;
12 加粗样式:综合了FactoryBeanRegistrySupport及ConfigurableBeanFactory功能;
13 AbstractAutowireCapableBeanFactory:合AbstractBeanFactory并对AutowireCapableBeanFactory进行实现;
14 ConfigurableListableBeanFactory:Beanfactory配置清单,指定忽略类型及接口等;
15 DefaultListableBeanFactory:综合上面所有功能, 主要是对 bean 注册后的处理;
XmlBeanFactory:是对DefaultListableBeanFactory的扩展,主要是从xml文档中读取BeanDefinition,对于注册及获取的bean都是从父类DefaultListableBeanFactory继承的方法中实现,与父类不同的个性化实现就是增加了XmlBeanDefinitionReader自定义读取器,在XmlBeanFactory主要使用reader属性对资源文件进行读取和注册。
- XmlBeanDefinitionReader
xml配置文件的读取时spring中的重要功能,因为spring的大部分功能都是以配置文件作为切入点的,可以从XmlBeanDefinitionReader中梳理资源文件的读取、解析及注册大致脉络
1 ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource (属于io流操作);
2 BeanDefinitionReader :主要定义资源文件读取并转换为 BeanDefinition 的各个功能;
3 EnvironmentCapable:定义获取Environment的方法;
4 AbstractBeanDefinitionReader:对BeanDefinitionReader、EnvironmentCapable进行实现;
5 DocumentLoader:定义从资源文件加载转为Document功能;
6 BeanDefinitionDocumentReader:定义读取 Docuemnt 并注册 BeanDefinition 功能;
7 BeanDefinitionParserDelegate:定义解析 Element 的各种方法
XML配置文件读取的大致流程,在XMLBeanDifinitionReader中主要包含以下几个步骤:
1、通过继承自 AbstractBeanDefinitionReader 中的方法,来使用ResourLoader将资源文件路径转换为对应的Resource文件;
2、通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件;
3、通过实现接口 BeanDefinitionDocumentReader 的 DefaultBeanDefinitionDocumentReader 类对Document进行解析,并使用 BeanDefinitionParserDelegate 对Element 进行解析。
二、容器的基础 XmlBeanFactory
1.基于xml文件的配置
// 定义 BeanFactoryTest.xml 配置
<bean id="mtTestBean" class="bean.MyTestBean"/>
BeanFactory bf= new XmlBeanFactory (new ClassPathResource ("beanFactoryTest.xml")),
2.运行时序图
从上述时序图看出BeanFactory首先调用ClassPathResource的构造函数来构造Resource资源文件的实例对象,通过XmlBeanFactory的构造方法注入Resources,调用XmlBeanDefinitionReader来定义Resources资源文件注入Bean对象
3.配置文件封装
BeanFactory bf= new XmlBeanFactory (new ClassPathResource ("beanFactoryTest.xml")),
配置文件的读取是通过ClaaPathResource进行封装的,那么是怎么进行封装的:
Java中将不同的来源的资源抽象程URL,通过注册不同的Hander(URLStreamHandler)来处理不同来源的资源的读取逻辑,一般handler的类型使用不同的前缀(协议、protocol)来识别,如file、http:、jar等,然而URL没有默认定义相对Classpath或ServletContext等资源的handler,虽然可以注册到子级的URLStreamHandler来解析特定的URL前缀(协议),比如ClassPath 然而这需要了解URL的实现机制,而且URl也没有提供基本的方法,如检查当前资源是否存在、是否可读等方法,因而spring对其内部实现了自己的抽象结构:Resources接口封装底层资源。
package org.springframework.core.io;
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
package org.springframework.core.io;
public interface Resource extends InputStreamSource {
boolean exists ();
boolean isReadable() ;
boolean isOpeη ();
URL getURL() throws IOException ;
URI getURI() throws IOException ;
File getFile() throws IOException ;
long lastModified() throws IOException ;
Resource createRelative (S tring relativePath) throws IOException ;
String getFilenarne() ;
String getDescription() ;
}
InputStreamSource封装任何能返回InputStream的类,比如File、 Classpath下的资源和Byte Array等。它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象。
Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性( exists)、可读性(isReadable)、是否处于打开状态(isOpen )。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取 lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法。为了便于
操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative()。在错误处
理中需要详细地打印出错的资源文件,因而Resource还提供了getDescription()方法用来在错
误处理中打印信息。
对不同来源的资源文件都有相应的 Resource 实现 文件( FileSystemResource) Classpath
资源( ClassPathResource )、 URL 资源( UrlResource )、 InputStream 资源( InputStreamResource ) Byte 数组( ByteArrayResource )等
当Resource相关类完成对配置文件进行封装后配置文件的读取工作就交给XmlBeanDefinitionReader来处理
// XmlBeanFactory.java
public XmlBeanFactory(Resource resource) throws BeansException {
// 调用XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory)构造方法
this(resource, null);
}
// 构造方法内部再次调用内部构造函数
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
// 初始化操作
super(parentBeanFactory);
// 资源加载的真正实现
this.reader.loadBeanDefinitions(resource);
}
// 初始化操作 跟踪到父类源码中 DefaultListableBeanFactory
public DefaultListableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
super(parentBeanFactory);
}
// 继续跟进 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#AbstractAutowireCapableBeanFactory(org.springframework.beans.factory.BeanFactory)
public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) {
this();
setParentBeanFactory(parentBeanFactory);
}
// this();
public AbstractAutowireCapableBeanFactory() {
super(); // 空参构造
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
}
IgnoreDependencyInterface方法的主要功能是忽略给定接口的自动装配功能,两个问题:目的是什么? 功能是什么?
举例:当A中有属性B时,当Spring获取A的Bean的时候如果属性B还没初始化,那么Spring会自动初始化B,这也是Spring的一个重要的属性,在某些情况下B并不会初始化,其中一种情况就是B实现了BeanNameAware接口。Sping是这样介绍的:自动装配时,忽略给定的依赖接口,典型的应用就是通过其他方式解析Application上下文注册依赖,类似于BeanFactory通过BeanFactoryAware进行注入或者ApplicationContext通过ApplicationContextAware进行注入。
4.加载Bean
在之前提到的XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类的reader属性提供的方法this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点,下图是调用的时序图:
加载XML文档和解析注册Bean会在稍后讲解,先分析下上述时序图
处理过程如下:
1、封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource进行封装
// XmlBeanDefinitionReader
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
注:EncodedResource的作用是对资源进行编译操作,主要的逻辑体现在getReader()方法中 。设置编码属性的时候Spring就会使用相应的编码作为输入流的编码
public Reader getReader() throws IOException {
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
else if (this.encoding != null) {
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
}
else {
return new InputStreamReader(this.resource.getInputStream());
}
}
上面构造了一个有比那吗(encoding)的InputStreamReader,当构造好EncodedResource对象后,再次转入了可复用方法loadBeanDefinitions(new EncodedResource(resource)),这个方法内部才是真正的数据准备阶段。
2、获取输入流。从Resource中获取对应的InputStream并构造InputSource。通过构造的InputSource 实例和Resource实例继续调用函数doLoadBeanDefinitions
// XmlBeanDefinitionReader
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource);
}
// 通过属性记录已加载的资源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// 从encodedResource获取resource对象在获取InputStream
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 真正进入核心逻辑部分
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
先做个梳理:首先对传入的resource参数进行封装,目的是开撸到Resource可能存在编码要求情况;其次通过SAX读取XML文件的方式来准备InputSource对象;最后将准备的数据通过参数参入真正的核心处理部分 doLoadBeanDefinitions(inputSource, encodedResource.getResource());
3、现在分析 doLoadBeanDefinitions(inputSource, encodedResource.getResource()) 核心逻辑方法
// XmlBeanDefinitionReader
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
}
... ...
}
上述代码除了抛出的异常,做了三件事,且必不可少
1、获取对xml文件的验证模式(在doLoadDocument方法中进行验证)
2、加载xml 获取Document 对象
3、根据document对象注册Bean对象信息 (较复杂)
这三个步骤支撑着整个Spring容器部分的实现,尤其是第三步对文件的解析,下面将捉个介绍。
三、获取XML的验证模式
XML文件的验证模式保证了XML文件的正确性,而比较常用的验证模式有两种:DTD和XSD。
1.DTD和XSD的区别
DTD ( Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。
一个DTD文档包含:元素的定义规则、元素间关系的定义规则、元素可使用的属性、可使用的实体或符号规则。
要使用DTD 验证模式的时候需要在XML文件的头部声明,以下是在Spring中使用DTD声明方式的代码:
<!-- 声明式代码 -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"https://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
... ...
</beans>
以spring为例 具体的DTD代码如下
<!ELEMENT beans (
description?,
(import | alias | bean)*
)>
<!--
Default values for all bean definitions. Can be overridden at
the "bean" level. See those attribute definitions for details.
-->
<!ATTLIST beans default-lazy-init (true | false) "false">
<!ATTLIST beans default-merge (true | false) "false">
<!ATTLIST beans default-autowire (no | byName | byType | constructor | autodetect) "no">
<!ATTLIST beans default-init-method CDATA #IMPLIED>
<!ATTLIST beans default-destroy-method CDATA #IMPLIED>
XML Schema语言就是XSD(XML Schemas Definition )。XML Schema描述了XML文档的结构。可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。文档设计者可以通过XML Schema指定XML文档所允许的结构和内容,并可据此检查XML文档是否是有效的。XML Schema本身是XML文档,它符合XML语法结构。可以用通用的XML解析器解析它。
在使用XML Schema文档对XML实例文档进行检验,除了要声明名称空间外( xmIns=http:/www.Springframework.org/schema/beans),还必须指定该名称空间所对应的XML Schema文档的存储位置。通过schemaLocation属性来指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的 URI,另一部分就是该名称空间所标识的XML Schema文件位置或URL地址(xsi:schemaLocation="http:/www.springframework.org/schema/beans htp://www.Springframework.org/schema/beans/Spring-beans.xsd )。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://www.springframework.org/schema/tool"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.springframework.org/schema/tool"
elementFormDefault="qualified">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/><xsd:annotation>
<xsd:complexType name="typedParameterType">
<xsd:attribute name="type" type="xsd:string" use="required"/>
</xsd:complexType>
2.验证模式的读取
了解了DTD与XSD 的区别后我们再去分析Spring 中对于验证模式的提取就更容易理解了。通过之前的分析我们锁定了Spring通过getValidationModeForResource方法来获取对应资源的的验证模式。
// org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadDocument
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
// getValidationModeForResource(resource)
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
// Hmm, we didn't get a clear indication... Let's assume XSD,
// since apparently no DTD declaration has been found up until
// detection stopped (before finding the document's root tag).
return VALIDATION_XSD;
}
方法的实现其实还是很简单的,无非是如果设定了验证模式则使用设定的验证模式,否则使用自动检测的方式。而自动检测验证模式的功能是在函数 detectValidationMode方法中实现的,在detectValidationMode函数中又将自动检测验证模式的工作委托给了专门处理类XmlValidationModeDetector,调用了XmlValidationModeDetector的 validationModeDetector方法,具体代码如下:
// XmlBeanDefinitionReader
protected int detectValidationMode(Resource resource) {
... ...
//@link XmlValidationModeDetector#detectValidationMode
return this.validationModeDetector.detectValidationMode(inputStream);
... ...
}
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
content = consumeCommentTokens(content);
// 如果是注释或者是空则跳过
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
// 读取到<开始符号,验证模式一定会在开始符号之前
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
catch (CharConversionException ex) {
// Choked on some character encoding...
// Leave the decision up to the caller.
return VALIDATION_AUTO;
}
finally {
reader.close();
}
}
只要我们理解了XSD与 DTD的使用方法,理解上面的代码应该不会太难,Spring用来检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。
四、获取Document
Document doc = doLoadDocument(inputSource, resource);
Document的获取是XmlBeanDefinitionReader委托给DocumentLoader中的loadDocument接口去调用,由DefaultDocumentLoader去执行
// org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadDocument
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
public interface DocumentLoader {
// 上个代码块中的 this.documentLoader.loadDocument 方法
Document loadDocument(
InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware)
throws Exception;
}
// org.springframework.beans.factory.xml.DefaultDocumentLoader#loadDocument
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
通过Sax解析xml文档都差不多,spring这里同样创建DocumentbuilderFactory 再通过DocumentBuilderFactory创建DocumentBuilder,进而解析InputSource返回Document对象,
//@link createDocumentBuilder
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
@Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
throws ParserConfigurationException {
DocumentBuilder docBuilder = factory.newDocumentBuilder();
if (entityResolver != null) {
docBuilder.setEntityResolver(entityResolver);
}
if (errorHandler != null) {
docBuilder.setErrorHandler(errorHandler);
}
return docBuilder;
}
// 获取Document对象
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, this.getEntityResolver(), this.errorHandler, this.getValidationModeForResource(resource), this.isNamespaceAware());
}
// 在上述获取Document对象种 this.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有什么作用
在 loadDocument方法中涉及一个参数EntityResolver,何为EntityResolver?官网这样解释:如果SAX应用程序需要实现自定义处理外部实体,则必须实现此接口并使用setEntityResolver方法向SAX驱动器注册一个实例。
也就是说,对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对文档进行一个验证。默认的寻找规则,即通过网络(实现上就是声明的DTD的URI地址)来下载相应的DTD声明,并进行认证。
下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的DTD声明没有被找到的原因。
EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可。这样就避免了通过网络来寻找相应的声明。
首先看entityResolver的接口方法声明:
public interface EntityResolver {
public abstract InputSource resolveEntity (String publicId,
String systemId)
throws SAXException, IOException;
}
这里有两个参数:publicId、systemId 返回InputSource 对象,这里以也顶配置文件解析
1、如果在这里解析验证模式位XSD的配置文件,代码如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xsi:schemaLocation="http://www.Springframework.org/schema/beans
http://www.springframework.org/schema/beans/Spring-beans.xsd">
</beans>
读取到一下两个参数
publicId : null
systemId: http://www.springframework.org/schema/beans/Spring-beans.xsd
2、若在解析验证模式为DTD的配置文件 代码如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//Spring//DPD BEAN2.0//EN"
"http://www.Springframework.org/dtd/Spring-beans-2.0.dtd">
<beans>
......
</beans>
读取到两个参数
publicId:-//Spring//DPD BEAN2.0//EN
systemId:http://www.Springframework.org/dtd/Spring-beans-2.0.dtd
之前已经提到过,验证文件默认的加载方式是通过URL 进行网络下载获取,这样会造成延迟,用户体验也不好,一般的做法都是将验证文件放置在自己的工程里,那么怎么做才能将这个URL转换为自己工程里对应的地址文件呢?我们以加载DTD文件为例来看看Spring中是如何实现的。
根据之前Spring中通过getEntityResolver()方法对EntityResolver的获取,我们知道,Spring 中使用DelegatingEntityResolver类为EntityResolver的实现类,resolveEntity实现方法如下:
// DelegatingEntityResolver
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
throws SAXException, IOException {
if (systemId != null) {
// 若是DTD从这里解析
if (systemId.endsWith(DTD_SUFFIX)) { // DTD_SUFFIX = .dtd
return this.dtdResolver.resolveEntity(publicId, systemId);
}
// 若是XSD从这里解析
else if (systemId.endsWith(XSD_SUFFIX)) { // XSD_SUFFIX = .xsd
return this.schemaResolver.resolveEntity(publicId, systemId);
}
}
// Fall back to the parser's default behavior.
return null;
}
我们可以看到,对不同的验证模式,Spring使用了不同的解析器解析。这里简单描述一下原理,比如加载DTD类型的BeansDtdResolver的resolveEntity是直接截取systemld最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity 是默认到META-INF/Spring.schemas文件中找到systemid所对应的XSD文件并加载。
// org.springframework.beans.factory.xml.BeansDtdResolver#resolveEntity
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Trying to resolve XML entity with public ID [" + publicId + "] and system ID [" + systemId + "]");
}
if (systemId != null && systemId.endsWith(".dtd")) {
int lastPathSeparator = systemId.lastIndexOf(47);
int dtdNameStart = systemId.indexOf("spring-beans", lastPathSeparator);
if (dtdNameStart != -1) {
String dtdFile = "spring-beans.dtd";
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
}
try {
Resource resource = new ClassPathResource(dtdFile, this.getClass());
InputSource source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isTraceEnabled()) {
logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
}
return source;
} catch (FileNotFoundException var8) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", var8);
}
}
}
}
return null;
}
五、解析及注册BeanDefinitions
// org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadBeanDefinitions
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
... ...
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
... ...
当把文件转为Document后 接下来的提取及注册Bean就是我们的重头戏。继续上述分析,当程序已拥有xml文档文件的Document实例对象时,就会被引入下面的方法
// XmlBeanDefinitionReader#registerBeanDefinitions(Document doc, Resource resource)
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 用DefaultBeanDefinitionDocumentReader 实例化 BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 记录加载前BeanDefinition的加载个数
int countBefore = getRegistry().getBeanDefinitionCount();
// 加载注册Bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 记录本次加载的BeanDefinitionCount个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
其中的参数doc是通过上一节loadDocument加载转换出来的。在这个方法中很好地应用了面向对象中单一职责的原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是BeanDefinitionDocumentReader。BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReaderO中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续
BeanDefinition的注册。
// org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#registerBeanDefinitions
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
doRegisterBeanDefinitions(root);
}
核心逻辑
// org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions
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.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
// 解析前处理 留给子类实现
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
// 解析后处理 留给子类实现
postProcessXml(root);
this.delegate = parent;
}
通过上面的代码我们看到了处理流程,首先是对profile的处理,然后开始进行解析,可是当我们跟进preProcessXml(root)或者postProcessXml(root)发现代码是空的,既然是空的写着还有什么用呢?
就像面向对象设计方法学中常说的一句话,一个类要么是面向继承的设计的,要么就用final修饰。
在 DefaultBeanDefinitionDocumentReader中并没有用final修饰,所以它是面向继承而设计的。这两个方法正是为子类而设计的,如果读者有了解过设计模式,可以很快速地反映出这是模版方法模式,如果继承自DefaultBeanDefinitionDocumentReader的子类需要在Bean解析前后做一些处理的话,那么只需要重写这两个方法就可以了。
profile 属性的使用
在注册的Bean最开始是对PROFILE_ATTRIBUTE属性进行解析,profile属性不常用
<beans profile="dev">
... ... </beans>
集成到web环境中,在web.xml中加入以下代码
<context-param>
<param-name>Spring.profiles.active</param-name>
<param-value>dev</param-value>
</context-param>
解析并注册BeanDefinition
// org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader#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) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 默认命名空间解析
parseDefaultElement(ele, delegate);
}
else {
// 自定义命名空间解析
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
对于根节点或者子节点如果是默认命名空间的话则采用parseDefaultElement方法进行解析,否则使用delegate.parseCustomElement方法对自定义命名空间进行解析。
而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并与Spring 中固定的命名空间http://www.springframework.org/schema/beans进行比对。如果一致则认为是默认,否则就认为时候自定义
注:
参考自 《Spring源码深度解析(第2版)》