概述

这一篇开始进行 Document 加载了,XmlBeanFactoryReader 类对于文档读取并没有亲历亲为,而是委托给了DocumentLaoder 去执行,DocumentLoader 是个接口,真正调用的是 DefaultDocumentLoader。

解析代码

  1. /**
  2. * Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured
  3. * XML parser.
  4. */
  5. @Override
  6. public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
  7. ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
  8. DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
  9. if (logger.isDebugEnabled()) {
  10. logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
  11. }
  12. DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
  13. return builder.parse(inputSource);
  14. }

对于这部分代码其实并没有太多可以描述的,因为通过 SAX 解析 XML 文档的套路都差不多,Spring 在这里并没有什么特殊的地方,同样首先创建 DocumentBuilderFactory,再通过 DocumentBuilderFactory 创建DocumentBuilder,进而解析 inputSource 来返回 Document 对象。这里有必要提及一下 EntityResolver,对于参数 entityResolver,传入的是通过getEntityResolver() 函数获取的返回值,如下代码:

  1. protected EntityResolver getEntityResolver() {
  2. if (this.entityResolver == null) {
  3. // Determine default EntityResolver to use.
  4. ResourceLoader resourceLoader = getResourceLoader();
  5. if (resourceLoader != null) {
  6. this.entityResolver = new ResourceEntityResolver(resourceLoader);
  7. }
  8. else {
  9. this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
  10. }
  11. }
  12. return this.entityResolver;
  13. }

那么,EntityResolver 到底是做什么用的呢?


EntityResolver 用法

在 loadDocument 方法中涉及一个参数 EntityResolver,何为 EntitiResolver?官网这样解释:如果 SAX 应用程序需要实现自定义处理外部实体,则必须实现此接口并使用 setEntityResolver 方法向 SAX 驱动器注册一个实例。也就是说,对于解析一个 XML,SAX 首先读取该 XML 文档上的声明,根据声明去寻找相应的 DTD 定义,以便对文档进行一个验证。默认的寻找规则,即通过网络(实现上就是声明的 DTD 的 URL 地址)来下载相应的 DTD 声明,并进行认证。下载的过程漫长,而且当网络中断或不可用的时候,这里会报错,就是因为相应的 DTD 声明没有被找到的原因。
enntityResolver 的作用是项目本身就可以提供一个如何寻找 DTD 声明的方法,即由程序来实现寻找 DTD 声明的过程,比如我们将 DTD 文件放到项目中某处,在实现时直接将此文档读取并返回给 SAX 即可。这样就避免了通过网络来寻找相应的声明。

首先看enntityResolver的接口方法声明:

  1. public abstract InputSource resolveEntity (String publicId, String systemId)
  2. throws SAXException, IOException;

这里,它接受两个参数 publicId 和 systemId ,并返回一个 inputSource 对象。这里我们以特定配置文件来进行讲解。

(1)如果我们在解析验证模式为XSD的配置文件,代码如下:

读取到以下两个参数。

(2)如果我们在解析验证模式为DTD的配置文件,代码如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://www.springframework.org/schema/beans
  5. http://www.springframework.org/schema/beans/spring-beans.dtd">
  6. ......
  7. </beans>

读取到以下两个参数:

之前已经提到过,验证文件默认的加载方式是通过URL进行网络下载,这样会造成延时,用户体验也不好,一般的做法是将验证文件放置在自己的工程里,那么怎么做才能将这个 URL 转换为自己工程里对应的地址文件呢?我们以加载 DTD 文件为例来看看 Spring 中是如何实现的。根据之前 Spring 中通过 getEntityResolver() 方法对EntityResolver 的获取,我们知道,Spring 中使用 DelegatingEntityResolver 类为 EntityResolver 的实现类,resolveEntity实现方法如下:

  1. @Override
  2. @Nullable
  3. public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
  4. if (systemId != null) {
  5. if (systemId.endsWith(DTD_SUFFIX)) {
  6. // 如果是dtd从这里解析
  7. return this.dtdResolver.resolveEntity(publicId, systemId);
  8. }
  9. else if (systemId.endsWith(XSD_SUFFIX)) {
  10. // 通过调用META-INF/Spring.schemas解析
  11. return this.schemaResolver.resolveEntity(publicId, systemId);
  12. }
  13. }
  14. return null;
  15. }

我们可以看到,对不同的验证模式,Spring 使用了不同的解析器解析。这里简单描述一下原理,比如加载 DTD 类型的 BeanDtdResolver 的 resolveEntity 是直接截取 systemId 最后的 xx.dtd 然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver 类的resolveEntity 是默认到 META-INF/Spring.schemas 文件中找到 systemId 所对应的 XSD 文件并加载。下面是 BeansDtdResolver 的源码:

  1. @Override
  2. @Nullable
  3. public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException {
  4. if (logger.isTraceEnabled()) {
  5. logger.trace("Trying to resolve XML entity with public ID [" + publicId +
  6. "] and system ID [" + systemId + "]");
  7. }
  8. if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
  9. int lastPathSeparator = systemId.lastIndexOf('/');
  10. int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
  11. if (dtdNameStart != -1) {
  12. String dtdFile = DTD_NAME + DTD_EXTENSION;
  13. if (logger.isTraceEnabled()) {
  14. logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
  15. }
  16. try {
  17. Resource resource = new ClassPathResource(dtdFile, getClass());
  18. InputSource source = new InputSource(resource.getInputStream());
  19. source.setPublicId(publicId);
  20. source.setSystemId(systemId);
  21. if (logger.isDebugEnabled()) {
  22. logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
  23. }
  24. return source;
  25. }
  26. catch (IOException ex) {
  27. if (logger.isDebugEnabled()) {
  28. logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
  29. }
  30. }
  31. }
  32. }
  33. // Use the default behavior -> download from website or wherever.
  34. return null;
  35. }