1.获取XML的验证模式

1.DTD与XSD验证模式的区别

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

一个DTD文档包含:元素的定义规则,元素间关系的对应规则,元素可使用的属性,可使用的实体或符号规则。DTD和XSD相比:DTD 是使用非 XML 语法编写的。 DTD 不可扩展,不支持命名空间,只提供非常有限的数据类型 。

XML Schema语言也就是XSD。XML Schema描述了XML文档的结构。 可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求。文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的。XML Schema本身是一个XML文档,它符合XML语法结构。可以用通用的XML解析器解析它。 一个XML Schema会定义:文档中出现的元素、文档中出现的属性、子元素、子元素的数量、子元素的顺序、元素是否为空、元素和属性的数据类型、元素或属性的默认 和固定值。

XSD是DTD替代者的原因,一是据将来的条件可扩展,二是比DTD丰富和有用,三是用XML书写,四是支持数据类型,五是支持命名空间。
在使用XML Schema文档对XML实例文档进行检验,除了要声明名称空间外(xmlns="http://www.springframework.org/schema/beans"),还必须指定该名称空间做对应的XML Schema文档的存储位置。通过schemaLocation属性来指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的URI,另一部分是名称空间所标识的XML Schema文件位置或URL地址(xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd")。


2.验证模式的读取

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;
	}
	// Hmm, we didn't get a clear indication... Let's assume XSD,
	// since apparently no DTD declaration has been found up until
	// detection stopped (before finding the document's root tag).
	return VALIDATION_XSD;
}

上面的意思就是如果设定了验证模式就使用设定的验证模式,否则使用自动检测的方式。而自动检测的模式是在XmlValidationModeDetector的validationModeDetector方法,代码如下:

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();
		}
	}

Spring检测验证模式的办法就是判断是否包含DOCTYPE,如果包含就是DTO,否则就是XSD.

2.获取Document

Spring中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.isTraceEnabled()) {
			logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
		}
		DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
		return builder.parse(inputSource);
	}

1.EntityResolver用法

EntityResolver是解决实体的基本界面,如果SAX应用程序需要为外部实体实现定制处理,则必须实现该接口,并且使用setEntityResolver方法项SAX驱动程序注册一个实例。也就是说,对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对文档进行一个验证。

EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如我们将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可。

EntityResolver接口中resolveEntity方法:有两个参数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" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc 
    http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-4.2.xsd 
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd">


读取到下面两个参数: 
publicId:null 
systemId:http://www.springframework.org/schema/aop/spring-aop-4.2.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.0dtd">
<beans>
... ...
<beans>


读取到下面两个参数: 
publicId:-//Spring//DTD BEAN 2.0//EN 
systemId:http://www.Springframework.org.dtd/Spring-beans-2.0dtd

在之前已经提到,验证文件默认的加载方式是通过URL进行网络下载获取,这样做会有延迟和网络中断等因素,一般的做法都是将验证文件防止在自己的工程里,那么怎么做才能将这个URL转换为自己工程里对应的地址文件呢?我们以加载DTD文件为例来看看Spring中是如果实现。根据之前Spring通过getEntityResolver()方法对EntityResolver的获取,在Spring中使用DelegatingEntityResolver类为EntityResolver的实现类,resolverEntity实现方法如下:

@Override
    public InputSource resolveEntity(String publicId, String systemId)
            throws SAXException, IOException {
        if (systemId != null) {

            // 验证模式为:dtd
            if (systemId.endsWith(DTD_SUFFIX)) {
                return this.dtdResolver.resolveEntity(publicId, systemId);
            }
            // 验证模式为:xsd
            else if (systemId.endsWith(XSD_SUFFIX)) {
                // 调用META-INF/Spring.schemas解析
                InputSource inputSource = this.schemaResolver.resolveEntity(publicId, systemId);
                return inputSource;
            }
        }
        return null;
    }

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

BeansDtdResolver中resolveEntity实现如下:
 

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;
    }

3.解析及注册BeanDefinitions

文件转换为 Document后,接下来就是提取及注册bean。

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        //使用DefaultBeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
		BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        //在实例化BeanDefinitionDocumentReader时候会将BeanDefinitionRegistry传入,默认使用继承自DefaultListableBeanFactory的子类
        //记录统计前BeanDefinition的加载个数
		int countBefore = getRegistry().getBeanDefinitionCount();
        //加载及注册
		documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
		//记录本次加载的BeanDefinition个数
        return getRegistry().getBeanDefinitionCount() - countBefore;
	}
protected void doRegisterBeanDefinitions(Element root) {
		// Any nested <beans> elements will cause recursion in this method. In
		// order to propagate and preserve <beans> default-* attributes correctly,
		// keep track of the current (parent) delegate, which may be null. Create
		// the new (child) delegate with a reference to the parent for fallback purposes,
		// then ultimately reset this.delegate back to its original (parent) reference.
		// this behavior emulates a stack of delegates without actually necessitating one.
		//专门处理解析
        BeanDefinitionParserDelegate parent = this.delegate;
		this.delegate = createDelegate(getReaderContext(), root, parent);

		if (this.delegate.isDefaultNamespace(root)) {
            //处理profile属性
			String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
			if (StringUtils.hasText(profileSpec)) {
				String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
						profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
				// We cannot use Profiles.of(...) since profile expressions are not supported
				// in XML config. See SPR-12458 for details.
				if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
					if (logger.isDebugEnabled()) {
						logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
								"] not matching: " + getReaderContext().getResource());
					}
					return;
				}
			}
		}
        //解析前处理 留给子类实现
		preProcessXml(root);
		parseBeanDefinitions(root, this.delegate);
        //解析后处理 留给子类实现
		postProcessXml(root);

		this.delegate = parent;
	}

解析并注册BeanDefinition

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
		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);
		}
	}
04-03 12:07