XmlBeanDefinitionReader实现

726 阅读5分钟

XmlBeanDefinitionReader的入口

从XmlBeanFactory寻找入口

BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource('application.xml'))先构造一个资源文件的实例对象,然后再构造BeanFactory。

Spring对其内部使用到的资源实现了自己的抽象结构,使用Resource接口封装底层资源。Resource继承自InputStreamSource,Java的InputStreamSource封装任何能返回InputStream的类,比如File,Classpath下的资源等,它只有一个方法getInputStream(),该方法返回一个InputStream对象。

Resource接口抽象了所有Spring内部使用到的底层资源,比如File,URL,ClassPath。对不同的来源的资源文件都有相应的Resource实现,有了Resource接口便可以对所有的资源文件进行统一处理。

XmlBeanfactory的初始化方法为:

// XmlBeanDefinitionReader用于真正加载资源中数据
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);

// parentBeanFactory为父类BeanFactory用于和factory合并,可以为空
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
   super(parentBeanFactory);
   this.reader.loadBeanDefinitions(resource);
}

this.reader.loadBeanDefinitions(resource)是加载资源的真正实现,也是资源加载的入口。

加载Bean

XmlBeanDefinitionReader加载资源的过程大概为:

  1. 封装资源文件,会将resource封装为EncodeResource
  2. 获取输入流,从resource中获取对于的InputStream并构造InputSource
  3. 通过构造InputSource实例和Resource实例继续调用doLoadBeanDefinitions loadBeanDefinitions实现为:
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
   return loadBeanDefinitions(new EncodedResource(resource));
}

EncodeResource用于对资源文件的编码进行处理,在这里只是简单的封装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);
   }

   // 记录已经加载过的Resource
   Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();

   // 避免玄幻加载
   if (!currentResources.add(encodedResource)) {
      throw new BeanDefinitionStoreException(
            "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
   }

   // 获取封装的资源
   try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
      // 构造InputSource实例
      InputSource inputSource = new InputSource(inputStream);
      if (encodedResource.getEncoding() != null) {
         inputSource.setEncoding(encodedResource.getEncoding());
      }
      // 核心逻辑
      return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException(
            "IOException parsing XML document from " + encodedResource.getResource(), ex);
   }
   finally {
      currentResources.remove(encodedResource);
      if (currentResources.isEmpty()) {
         this.resourcesCurrentlyBeingLoaded.remove();
      }
   }
}

loadBeanDefinitions主要是将Resource封装成InputSource对象,并且判断是否已经加载了该资源,如果加载了直接抛出异常,实际上还是资源的准备,doLoadBeanDefinitions()是核心处理部分。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
      throws BeanDefinitionStoreException {

   try {
      // 加载XML文件
      Document doc = doLoadDocument(inputSource, resource);
      // 注册Bean信息
      int count = registerBeanDefinitions(doc, resource);
      if (logger.isDebugEnabled()) {
         logger.debug("Loaded " + count + " bean definitions from " + resource);
      }
      return count;
   }
   catch (Exception ex) {
      throw ex;
   }
}

doLoadBeanDefinitions()主要包含了三部分,其中1和2均在doLoadDocument()中实现:

  1. 获取XML的文件验证模式
  2. 加载XML文件,并得到对应的Document
  3. 根据返回的Document注册Bean信息

XML的验证方式

DTD和XSD

DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分,由非XML语言编写。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来查看文档是否符合规范,元素和表钱是否正确。一个DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。使用DTD验证模式需要在XML文件的头部声明,Spring种的声明为

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
...
</beans>

XML Schema语言就是XSD(XML Schemas Definition)。XML Schema描述了XML文档的结构。可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合XML Schema的要求。文档设计者可以通过XML Schema指定XML文档所允许的结构和内容,并可根据此检查XML文档是否是有效的。Xml Schema本身是XML文档,它符合XML的语法结构,可以用通用的XML解析器解析。

在使用XML Schema文档对XML实例文档进行校验,除了要声明名称空间外(xmlns="http://www.springframework.org/schema/beans"),还必须指定该名称空间所对应的XML Schema文档存储的位置。通过schemaLocation属性来指定名称空间所对应的XML Schema文档的存储位置。它包含两部分,一部分是名称空间的URI,另一部分是该名称空间所标识的XML Schema文件位置或者URI地址(xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd)

验证模式的读取

doLoadDocument()是XmlBeanDefinitionReader类中的方法,该方法会使用this.documentLoader.loadDocument()加载XML文件,loadDocument()方法的第四个参数就是XML的验证模式,通过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;
}

如果没有指定XML的验证模式,则使用detectValidationMode()方法获取,该方法是XmlBeanDefinitionReader中的,主要是获取InputStream,然后再调用了XmlValidationModeDetector的detectValidationMode()方法获取验证模式。

public int detectValidationMode(InputStream inputStream) throws IOException {
   try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
      boolean isDtdValidated = false;
      String content;
      // 遍历XML文件
      while ((content = reader.readLine()) != null) {
         content = consumeCommentTokens(content);
         // 如果读取的行是空或者注释则略过
         if (this.inComment || !StringUtils.hasText(content)) {
            continue;
         }
         // 是否包含DOCTYPE
         if (hasDoctype(content)) {
            isDtdValidated = true;
            break;
         }
         // 读取到<开始符号,验证模式一定会在开始符号之前
         if (hasOpeningTag(content)) {
            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;
   }
}

Spring用了检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。

获取Document

真正将资源转为Document是在DefaultDocumentLoader中的loadDocument()方法实现的。方法中先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析inputSource来返回Document对象。

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);
}

解析并注册BeanDefinitions

把文档转为Document之后就是提取并注册Bean,该过程是在XmlBeanDefinitionReader中的registerBeanDefinitions()方法实现的

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
   // 实例化后的documentReader类型为DefaultBeanDefinitionDocumentReader
   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
   // 记录加载Bean之前的BeanDefinition的加载个数
   int countBefore = getRegistry().getBeanDefinitionCount();
   // 加载并注册bean
   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
   // 记录本次加载的BeanDefinition的个数
   return getRegistry().getBeanDefinitionCount() - countBefore;
}

进入到DefaultBeanDefinitionDocumentReader类中的registerBeanDefinitions()方法,该方法实际上调用了doRegisterBeanDefinitions()方法,并传入doc.getDocumentElement()

protected void doRegisterBeanDefinitions(Element root) {
   // 处理Profile属性
   BeanDefinitionParserDelegate parent = this.delegate;
   this.delegate = createDelegate(getReaderContext(), root, parent);

   if (this.delegate.isDefaultNamespace(root)) {
      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()) {
               logger.debug("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属性同时在配置文件中部署两套配置来适用于生产环境和开发环境,可以方便进行切换开发,部署环境。而解析profile的过程为:首先获取beans节点是否定义了profile属性,如果定义了则到环境变量中寻找,所以会先判断environment不可能为空,因为profile是可以同时指定多个的,需要程序对其拆分,并解析每个profile是都符合环境变量中所定义的,不定义则不浪费性能解析。

解析并注册BeanDefination

Spring的XML配置中里面由两大类Bean声明,一个是默认的<bean id="xxx" calss ="xx.xx"/>,另一类是自定义的,例如<tx:annotation-driven/>

根节点或者子节点如果是默认命名空间的话则采用parseDefaultElement()方法解析,否则使用delegate.parseCustomElement(root)方法对自定义命名空间进行解析。而判断是默认命名空间还是自定义命名空间的办法是使用node.getNamespaceURI()获取命名空间,然后并于Spring中固定的命名空间http://www.springframework.org/schema/beans进行对比,如果一致则认为是默认的声明,否则认为自定义声明。

parseBeanDefinitions()方法解析Bean时就时按照上述逻辑处理

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
   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);
   }
}