1. 优享JAVA首页
  2. Java
  3. Spring
  4. Spring源码分析

获取XML的验证模式

上文说到加载bean的代码中假如不考虑异常类的代码,其实只做了三件事,这三件事每一件必不可少。
1、获取对XML文件的验证模式
2、加载XML文件,并得到对应的Document
3、根据返回的Document注册bean信息
这三个步骤支撑着整个Spring容器部分的实现基础,尤其是第三部对配置文件的解析,下面我们先从xml文件的验证模式开始继续深入。

DTD与XSD的区别

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

XML Schema语言就是XSD(XML Schemas Definition)XML Schema描述了XML文档结构,可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的,XML Schema本身是一个XML文档,它符合XML语法结构,可以用通用的XML解析器解析它。
简单地介绍了一下XML文件的验证模式的相关知识,如果对XML有兴趣的小伙伴可以进一步查阅相关资料。

验证模式的读取

了解了DTD和XSD的区别后我们再去分析Spring中对于验证模式的提取就更容易理解了,通过之前的分析我们锁定了Spring通过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;
 }

方法的实现其实还是很简单的,无非是如果设定了验证模式则使用设定的验证模式(可以通过对调用XmlBeanDefinitionReader中的setValidationMode方法进行设定),否则使用自动检测的方式。而自动检测验证模式的功能是在函数detectValidationMode方法中实现的,在detectValidationMode函数中又将自动检测验证模式的工作委托给了专门处理类XmlValidationModeDetector,调用了XmlValidationModeDetector的validationModeDetector方法,具体代码如下:

protected int detectValidationMode(Resource resource) {
	if (resource.isOpen()) {
		throw new BeanDefinitionStoreException(
				"Passed-in Resource [" + resource + "] contains an open stream: " +
				"cannot determine validation mode automatically. Either pass in a Resource " +
				"that is able to create fresh streams, or explicitly specify the validationMode " +
				"on your XmlBeanDefinitionReader instance.");
	}

	InputStream inputStream;
	try {
		inputStream = resource.getInputStream();
	}
	catch (IOException ex) {
		throw new BeanDefinitionStoreException(
				"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
				"Did you attempt to load directly from a SAX InputSource without specifying the " +
				"validationMode on your XmlBeanDefinitionReader instance?", ex);
	}

	try {
		return this.validationModeDetector.detectValidationMode(inputStream);
	}
	catch (IOException ex) {
		throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
				resource + "]: an error occurred whilst reading from the InputStream.", ex);
	}
}

XmlValidationModeDetector.java

public int detectValidationMode(InputStream inputStream) throws IOException {
	// Peek into the file to look for DOCTYPE.
	BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
	try {
		boolean isDtdValidated = false;
		String content;
		while ((content = reader.readLine()) != null) {
			content = consumeCommentTokens(content);
			if (this.inComment || !StringUtils.hasText(content)) {
				continue;
			}
			if (hasDoctype(content)) {
				isDtdValidated = true;
				break;
			}
			if (hasOpeningTag(content)) {
				// End of meaningful data...
				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;
	}
	finally {
		reader.close();
	}
}
private boolean hasDoctype(String content) {
	return content.contains(DOCTYPE);
}

只要我们理解了XSD与DTD的使用方法,理解上面的代码应该不会太难,,Spring用来检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTD,否则就是XSD。

进行Document加载


经过验证模式准备的步骤,就可以进行Document的加载了。同样的XmlBeanFactoryReader类对于文档读取并没有亲力亲为,而是委托给了DocumentLoader去执行,这里的DocumentLoader是个接口。真正调用的是DefaultDocumentLoader,代码如下:

public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
	 ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {

   DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
   if (logger.isDebugEnabled()) {
	 logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
   }
   DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
   return builder.parse(inputSource);
}

这部分代码通过SAX解析XML文档,解析过程大致都差不多,Spring这里并没有什么特殊的地方,同样是创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析InputStream来返回Document对象。这里要提及一下EntityResolver,对于entityResolver,传入的是通过getEntityResolver()函数获取的返回值,代码如下:

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到底做什么用的呢?

EntityResolver的作用

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

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

InputSource resolveEntity(String publicId,String systemId)

这里接收的两个参数publicId和systemId,并返回一个inputSource对象,下面我们以特定的配置文件进行说明

1、如果我们在解析验证模式为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="myTestBean" class="container.MyTestBean" />

</beans>

读取到以下两个参数
publicId:null
systemId:http://www.Springframework.org/schema/beans/Spring-beans.xsd

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

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<!-- <bean/> definitions here -->
</beans>

读取到以下两个参数
publicId:-//SPRING//DTD BEAN 2.0//EN
systemId:http://www.springframework.org/dtd/spring-beans-2.0.dtd
验证文件默认的加载方式是从网络上通过URL获取的,那么这样会造成延迟等问题,一般做法是将验证文件放置在自己的工程中,那么Spring中是怎样实现加载DTD文件的呢?
Spring通过getEntityResolver()方法对EntityResolver的获取,我们知道Spring中使用DelegatingEntityResolver类为EntityResolver的实现类,resolverEntity实现方法如下:

DelegatingEntityResolver.java

  public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
	 if (systemId != null) {
	   if (systemId.endsWith(DTD_SUFFIX)) {
			  //如果是dtd从这里解析
		  return this.dtdResolver.resolveEntity(publicId, systemId);
	   }
	   else if (systemId.endsWith(XSD_SUFFIX)) {
			  //通过调用META-INF/Spring.schemas解析
		  return this.schemaResolver.resolveEntity(publicId, systemId);
	   }
	 }
	 return null;
  }

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

public InputSource resolveEntity(String publicId, 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 && systemId.endsWith(DTD_EXTENSION)) {
		  int lastPathSeparator = systemId.lastIndexOf('/');
		  int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
		  if (dtdNameStart != -1) {
			  String dtdFile = DTD_FILENAME + DTD_EXTENSION;
			  if (logger.isTraceEnabled()) {
				  logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
			  }
			  try {
				  Resource resource = new ClassPathResource(dtdFile, getClass());
				  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;
}

解析及注册BeanDefinitions

文档转换为Document后,接下来的提取及注册bean就是我们的重头戏 。继续上面的分析当程序已经拥有XML文档文件的Document实例对象时,就会被引入下面这个方法

XmlBeanDefinitionReader.java

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
	//使用DefaultBeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
	BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
	//将环境变量设置其中
	documentReader.setEnvironment(this.getEnvironment());
	//在实例化BeanDefinitionReader时候会将BeanDefinitionRegistry传入,默认使用继承自DefaultListableBeanFactory的子类
	//记录统计前的BeanDefinition的加载个数
	int countBefore = getRegistry().getBeanDefinitionCount();
	//加载及注册bean
	documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
	//记录本次加载的BeanDefinition个数
	return getRegistry().getBeanDefinitionCount() - countBefore;
}

其中的参数doc是通过上面的loadDocument加载转换出来的,这个方法中很好地应用了面向对象中单一职责的原则,将逻辑处理委托给单一的类进行处理,而这个逻辑处理类就是BeanDefinitionDocumentReader,BeanDefinitionDocumentReader是一个接口,而实例化工作实在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentreader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后发现这个方法的重要目的之一就是提取root,以便再次将root作为参数继续BeanDefinition的注册。

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
	this.readerContext = readerContext;
	logger.debug("Loading bean definitions");
	Element root = doc.getDocumentElement();
	doRegisterBeanDefinitions(root);
}

上面代码中我们终于找到了核心逻辑的底部doRegisterBeanDefinitions(root),如果说以前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions算是真正地开始解析了。

上面代码的处理流程首先是对profile的处理,然后开始进行解析。我们跟进preProcessXml(root)或者postProcessXml(root)发现代码是空的,既然是空的那么写着有啥用呢?就像面向对象设计方法中常用的一句话,一个类要么是面向集成的设计的,要么就是用final修饰的。在DefaultBeanDefinitionDocumentReader中并没有用final修饰,所以它是面向集成设计的。这两个方法正是为子类而设计的,这种设计的模式就是模版方法模式,如果继承自DefaultBeanDefinitionDocumentReader的子类需要在Bean解析前后做一些处理的话,那么只需要重写这两个方法就可以了。

profile属性的使用

注册bean的最开始是对PROFILE_ATTRIBUTE属性的解析,可能对我们来说profile属性并不是很常用。下面我们了解下这个属性。
先看看官方给出的示例代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:jdbc="http://www.springframework.org/schema/jdbc"
  xmlns:jee="http://www.springframework.org/schema/jee"
  xsi:schemaLocation="...">
  <beans profile="dev">
	... ...
  </beans>
  <beans profile="production">
	... ...
  </beans>
</beans>

集成到Web环境中时,在web.xml文件中加入以下代码:

<context-param>
   <param-name>Spring.profiles.active</param-name>
   <param-value>dev</param-value>
</context-param>

这个特性是用于同时在配置文件中部署两套配置来适用于生产环境和开发环境,这样可以方便的进行切换开发,部署环境,最常用的就是更换不同的数据库。
分析profile的代码,首先程序会获得beans节点是否定义了profile属性,如果定义了则会需要到环境变量中去寻找,所以这里先断言environment不可能为空,因为profile可以同时指定多个的,需要程序对其拆分,并解析每个profile是都符合环境变量所定义的,不定义则不会浪费性能去解析。

解析并注册BeanDefinition

处理完profile后就可以进行XML的读取了,跟踪代码进入parseBeanDefinitions(root,this.delegate)。

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
	//对beans的处理
	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)) {
					//对bean的处理
					parseDefaultElement(ele, delegate);
				}
				else {
					//对bean的处理
					delegate.parseCustomElement(ele);
				}
			}
		}
	}
	else {
		delegate.parseCustomElement(root);
	}
}

上面的代码看起来逻辑还是蛮清晰的,因为在Spring的XML配置文件中有两大类Bean声明,一个是默认的:

<bean id="test" class="test.TestBean"/>

另一类就是自定义的:

 <tx:annotation-driven/> 

两种方式的读取及解析差别是非常大的,如果采用Spring默认的配置,由Spring的默认处理逻辑进行处理,如果是自定义的,那么就需要实现一些借口及配置。对于根节点或子节点如果是默认命名空间的话则采用parseDefaultElement方法进行解析,否则使用delegate.parseCustomElement方法对自定义命名空间进行解析,而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并与Spring中固定的命名空间http://www.Springframework.org/schema/beans进行对比,如果一致则认为是默认,否者就认为是自定义。

原创文章,作者:Craig,如若转载,请注明出处:https://www.goodlymoon.com/archives/646.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注

QR code