【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 对象。在调用时,需要调用方提供publicId
和systemId
两个参数,可以把它们理解为约束文件的标识符,它们可以从 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
是空systemId
是http://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
systemId
是http://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;
}
- 首先,类中定义了两个常量:
DTD_EXTENSION
:表示DTD文件的扩展名,通常为".dtd"。DTD_NAME
:表示Spring框架的DTD文件的名称,为"spring-beans"。
- 然后,方法
resolveEntity
会尝试解析XML实体,参数包括公共ID(publicId)和系统ID(systemId)。公共ID通常用于在多个XML文档中标识相同的DTD,而系统ID通常用于指定DTD文件的位置。 - 方法中首先会判断是否启用了跟踪日志,如果启用了,则会打印一条跟踪信息,显示正在尝试解析的XML实体的公共ID和系统ID。
- 接下来,会检查系统ID是否不为空并且是否以".dtd"结尾,这是为了确定是否在解析DTD文件。
- 如果系统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;
}
- 首先,它调用父类的也就是
DelegatingEntityResolver
的resolveEntity
方法来尝试解析实体。如果该方法返回了一个非空的InputSource
对象,说明实体已成功解析,直接返回。 - 如果无法解析实体,它会尝试解析
systemId
。如果systemId
以 “.dtd” 或 “.xsd” 结尾,说明可能是一个 DTD 或 XSD,于是调用resolveSchemaEntity
方法来尝试远程解析。 - 在
resolveSchemaEntity
方法中,它会尝试将systemId
转换为 HTTPS URL,然后通过 HTTPS 打开该 URL 并创建一个InputSource
对象。 - 如果远程解析成功,返回该
InputSource
对象,否则返回null
,以请求解析器执行默认行为。
现在我们知道了什么是EntityResolver :主要用于加载不同的Xml约束文件
getEntityResolver
在上面我们了解到了DTD
和XSD
,和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
只是加载静态文件所以使用类加载器即可