Spring如何解析xml配置文件?

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: 对于DTD模式验证:

<!DOCTYPE beans PUBLIC "-//Spring//DTD BEAN 2.0//EN"
        "http://www.springframework.org/dtd/spring-beans-2.0.dtd">

读取到两个参数:publicId:-//Spring//DTD BEAN 2.0//EN,

默认spring对于两种验证方式提供了不同的解析器。

this.dtdResolver = new BeansDtdResolver();
this.schemaResolver = new PluggableSchemaResolver(classLoader);

XSD解析:
默认的XSD解析PluggableSchemaResolver使用了private volatile Map<String, String> schemaMappings;来保存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的比较深入的步骤了,接下来

<bean id="myTestBean" class="MyTestBean.class"/>

经过默认命名空间的解析,接下来就是对bean标签的解析以及注册。
下次再写了。。。

以上只是自己的一些浅薄见解,是为了做一下总结,想努力地提升自己。如果有不对的地方,或者是哪里出错的地方,希望大佬可以指点一下小弟。。。