写在前面
本文在这篇文章的基础上继续分析,这篇文章分析了如何获取验证模式,获取完验证模式之后就是将xml文件生成对应的文档对象了。
1:作用
用来从中解析用户配置的bean等信息为BeanDefinition数据结构。
2:测试代码
为了方便调试再贴下测试代码:
@Test
public void testBeanDefinitionLoad() {
// 定义资源
ClassPathResource classPathResource = new ClassPathResource("testbeandefinition.xml");
// 定义IOC容器
DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
// 定义bean定义读取器
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory);
// 通过bean定义读取器从资源中读取bean定义
int i = xmlBeanDefinitionReader.loadBeanDefinitions(classPathResource);
System.out.println("bean定义的个数是:" + i);
}
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="testBeanDefinitionBean"
class="yudaosourcecode.spring.TestBeanDefinitionBean"></bean>
<bean id="testBeanDefinitionBean1"
class="yudaosourcecode.spring.TestBeanDefinitionBean"></bean>
<!-- 这里引入自己的话会发生org.springframework.beans.factory.BeanDefinitionStoreException异常 -->
<!--<import resource="testbeandefinition.xml"/>-->
</beans>
最终调用到代码:
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadDocument
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
其中获取验证模式的代码getValidationModeForResource(resource),
我们已经再这篇文章中进行了分析。我们分析的入口方法是this.documentLoader.loadDocument
。我们先来分析this.documentLoader
。
3:DocumentLoader
DocumentLoader全类名为org.springframework.beans.factory.xml.DocumentLoader
,是一个顶层接口。定义了从资源文件转换为Document文档对象的功能,只有一个方法如下:
org.springframework.beans.factory.xml.DocumentLoader
public interface DocumentLoader {
Document loadDocument(
InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware)
throws Exception;
}
看下其中的参数:
inputSource:待解析的资源
entityResolver:解析文件的解析器
errorHandler:处理解析文件过程中发生的错误
validationMode:xml文件验证模式DTD(Document Type Definition) OR XSD(Xml Schema Definition)
namesapceAware:xml命名空间支持,true为支持,false为不支持
我们来debug看下这些参数:
该接口只有一个实现类org.springframework.beans.factory.xml.DefaultDocumentLoader
,其实现的加载方法loadDocument
我们单起一部分讲解。
3.1:loadDocument
源码如下:
org.springframework.beans.factory.xml.DefaultDocumentLoader#loadDocument
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
// <2021-02-26 17:25> 创建文档构造器工厂
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
// <2021-02-26 17:26>通过文档构造器工厂创建文档构造器
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
<2021-02-26 17:25>
处代码创建文档构造器工厂,其源码如下:
org.springframework.beans.factory.xml.DefaultDocumentLoader#createDocumentBuilderFactory
protected DocumentBuilderFactory createDocumentBuilderFactory(int validationMode, boolean namespaceAware)
throws ParserConfigurationException {
// 注意这里的类是jdk的rt.jar中的类,而非spring的了
// 创建实例对象
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 设置命名空间支持
factory.setNamespaceAware(namespaceAware);
// 如果有验证模式,目前一般都是xsd VALIDATION_XSD
if (validationMode != XmlValidationModeDetector.VALIDATION_NONE) {
// 开启验证
factory.setValidating(true);
// 如果是xsd强制开启命名空间支持
if (validationMode == XmlValidationModeDetector.VALIDATION_XSD) {
// Enforce namespace aware for XSD...
factory.setNamespaceAware(true);
try {
// 设置SCHEMA_LANGUAGE_ATTRIBUTE属性
factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE);
}
catch (IllegalArgumentException ex) {
ParserConfigurationException pcex = new ParserConfigurationException(
"Unable to validate using XSD: Your JAXP provider [" + factory +
"] does not support XML Schema. Are you running on Java 1.4 with Apache Crimson? " +
"Upgrade to Apache Xerces (or Java 1.5) for full XSD support.");
// <2021-02-26 18:38>
pcex.initCause(ex);
throw pcex;
}
}
}
return factory;
}
<2021-02-26 18:38>
代码是设置引起异常,关于方法initCause可以参考这里。
<2021-02-26 17:26>
处代码通过文档构造器工厂创建文档构造器
,其源码如下:
org.springframework.beans.factory.xml.DefaultDocumentLoader#createDocumentBuilder
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory,
@Nullable EntityResolver entityResolver, @Nullable ErrorHandler errorHandler)
throws ParserConfigurationException {
// 通过工厂创建文件构造器
DocumentBuilder docBuilder = factory.newDocumentBuilder();
// <2021-02-26 18:50>
// 如果解析文件的解析器不为空,则设置
if (entityResolver != null) {
docBuilder.setEntityResolver(entityResolver);
}
// 如果错误处理handler不为空,则设置
if (errorHandler != null) {
docBuilder.setErrorHandler(errorHandler);
}
// 返回工厂构造器
return docBuilder;
}
<2021-02-26 18:50>
处代码是设置实体解析器,作用是查找约束文件,DTD类型就是对应的.dtd
文件,XSD类型就是对应的.xsd
文件,对应的接口是org.xml.sax.EntityResolver
是在jdk的rt.jar
中,并不是spring自己提供的功能,源码如下:
org.xml.sax.EntityResolver#resolveEntity
public interface EntityResolver {
public abstract InputSource resolveEntity (String publicId,
String systemId)
throws SAXException, IOException;
}
可以看到只有一个方法resolveEntity
,其中有两个参数,第一个是publicId,第二个是systemId,其中第二个参数systemId代表的是约束文件的地址,本例中的参数值如下图:
参数entityResolver
的获取是在方法调用org.springframework.beans.factory.xml.DocumentLoader#loadDocument
中通过org.springframework.beans.factory.xml.XmlBeanDefinitionReader#getEntityResolver
方法获取的,该方法如下:
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#getEntityResolver
protected EntityResolver getEntityResolver() {
// 为null才进入,可以认为就是null
if (this.entityResolver == null) {
// <2021-02-28 09:32>
// 获取资源加载器
ResourceLoader resourceLoader = getResourceLoader();
// 如果资源加载器不为空则直接new一个ResourceEntityResolver,这里执行最终返回的就是这个对象
if (resourceLoader != null) {
this.entityResolver = new ResourceEntityResolver(resourceLoader);
}
// 否则返回DelegatingEntityResolver对象
else {
this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader());
}
}
// 返回最终的实体解析器
return this.entityResolver;
}
<2021-02-28 09:32>
处代码是获取资源加载器,关于资源加载器可以参考这里,如果有资源加载器,则创建一个资源实体解析器ResourceEntityResolver,该类定义如下:
public class ResourceEntityResolver extends DelegatingEntityResolver {}
其中DelegatingEntityResolver
定义如下:
public class DelegatingEntityResolver implements EntityResolver {}
接下来我们看下EntityResolver
的的具体子类实现查找约束文件的逻辑。
4:EntityResolver
4.1:DelegatingEntityResolver
这是spring实现EntityResolver的直接子类,该类是一个代理类,根据具体的验证模式是调用具体的子类进行验证,代理源码如下:
org.springframework.beans.factory.xml.DelegatingEntityResolver
public class DelegatingEntityResolver implements EntityResolver {
/** Suffix for DTD files. */
public static final String DTD_SUFFIX = ".dtd";
/** Suffix for schema definition files. */
public static final String XSD_SUFFIX = ".xsd";
// dtd验证模式的实体解析器
private final EntityResolver dtdResolver;
// xsd验证模式的实体解析器
private final EntityResolver schemaResolver;
@Override
@Nullable
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
throws SAXException, IOException {
// 有systemId才处理,systemId标记的是验证文件信息
if (systemId != null) {
// 如果是dtd则使用this.dtdResolver
if (systemId.endsWith(DTD_SUFFIX)) {
return this.dtdResolver.resolveEntity(publicId, systemId);
}
// 如果是xsd则使用this.schemaResolver
else if (systemId.endsWith(XSD_SUFFIX)) {
return this.schemaResolver.resolveEntity(publicId, systemId);
}
}
// 都不符合,返回null,则后续使用默认的网络下载方式获取
return null;
}
}
4.2:BeansDtdResolver
该类源码如下:
org.springframework.beans.factory.xml.BeansDtdResolver
public class BeansDtdResolver implements EntityResolver {
// 约束文件扩展名常量
private static final String DTD_EXTENSION = ".dtd";
// 约束文件的名称常量
private static final String DTD_NAME = "spring-beans";
private static final Log logger = LogFactory.getLog(BeansDtdResolver.class);
@Override
@Nullable
public InputSource resolveEntity(@Nullable 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('/');
// 获取文件名地址
int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator);
// 存在文件名
if (dtdNameStart != -1) {
// 拼接生成dtd文件名
String dtdFile = DTD_NAME + DTD_EXTENSION;
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath");
}
try {
// 构造资源
Resource resource = new ClassPathResource(dtdFile, getClass());
// 构造InputSource
InputSource source = new InputSource(resource.getInputStream());
// 设置publicId
source.setPublicId(publicId);
// 设置systemId
source.setSystemId(systemId);
if (logger.isTraceEnabled()) {
logger.trace("Found beans DTD [" + systemId + "] in classpath: " + dtdFile);
}
// 返回
return source;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex);
}
}
}
}
// 返回null,使用网络下载默认行为
return null;
}
}
4.3:PluggableSchemaResolver
该类有一个重要的属性DEFAULT_SCHEMA_MAPPINGS_LOCATION
,如下:
public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
该文件定义如下:
定义的是xsd的网络地址对应的jar包物理地址信息,最终会加载到变量private volatile Map<String, String> schemaMappings;
其中加载代码如下:
Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
schemaMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
this.schemaMappings = schemaMappings;
加载完毕后如下图:
接下来看下获取资源方法:
org.springframework.beans.factory.xml.PluggableSchemaResolver#resolveEntity
public InputSource resolveEntity(@Nullable 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 + "]");
}
// 如果systemId不为空
if (systemId != null) {
// <2021-02-28 11:26>,从映射map中获取资源jar包中的地址
String resourceLocation = getSchemaMappings().get(systemId);
// 如果是没有获取到地址,则尝试使用http协议开头的网络地址获取
if (resourceLocation == null && systemId.startsWith("https:")) {
// Retrieve canonical http schema mapping even for https declaration
resourceLocation = getSchemaMappings().get("http:" + systemId.substring(6));
}
// 如果是不为空,则创建ClassPathResource并返回
if (resourceLocation != null) {
Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
try {
InputSource source = new InputSource(resource.getInputStream());
// 设置publicId,systemId
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isTraceEnabled()) {
logger.trace("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
}
// 返回
return source;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not find XML schema [" + systemId + "]: " + resource, ex);
}
}
}
}
// Fall back to the parser's default behavior.
return null;
}
<2021-02-28 11:26>
处代码是从META-INF/spring.schema
文件中加载约束文件url地址和jar包中地址的映射关系,源码如下:
org.springframework.beans.factory.xml.PluggableSchemaResolver#getSchemaMappings
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("Loading schema mappings from [" + this.schemaMappingsLocation + "]");
}
try {
// 从spring.schema文件中加载验证文件URL和jar包地址映射关系
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.schemaMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded schema mappings: " + mappings);
}
schemaMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, schemaMappings);
this.schemaMappings = schemaMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load schema mappings from location [" + this.schemaMappingsLocation + "]", ex);
}
}
}
}
// 返回模式映射字典
return schemaMappings;
}
4.4:ResourceEntityResolver
源码:
org.springframework.beans.factory.xml.ResourceEntityResolver#resolveEntity
public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId)
throws SAXException, IOException {
// <2021-02-28 12:40>,直接调用父类方法获取
InputSource source = super.resolveEntity(publicId, systemId);
// 如果是没有获取到,并且systemId不为null
if (source == null && systemId != null) {
String resourcePath = null;
try {
// 使用u8编码
String decodedSystemId = URLDecoder.decode(systemId, "UTF-8");
String givenUrl = new URL(decodedSystemId).toString();
// <2021-02-28 12:41>
// 获取file://协议的项目所在地址
String systemRootUrl = new File("").toURI().toURL().toString();
// Try relative to resource base if currently in system root.
if (givenUrl.startsWith(systemRootUrl)) {
resourcePath = givenUrl.substring(systemRootUrl.length());
}
}
catch (Exception ex) {
// Typically a MalformedURLException or AccessControlException.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve XML entity [" + systemId + "] against system root URL", ex);
}
// No URL (or no resolvable URL) -> try relative to resource base.
resourcePath = systemId;
}
if (resourcePath != null) {
if (logger.isTraceEnabled()) {
logger.trace("Trying to locate XML entity [" + systemId + "] as resource [" + resourcePath + "]");
}
Resource resource = this.resourceLoader.getResource(resourcePath);
source = new InputSource(resource.getInputStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
if (logger.isDebugEnabled()) {
logger.debug("Found XML entity [" + systemId + "]: " + resource);
}
}
else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) {
// External dtd/xsd lookup via https even for canonical http declaration
String url = systemId;
if (url.startsWith("http:")) {
url = "https:" + url.substring(5);
}
try {
// 创建InputSource并设置publicId,sysetemId信息
source = new InputSource(new URL(url).openStream());
source.setPublicId(publicId);
source.setSystemId(systemId);
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve XML entity [" + systemId + "] through URL [" + url + "]", ex);
}
// Fall back to the parser's default behavior.
source = null;
}
}
}
return source;
}
<2021-02-28 12:40>
,这里调用的方法就是org.springframework.beans.factory.xml.DelegatingEntityResolver#resolveEntity
已经在前面分析过。<2021-02-28 12:41>
处代码是获取file://协议的项目根路径地址,如file:/Users/xb/Desktop/D/dongsir-dev/java-life-current/java-life/
是我本地的结果值,用于从系统根路径查找约束文件,换句话说就是处理约束文件在系统根路径内的情况。
5:获取Document
最终调用方法org.springframework.beans.factory.xml.DefaultDocumentLoader#loadDocument
获取文档对象,代码如下:
org.springframework.beans.factory.xml.DefaultDocumentLoader#loadDocument
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
到这里已经成功过去了xml文件对应的Document,接下来就是分析如何解析为BeanDefinition了,详细参考这里。