Spring 源码分析 (一) 容器的基本实现

一、Spring结构组成

了解Spring之前需要先了解Spring中的两个核心类。

  1. 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属性对资源文件进行读取和注册。

  1. 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中主要包含以下几个步骤:
XmlBeanDefinitionReader
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 )等
ClassRelativeContextResource
当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),而这句代码则是整个资源加载的切入点,下图是调用的时序图:loadBeanDefinitions
加载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.验证模式的读取

了解了DTDXSD 的区别后我们再去分析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版)》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值