【Spring源码解析】- EntityResolver

【Spring源码解析】- EntityResolver

开始之前我们先了解下Xml文件的定义格式

什么是DTD

DTD 是 Document Type Definition(文档类型定义)的缩写,它是一种用于定义 XML 文档结构和规则的文档标准。DTD 定义了 XML 文档中可以包含哪些元素,这些元素的结构和属性,以及它们之间的关系。DTD 可以用来验证 XML 文档是否符合特定的结构和规则。它没有使用xml的格式,而是自己定义了一套格式

什么是XSD

XSD(XML Schema Definition,XML模式定义)是一种用于定义XML文档结构和内容规则的XML标准。XSD是XML的一种模式语言,它允许您定义XML文档中可以包含的元素、元素的数据类型、元素的结构、元素的属性以及它们之间的关系。XSD通常用于验证XML文档的结构和内容是否符合预期的规则。

Spring中的示例

Spring在Xml的配置文件中支持使用DTD和XSD来定义XML结构和验证配置文件的有效性

DTD:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="myBean" class="com.example.MyBean">
        <property name="property1" value="someValue" />
    </bean>
</beans>

XSD:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="myBean" class="com.example.MyBean">
        <property name="property1" value="someValue" />
    </bean>
</beans>

XML的加载与验证

在上一篇文章,我们已经了解到加载与注册BeanDefinition首先是去先加载Document

/*
* 实际使用配置的DocumentLoader加载指定的文档。
*/
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
		return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
				getValidationModeForResource(resource), isNamespaceAware());
	}

参数相关:

  • ​ InputSource :调用的资源文件。
  • ​ EntityResolver: 处理文件的验证方式
  • ​ ErrorHandler:错误处理器
  • ​ validationMode:XML 文件的验证模式。
  • ​ namespaceAware :是否开启自动感知名称空间。

今天着重介绍点

  • getEntityResolver() 加载资源解析器,获取处理文件的验证方式

EntityResolver:

EntityResolver是jdk中rt.jar包中的接口,接口中只有一个方法,用于处理 XML 文档解析时的实体引用。这是 Java XML 解析的一部分,不是 Spring 框架特有的接口。

public interface EntityResolver {
    
    public abstract InputSource resolveEntity (String publicId,
                                               String systemId)
        throws SAXException, IOException;

}

方法的返回值是一个包含了约束文件的 InputSource 对象。在调用时,需要调用方提供publicIdsystemId两个参数,可以把它们理解为约束文件的标识符,它们可以从 XML 文件中获取到

参数:
  • XSD约束的XML文件中

    • <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
          <bean id="myBean" class="com.example.MyBean">
              <property name="property1" value="someValue" />
          </bean>
      </beans>
      
      • publicId是空
      • systemIdhttp://www.springframework.org/schema/beans/spring-beans.xsd
  • DTD约束的XML文件中

    • <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
      <beans>
          <bean id="myBean" class="com.example.MyBean">
              <property name="property1" value="someValue" />
          </bean>
      </beans>
      
      • publicId-//SPRING//DTD BEAN//EN
      • systemIdhttp://www.springframework.org/dtd/spring-beans.dtd

​ 可以看到systemId指向的是下载约束文件的网址,DTD当中还有一个publicId,但是XSD中是没有publicId

继承关系

在这里插入图片描述

DelegatingEntityResolver

DelegatingEntityResolver类实现了接口中的方法,把具体的解析工作委托给了dtdResolver、schemaResolver。

	@Override
	@Nullable
	public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
			throws SAXException, IOException {
		// 根据systemId判断是DTD还是XSD约束
		if (systemId != null) {
			if (systemId.endsWith(DTD_SUFFIX)) {
				return this.dtdResolver.resolveEntity(publicId, systemId);
			}
			else if (systemId.endsWith(XSD_SUFFIX)) {
				return this.schemaResolver.resolveEntity(publicId, systemId);
			}
		}

		// Fall back to the parser's default behavior.
		return null;
	}

   /** dtd文件的后缀. */
	public static final String DTD_SUFFIX = ".dtd";

	/** xsd文件后缀 */
	public static final String XSD_SUFFIX = ".xsd";


	private final EntityResolver dtdResolver;

	private final EntityResolver schemaResolver;
	/**
	   默认的解析器
	 */
	public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
		this.dtdResolver = new BeansDtdResolver();
		this.schemaResolver = new PluggableSchemaResolver(classLoader);
	}

	/**
	   支持自定义的解析器构造方法
	 */
	public DelegatingEntityResolver(EntityResolver dtdResolver, EntityResolver schemaResolver) {
		Assert.notNull(dtdResolver, "'dtdResolver' is required");
		Assert.notNull(schemaResolver, "'schemaResolver' is required");
		this.dtdResolver = dtdResolver;
		this.schemaResolver = schemaResolver;
	}

通过我们DelegatingEntityResolver的整体结构解读,我们发现

  • 定义了区分两种文件的后缀约束
  • 定义两个解析器,默认情况dtd和schema的约束验证就是交由这两个类进行处理的,当然也支持我们的自定义
  • 在解析方法中,根据后缀不同来决定使用什么解析器
XSD约束文件的resolveEntity
PluggableSchemaResolver

我们在上面DelegatingEntityResolver构造器中可以看到加载XSD主要是使用PluggableSchemaResolver 进行的

/**
  *定义架构映射的文件的位置。可以存在于多个 JAR 文件中。
  */
public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";

/**
  *网络路径与本地路径的映射。
  */
@Nullable
private volatile Map<String, String> schemaMappings;


/**
 * 这个方法用于解析XML实体,通常用于解析XML文档中的外部引用,比如DTD或XSD。
 * @param publicId 公共ID,通常为null
 * @param systemId 系统ID,表示要解析的XML实体的标识符
 * @return 返回一个InputSource对象,包含了要解析的实体内容,如果无法解析则返回null
 * @throws IOException 如果解析过程中发生IO错误,则抛出IOException异常
 */
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
    if (logger.isTraceEnabled()) {
        logger.trace("尝试解析具有公共ID [" + publicId +
                "] 和系统ID [" + systemId + "] 的XML实体");
    }

    if (systemId != null) {
        String resourceLocation = getSchemaMappings().get(systemId);
        if (resourceLocation == null && systemId.startsWith("https:")) {
            // 即使是https声明,也要检索规范的http模式映射
            resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
        }
        if (resourceLocation != null) {
            Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
            try {
                InputSource source = new InputSource(resource.getInputStream());
                source.setPublicId(publicId);
                source.setSystemId(systemId);
                if (logger.isTraceEnabled()) {
                    logger.trace("在类路径中找到XML模式 [" + systemId + "]: " + resourceLocation);
                }
                return source;
            }
            catch (FileNotFoundException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("无法找到XML模式 [" + systemId + "]: " + resource, ex);
                }
            }
        }
    }

    // 如果找不到匹配的模式映射,就返回null,使用解析器的默认行为
    return null;
}

/**
 * 这个方法用于延迟加载指定的模式映射。
 * @return 返回一个包含模式映射的Map对象
 */
private Map<String, String> getSchemaMappings() {
    Map<String, String> schemaMappings = this.schemaMappings;
    if (schemaMappings == null) {
        synchronized (this) {
            schemaMappings = this.schemaMappings;
            if (schemaMappings == null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("从 [" + this.schemaMappingsLocation + "] 加载模式映射");
                }
                try {
                    Properties mappings =
                            PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
                    if (logger.isTraceEnabled()) {
                        logger.trace("加载的模式映射: " + mappings);
                    }
                    schemaMappings = new ConcurrentHashMap<>(mappings.size());
                    CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
                    this.schemaMappings = schemaMappings;
                }
                catch (IOException ex) {
                    throw new IllegalStateException(
                            "无法从位置 [" + this.schemaMappingsLocation + "] 加载模式映射", ex);
                }
            }
        }
    }
    return schemaMappings;
}

  • 判断确保systemId是非空的,前面说过不管是XSD还是DTDsystemId都是非空的

  • 通过getSchemaMappings().get(systemId)获取到离线资源路径

    • 如果内部 schemaMappings 为空说明第一次加载模式映射进入同步块

    • 在同步块中,再次检查 schemaMappings 是否为空。这是因为多个线程可能同时到达同步块,第一个线程进入同步块后,会再次检查 schemaMappings 是否为空,以确保只有一个线程加载模式映射。

    • 如果还是为空则开始真正的加载操作

    • 使用 PropertiesLoaderUtils.loadAllProperties 方法从指定的位置 this.schemaMappingsLocation 加载所有的属性文件,通常这些属性文件包含了XML模式的映射信息

    • 创建一个新的 ConcurrentHashMap 对象 schemaMappings,用于存储模式映射,初始化容量为属性文件中的映射数量。

    • 使用 CollectionUtils.mergePropertiesIntoMap 方法将加载的属性合并到 schemaMappings 中,这将填充映射关系。

    • schemaMappings 赋值给成员变量 this.schemaMappings,以便下次直接获取,然后退出同步块。

    • schemaMappings是一个Map,这个 Map 中以 Key-Value 方式保存着 XSD 文件的systemId和离线文件的存储路径。

      • schemaMappings是从this.schemaMappingsLocation进行加载的

      • schemaMappingsLocation来历是构造方法中赋值

        • 	public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
          		this.classLoader = classLoader;
          		this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
          	}
          
          

          默认值呢就是我们上面看到的在类中定义的映射文件位置

          	public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
          

          我们接着找到这个位置

        在这里插入图片描述

  • 如果上一步没有获取到路径,并且systemId是以https:开头的,就把它替换为http:再试一遍。

  • 如果至此得到的路径不为空,那么根据路径加载相应的资源并封装成 InputSource 并返回。

小结一下,整个流程基本为 XSD 文件的解析器先从META-INF/spring.schemas文件中加载 Spring 中所有的 XSD 文件的systemId和离线文件的存储路径,保存在一个 Map 中,在根据提供的systemId从 Map 中找到离线的 XSD 文件的路径,并加载相应的文件资源

DTD约束文件的resolveEntity
BeansDtdResolver

我们在上面DelegatingEntityResolver构造器中可以看到加载DTD主要是使用BeansDtdResolver 进行的

private static final String DTD_EXTENSION = ".dtd";

private static final String DTD_NAME = "spring-beans";

@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException {
    // 如果启用了跟踪日志,打印正在尝试解析的XML实体的信息
    if (logger.isTraceEnabled()) {
        logger.trace("尝试解析具有公共ID [" + publicId + "] 和系统ID [" + systemId + "] 的XML实体");
    }

    // 如果systemId不为空并且以".dtd"结尾
    if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
        // 获取路径中的最后一个斜杠的位置
        int lastPathSeparator = systemId.lastIndexOf('/');
        // 在systemId中查找DTD名称的起始位置
        int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
        if (dtdNameStart != -1) {
            // DTD文件的名称,以".dtd"结尾
            String dtdFile = DTD_NAME + DTD_EXTENSION;
            // 如果启用了跟踪日志,打印正在尝试在Spring的JAR包中查找DTD文件的信息
            if (logger.isTraceEnabled()) {
                logger.trace("尝试在Spring JAR包的类路径中查找 [" + dtdFile + "]");
            }
            try {
                // 使用ClassPathResource从类路径中获取DTD文件
                Resource resource = new ClassPathResource(dtdFile, getClass());
                // 创建一个用于XML实体的输入源
                InputSource source = new InputSource(resource.getInputStream());
                // 设置publicId和systemId
                source.setPublicId(publicId);
                source.setSystemId(systemId);
                // 如果启用了跟踪日志,打印找到的DTD文件信息
                if (logger.isTraceEnabled()) {
                    logger.trace("在类路径中找到beans DTD [" + systemId + "]:" + dtdFile);
                }
                // 返回找到的DTD文件的输入源
                return source;
            }
            catch (FileNotFoundException ex) {
                if (logger.isDebugEnabled()) {
                    // 如果找不到DTD文件,记录警告信息
                    logger.debug("无法解析beans DTD [" + systemId + "]:在类路径中未找到", ex);
                }
            }
        }
    }

    // 如果无法解析DTD文件,返回null,采用解析器的默认行为
    return null;
}

  1. 首先,类中定义了两个常量:
    • DTD_EXTENSION:表示DTD文件的扩展名,通常为".dtd"。
    • DTD_NAME:表示Spring框架的DTD文件的名称,为"spring-beans"。
  2. 然后,方法resolveEntity会尝试解析XML实体,参数包括公共ID(publicId)和系统ID(systemId)。公共ID通常用于在多个XML文档中标识相同的DTD,而系统ID通常用于指定DTD文件的位置。
  3. 方法中首先会判断是否启用了跟踪日志,如果启用了,则会打印一条跟踪信息,显示正在尝试解析的XML实体的公共ID和系统ID。
  4. 接下来,会检查系统ID是否不为空并且是否以".dtd"结尾,这是为了确定是否在解析DTD文件。
  5. 如果系统ID符合要求,进一步处理:
    • 获取systemId中最后一个斜杠的位置,以确定文件路径。
    • 查找systemId中DTD名称的起始位置。
    • 如果找到DTD名称,则构建DTD文件的名称,即"spring-beans.dtd"。
    • 接着,尝试从类路径中查找该DTD文件,使用ClassPathResource来获取DTD文件的Resource
    • 如果找到DTD文件,就创建一个用于XML实体的InputSource对象,并设置其公共ID和系统ID。
    • 最后,返回创建的InputSource对象,这个对象用于告诉XML解析器去哪里找到DTD文件。

在这里插入图片描述

ResourceEntityResolver

DelegatingEntityResolver ,它扩展支持了在网络上查找约束文件的方式

/**
 * 为指定的 ResourceLoader(通常是 ApplicationContext)创建一个 ResourceEntityResolver。
 *
 * @param resourceLoader 用于加载 XML 实体的 ResourceLoader(或者是 ApplicationContext)
 */
public ResourceEntityResolver(ResourceLoader resourceLoader) {
    // 调用父类的构造方法,传入 ResourceLoader 的类加载器
    super(resourceLoader.getClassLoader());
    this.resourceLoader = resourceLoader;
}

@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
        throws SAXException, IOException {

    // 调用父类的 resolveEntity 方法来解析实体
    InputSource source = super.resolveEntity(publicId, systemId);

    if (source == null && systemId != null) {
        String resourcePath = null;
        try {
            // 尝试解码系统ID,将其转换成可读的 URL
            String decodedSystemId = URLDecoder.decode(systemId, StandardCharsets.UTF_8);
            String givenUrl = ResourceUtils.toURL(decodedSystemId).toString();
            String systemRootUrl = new File("").toURI().toURL().toString();
            // 如果给定的 URL 以系统根 URL 开头,尝试相对于资源基础路径进行解析
            if (givenUrl.startsWith(systemRootUrl)) {
                resourcePath = givenUrl.substring(systemRootUrl.length());
            }
        } catch (Exception ex) {
            // 通常是 MalformedURLException 或 AccessControlException
            if (logger.isDebugEnabled()) {
                logger.debug("无法解析 XML 实体 [" + systemId + "] 相对于系统根 URL", ex);
            }
            // 没有 URL(或不可解析的 URL)-> 尝试相对于资源基础路径
            resourcePath = systemId;
        }
        if (resourcePath != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("尝试作为资源 [" + resourcePath + "] 定位 XML 实体 [" + systemId + "]");
            }
            // 使用 ResourceLoader 获取资源,并创建用于 XML 实体的 InputSource
            Resource resource = this.resourceLoader.getResource(resourcePath);
            source = new InputSource(resource.getInputStream());
            source.setPublicId(publicId);
            source.setSystemId(systemId);
            if (logger.isDebugEnabled()) {
                logger.debug("找到 XML 实体 [" + systemId + "]:" + resource);
            }
        } else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) {
            // 如果无法解析为本地资源,尝试解析为远程资源
            source = resolveSchemaEntity(publicId, systemId);
        }
    }

    return source;
}

/**
 * 当无法将“schema”实体(DTD 或 XSD)解析为本地资源时的后备方法。
 * 默认行为是通过 HTTPS 执行远程解析。
 * 子类可以重写此方法以更改默认行为。
 */
@Nullable
protected InputSource resolveSchemaEntity(@Nullable String publicId, String systemId) {
    InputSource source;
    // 即使是 HTTP 声明,也要通过 HTTPS 执行外部的 DTD/XSD 查找
    String url = systemId;
    if (url.startsWith("http:")) {
        url = "https:" + url.substring(5);
    }
    if (logger.isWarnEnabled()) {
        logger.warn("未找到 DTD/XSD XML 实体 [" + systemId + "],正在回退到远程 HTTPS 解析");
    }
    try {
        // 尝试通过 HTTPS 获取输入源
        source = new InputSource(ResourceUtils.toURL(url).openStream());
        source.setPublicId(publicId);
        source.setSystemId(systemId);
    } catch (IOException ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("无法通过 URL [" + url + "] 解析 XML 实体 [" + systemId + "]", ex);
        }
        // 回退到解析器的默认行为
        source = null;
    }
    return source;
}

  1. 首先,它调用父类的也就是DelegatingEntityResolverresolveEntity 方法来尝试解析实体。如果该方法返回了一个非空的 InputSource 对象,说明实体已成功解析,直接返回。
  2. 如果无法解析实体,它会尝试解析systemId。如果systemId以 “.dtd” 或 “.xsd” 结尾,说明可能是一个 DTD 或 XSD,于是调用 resolveSchemaEntity 方法来尝试远程解析。
  3. resolveSchemaEntity 方法中,它会尝试将systemId转换为 HTTPS URL,然后通过 HTTPS 打开该 URL 并创建一个 InputSource 对象。
  4. 如果远程解析成功,返回该 InputSource 对象,否则返回 null,以请求解析器执行默认行为。

现在我们知道了什么是EntityResolver :主要用于加载不同的Xml约束文件

getEntityResolver

在上面我们了解到了DTDXSD,和EntityResolver 我们在把视角切换回XmlBeanDefinitionReader中继续看

	
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是否不为空
  • 为空则继续加载,如果能获取到ResourceLoader,则使用ResourceEntityResolver
  • 如果没有ResourceLoader,则使用DelegatingEntityResolver

前面的文章详细介绍过ResourceLoader ,它是用于获取资源(如文件、URL等)的接口,它可以处理不同类型的资源加载。

Spring会优先使用它,因为它更具通用性,可以处理多种资源类型。如果没有ResourceLoader可用,Spring会回退到使用类加载器。前面介绍ResourceEntityResolver它会在加载不到静态资源的情况下去网络上查找,所以会传递一个ResourceLoader,而DelegatingEntityResolver只是加载静态文件所以使用类加载器即可

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值