一、目的
前面提到过XML为了保证文档结构的正确性,可以使用两种验证模式:DTD和XSD;这两种验证模式对应两种验证文件。
SAX解析XML文档时,先读取该XML文档上声明的验证模式,然后根据声明去寻找对应的验证文件。默认的寻找规则是通过网络方式来读取验证文件,在网络传输的过程中会有各种各样的问题。 何况,本地写个demo没网就不能运行,本身就是个扯淡的事儿!
EntityResolver的作用是项目本身可以提供一个如何寻找验证文件的方法,Spring就实现了一个EntityResolver接口来实现不通过网络读取验证文件。
二、主要相关类
- org.springframework.beans.factory.xml.XmlBeanDefinitionReader:获取EntityResolver的入口
- org.springframework.beans.factory.xml.ResourceEntityResolver:EntityResolver的实现
- org.springframework.beans.factory.xml.DelegatingEntityResolver:EntityResolver的实现,是ResourceEntityResolver的父类。真正的委托逻辑在这个类中,具体的实现在下面这俩类里面。
- org.springframework.beans.factory.xml.BeansDtdResolver:DTD验证模式解析器
- org.springframework.beans.factory.xml.PluggableSchemaResolver:XSD验证模式解析器
三、源码分析
- XmlBeanDefinitionReader的doLoadDocument方法为加载xml文档做数据准备
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
// 真正加载xml文档的类是DocumentLoader,我们主要分析第二个入参
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
protected EntityResolver getEntityResolver() {
if (this.entityResolver == null) {
// Determine default EntityResolver to use.
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader != null) {
// ResourceEntityResolver为DelegatingEntityResolver的子类,在调用resolveEntity()方法来自定义寻找规则时
// 也是调用了父类的resolveEntity(),一般父类就能找到验证模式声明了,所以我们直接看DelegatingEntityResolver的resolveEntity()
this.entityResolver = new ResourceEntityResolver(resourceLoader);
}
else {
this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
}
}
return this.entityResolver;
}
- 自定义寻找验证模式的规则DelegatingEntityResolver的resolveEntity()
// 先看下DelegatingEntityResolver的构造方法,创建了两种类型的EntityResolver
public DelegatingEntityResolver(@Nullable ClassLoader classLoader) {
this.dtdResolver = new BeansDtdResolver();
this.schemaResolver = new PluggableSchemaResolver(classLoader);
}
@Override
@Nullable
public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException {
if (systemId != null) {
// 以.dtd结尾使用DTD解析器(3)
if (systemId.endsWith(DTD_SUFFIX)) {
return this.dtdResolver.resolveEntity(publicId, systemId);
}
// 以.xsd结尾使用schema解析器(4)
else if (systemId.endsWith(XSD_SUFFIX)) {
return this.schemaResolver.resolveEntity(publicId, systemId);
}
}
return null;
}
- DTD解析器BeansDtdResolver的实现
两个入参的值是在XML文档中声明的验证模式获取的,如下
publicId=-//SPRING//DTD BEAN 2.0//EN
systemId=http://www.springframework.org/dtd/spring-beans.dtd
@Override
@Nullable
public InputSource resolveEntity(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 + "]");
}
// 再确认一下,是以.dtd结尾的
if (systemId != null && systemId.endsWith(DTD_EXTENSION)) {
int lastPathSeparator = systemId.lastIndexOf('/');
// 并且systemId字符串中包含“spring-beans”
int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
if (dtdNameStart != -1) {
// 文件名:spring-beans.dtd
String dtdFile = DTD_NAME + DTD_EXTENSION;
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
}
try {
// 使用ClassPathResource来读取跟当前类(getClass())同路径下的spring-beans.dtd文件
// 也就是说org.springframework.beans.factory.xml这个包下的spring-beans.dtd文件,见(6)
// 这样就不用通过网络获取DTD验证文件了
Resource resource = new ClassPathResource(dtdFile, getClass());
// 包装成InputSource
InputSource source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isDebugEnabled()) {
logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
}
return source;
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
}
}
}
}
// Use the default behavior -> download from website or wherever.
return null;
}
- XSD解析器PluggableSchemaResolver的实现
两个入参的值是在XML文档中声明的验证模式获取的,如下
publicId=null
systemId=http://www.springframework.org/schema/beans/spring-beans.xsd
// 先看下构造方法
public PluggableSchemaResolver(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
// schemaMapping路径为META-INF/spring.schemas
// 与DTD不一样的是,XSD的验证文件路径不是拼接得到的,而是从META-INF/spring.schemas获取的
this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
}
@Override
@Nullable
public InputSource resolveEntity(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) {
// 从schemaMappings中通过systemId获取XSD文件路径;schemaMappings获取方式在getSchemaMappings()中
// 获取的值为:org/springframework/beans/factory/xml/spring-beans.xsd
String resourceLocation = getSchemaMappings().get(systemId);
if (resourceLocation != null) {
// 使用ClassPathResource加载对应的xsd文件
// 也就是说org/springframework/beans/factory/xml/spring-beans.xsd文件,见(6)
Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
try {
InputSource source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isDebugEnabled()) {
logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
}
return source;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex);
}
}
}
}
return null;
}
// 生成schemaMappings
private Map<String, String> getSchemaMappings() {
Map<String, String> schemaMappings = this.schemaMappings;
if (schemaMappings == null) {
synchronized (this) {
schemaMappings = this.schemaMappings;
if (schemaMappings == null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
}
try {
// schemaMappingsLocation的值为META-INF/spring.schemas,在上面的构造方法中可以看到
// 读取完文件,得到一个键值对对象。文件内容见(5)
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded schema mappings: " + mappings);
}
Map<String, String> mappingsToUse = new ConcurrentHashMap<>(mappings.size());
// 把mappings 合并到mappingsToUse然后返回
CollectionUtils.mergePropertiesIntoMap(mappings, mappingsToUse);
schemaMappings = mappingsToUse;
this.schemaMappings = schemaMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
}
}
}
}
return schemaMappings;
}
- xsd文件相关键值对文件:META-INF/spring.schemas
6. 两个验证文件
四、总结
- XmlBeanDefinitionReader把创建好的ResourceEntityResolver对象,传给DefaultDocumentLoader来自定义获取验证模式声明文件的方式。
- DocumentBuilder的parse方法在解析xml文档的时候,会调用自定义ResourceEntityResolver的resolveEntity()方法。
- ResourceEntityResolver的resolveEntity()方法会先调用父类DelegatingEntityResolver的resolveEntity()方法。
- DelegatingEntityResolver的resolveEntity()方法中根据不同的验证模式分发给真正干活儿的:BeansDtdResolver和PluggableSchemaResolver
- DTD验证模式,直接在BeansDtdResolver使用ClassPathResource读取org/springframework/beans/factory/xml/spring-beans.dtd文件即可。
- XSD验证模式,先把META-INF/spring.schemas文件的内容映射为一个Properties对象。然后根据传入的systemId值,获取到对应的xsd文件名,进而使用ClassPathResource读取。
五、借鉴
- org.xml.sax.EntityResolver
本次我们借鉴的不是spring,而是SAX程序的设计。通常我们设计多功能的程序,可由用户选择不同的功能,是通过用户传入不同的参数,我们程序实现不同的功能。如用户传入1,我们设置关灯;用户传入2,我们设置开灯。
其实我们更可以让用户传入自定义的代码,实现办法就是我们的某个控制功能的入参是一个接口。用户如果想扩展我们的程序功能,就可以实现这个接口的某个方法,然后我们在特定的时机就调用这个接口的那个方法。
EntityResolver的实现:
在DocumentBuilder的parse方法解析xml文档时,会调用EntityResolver的resolveEntity()方法来获取验证xml文档的声明文件,从而验证xml文档格式的正确性。其中EntityResolver就可以由用户自定义。