xml配置文件是Spring中极其重要的一部分,让我们一起看一下spring解析xml文件的。
以下是一段简单的通过类路径下的test.xml文件加载bean获得BeanFactory的代码:
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("test.xml"));
一行代码,spring做的事情极其的复杂,主要分为以下几步:
1, 把资源文件进行封装,封装为Resource,有了Resource就可以对所有的资源文件进行统一处理
ClassPathResource的核心逻辑: (其实是简单的用class或classLoader读取类路径的资源文件)
InputStream is;
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
2,在XmlBeanFactory构造方法中调用XmlBeanDefinitionReader开始Bean的加载
this.reader.loadBeanDefinitions(resource);
这句代码是整个资源加载的切入点。
3, EncodedResource对Resoure进行编码的处理,设置了编码属性的时候Spring会使用相应的编码作为输入流的编码
public Reader getReader() throws IOException {
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
else if (this.encoding != null) {
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
}
else {
return new InputStreamReader(this.resource.getInputStream());
}
}
这段代码设置了输入流的编码,但是我在看源码的过程中并没有发现其有被调用的情况,
在注释上@see getInputStream()
@Override
public InputStream getInputStream() throws IOException {
return this.resource.getInputStream();
}
发现直接就是调用resource方法。
到底怎么回事???我看的时候一脸蒙蔽0。0
4,构造InputSource,InputSource并不来自于Spring,全路径是org.xml.sax,SAX解析xml的时候会使用InputSource来决定如何读取xml文件
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
原来encode在这里实现了,那么上面的EncodeResource只是起到了简单的提供encode或者charset的作用,
这样的话那么设计的时候是不是可以把EncodeResource去掉呢,直接在InputSource.setEncoding即可。
5,现在到达了核心处理部分
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
首先getValidationModeForResource
获取xml文件的验证模式(DTD或者XSD),可以自己设置验证方式,否则默认是开启VALIDATION_AUTO
即自动获取验证模式的,底层实现是InputStream读取xml文件看xml文件是否包含DOCTYPE
单词,包含的话就是DTD,否则返回XSD。看一下关键逻辑:
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);
接着获取Document,Spring并没有进行特殊的对xml文档的处理,使用了SAX解析xml文档,三步走:先创建DocumentBuilderFactory,接着获取DocumentBuilder,最后解析InputStream返回Document对象。
6, 可以看一下EntityResolver
类,EntityResolver是什么呢,对于一个xml的验证,XSD或者DTD文件默认是从网上下载的,可以的话一般都是把DTD文件放在工程之中,而EntityResolver就是提供了一个如何寻找DTD文件的声明。
对于xsd模式:
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
读取到以下的两个参数: publicId:
null ,systemId:
http://www.springframework.org/schema/beans/spring-beans.xsd
对于DTD模式验证:
读取到两个参数:publicId:
-//Spring//DTD BEAN 2.0//EN,
systemId:
http://www.springframework.org/dtd/spring-beans-2.0.dtd
默认spring对于两种验证方式提供了不同的解析器。
this.dtdResolver = new BeansDtdResolver();
this.schemaResolver = new PluggableSchemaResolver(classLoader);
XSD解析:
默认的XSD解析PluggableSchemaResolver使用了private volatile Map
来保存schemaURL->local schema path的映射,默认的schemaMappingLocation位于META-INF/spring.schemas ,通过getSchemaMapping来获取这个映射,其中使用双重校验锁的方式来实现单例模式:
if (this.schemaMappings == null) {
synchronized (this) {
if (this.schemaMappings == null) {
...
}
}
}
有了map,接下来获取InputSource就是使用systemId获取resourceLocation,实现比较简单。
DTD解析:
DTD解析是直接截取systemId的最后的xx.dtd去当前路径下面寻找。
7,快了,快了再坚持一下,最后解析及注册bean
通过DocumentBuilder获取Document之后,就剩下
return registerBeanDefinitions(doc, resource);
方法的返回值是发现的定义的bean的数目,方法主要内容:
//创建DocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
//注册beanDefinition
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
//获得已有的beanDefinition数目
int countBefore = getRegistry().getBeanDefinitionCount();
return getRegistry().getBeanDefinitionCount() - countBefore;
注册bean的时候首先使用一个BeanDefinitionParserDelegate
类来判断是否是默认命名空间,实现是通过判断namespace uri 是否和默认的uri相等:
public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";
public boolean isDefaultNamespace(String namespaceUri) {
return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri));
}
对于默认的命名空间,首先开始的是对profile属性解析,profile用得最多的是DataSource在不同环境下使用不同的bean,spring使用StringTokenizer来进行字符串的分割,但是jdk为了兼容性是推荐使用String.split()方法的:
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);
接下来的解析使用了模板模式,preProcessXml和postProcessXml都是空方法,是为了方便之后的子类在解析之前之后进行一些处理。只需要覆写这两个方法即可。
在parseBeanDefinitions方法中spring对不同命名空间的元素的解析使用不同的方法:
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
对于不同的bean声明,spring的处理方法我先看一下,下次再写了。。
为了首尾呼应,回到开始的
BeanFactory bf = new XmlBeanFactory(new ClassPathResource("test.xml"));
我们已经进入到解析bean的比较深入的步骤了,接下来
id="myTestBean" class="MyTestBean.class"/>
经过默认命名空间的解析,接下来就是对bean标签的解析以及注册。
下次再写了。。。