转载

Spring IOC容器分析(二) 解析XML配置文件的前期工作

原创作品未经作者同意严禁私自转载,或私自修改后据为自有。违者将追究法律责任

Ⅰ. 令人震惊的凶杀案

深夜12点驾驶着 Chrome 汽车飞驰在 Tomcat 高速公路上,速度表飞速逼近160码,而我的心情却越来越忧虑。

我是 design pattern 市刑事探长 高Sir ,因 code 市发生一起重大凶杀案件,我被火速遣往 code 市参与破案。

以下是案情背景

codejavascript 大道 webpack 大酒店 444 号房间内发生一起谋杀,受害者 configuration.xml 先生于晚上11点被酒店服务员发现死亡。

犯罪现场十分整洁,只留下染红了半个房间的鲜血和一柄折断的 ClassPathResource 匕首,以及,一行神秘的注释 —— " configuration.xml

先生在某种意义上来说并没有死,只是转化为另一种形式,另一层更高境界的艺术..."

这是什么意思?没有人知道!但毫无疑问为了人民的安全,我必须找出这幕后的凶手!

FBI Warning: 前方内容血腥,如有不适感请即刻停止观看!!!

Ⅱ. 听说你也想当蜀黎破案?

我相信有很多朋友刚接触Spring就迫不及待地想要一探实现原理,但其实这样很快就会发现自己难以驾驭Spring的源码而自信心受挫。

要理解一个框架的工作原理,最重要的前提是: 你必须真的在实践中用过这个框架,了解它的基本使用方法

纸上谈兵是大忌,所以请还不熟悉Spring用法的朋友不要急功近利

千里之行,始于足下 —— 老子

任何事情都要有个头,解剖源代码更是如此,解析IOC容器的起点就是下面这段代码

XmlBeanFactory bf = new XmlBeanFactory(new ClassPathResource("configuration.xml"));
Dog dog = (Dog) bf.getBean("dog");

XML配置文件的内容如下, Dog 类是个空类(当前讲解解析流程不需要复杂的对象)

<?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="dog" class="com.bean.Dog" />
</beans>

我先来教大家快速读懂源码的秘技

读懂Spring源代码最有效的方式只有一个 —— Debug代码一步一步跟进

Ⅲ. 从凶器 ClassPathResource 开始

就是这把利刃刺穿了 configuration.xml 先生,造成他的大量出血并死亡,它是如此的锋利和轻盈,我们推断 configuration.xml 先生甚至都没有发觉自己受了重伤,直到大量的失血让他失去了行动力。

可是究竟是什么人要下此毒手呢?

new ClassPathResource("configuration.xml")

下面是 ClassPathResource 的家族成员

Spring IOC容器分析(二) 解析XML配置文件的前期工作

可以看到 ClassPathResource 的基本接口是 InputStreamSourceResource

Spring框架定制了自己的 Resource 抽象,从最基础的接口 InputStreamSource 的名称也可以看出,它们其实无非是对底层的 InputStream 做了些封装以方便使用罢了

具体的接口定义大家可以自行查阅源码, ClassPathResource 顾名思义就是用来操作位于 类装载路径上的资源 ,它有两个构造器

public ClassPathResource(String path) {
        this(path, (ClassLoader) null);
    }

    public ClassPathResource(String path, ClassLoader classLoader) {
        Assert.notNull(path, "Path must not be null");
        String pathToUse = StringUtils.cleanPath(path);
        if (pathToUse.startsWith("/")) {
            pathToUse = pathToUse.substring(1);
        }
        this.path = pathToUse;
        this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
    }

主要的工作集中在 StringUtils.cleanPath(path) 方法中,这个方法做了两件事

  1. path 中的Windows风格的路径分隔符 '/' 转换成 '/'
  2. 压缩 path ,比如将 file://core/../core/io/Resource.class 这样的路径压缩成 file://core/io/Resource.class

new XmlBeanFactory(new ClassPathResource("configuration.xml")) 的后续执行流程中,会调用 ClassPathResource.getInputStream() 这个 InputStreamSource 父接口中的方法,我们来看下这个方法的实现

@Override
    public InputStream getInputStream() throws IOException {
        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;
    }

可以看到获取ClassPath中的资源流十分简单,直接调用 ClassLoader.getResourceAsStream() 方法,剩下和操作系统的互动交给JVM就好了。

另外还需要注意的是 ClassPathResourcehashCode 实现,它会返回关联资源路径的 String 哈希值。关注这个是因为加载xml文件会去根据 ClassPathResource 的哈希码检测循环加载配置文件的情况

@Override
public int hashCode() {
    return this.path.hashCode();
}

午夜1点,唯一的线索分析完了,可是凶手很狡猾,除此之外犯罪现场没有留下任何有用的信息,我还是一筹莫展。无奈只能先去休息

Ⅳ. 转机!抓住了犯罪嫌疑人 XmlBeanFactoryXmlBeanDefinitionReader

凌晨2点。"叮铃铃"一阵急促的电话声将我惊醒,原来 code 市的破案人员通过监控系统 Intellj IdeaClassPathResource 对象进行调用分析,发现了 XmlBeanFactoryXmlBeanDefinitionReader 曾经与 ClassPathResource 有过接触。

这两个恶棍喝得酩酊大醉,经过一系列的审讯 XmlBeanFactory 只肯招供他仅仅把 ClassPathResource 交给了 XmlBeanDefinitionReader ,其他的一概不知,可恶!

XmlBeanDefinitionReader 则是醉得不省人事,趴在审讯桌上呼呼大睡!

根据我们的代码入口

new XmlBeanFactory(new ClassPathResource("configuration.xml"))

其实它做的事情就是这个

public XmlBeanFactory(Resource resource) throws BeansException {
        this(resource, null);
    }
    public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        super(parentBeanFactory);
        this.reader.loadBeanDefinitions(resource);
    }

将工作都交给 XmlBeanDefinitionReader.loadBeanDefinitions() 方法完成

XmlBeanDefinitionReader 做的事情就是把Xml配置文件内容转换成 Bean Definition 对象表示。它主要依靠以下两个类的协作完成解析工作

  1. DefaultDocumentLoader
    这个类主要是使用Java内置的 javax.xml 类库完成Xml文件到 Document 对象的转换
  2. BeanDefinitionDocumentReader
    这个类使用上一步的 Document 对象完成从xml标签到 Bean Definition 的转换,并将 Bean Definition 注册到IOC容器中

下面我们从 XmlBeanDefinitionReader 的入口方法一步一步跟进

/* XmlBeanDefinitionReader.class */
    @Override
    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
        return loadBeanDefinitions(new EncodedResource(resource));
    }

在第Ⅲ节中,我们了解到Spring将资源都抽象成 Resource ,而这个 EncodedResource 只是多了个编码属性的设置,当调用 EncodedResource.getReader() 时,会使用设置的编码

/* EncodedResource.class */
    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());
        }
    }

loadBeanDefnitions() 又调用了另一个重载方法

/* XmlBeanDefinitionReader.class */
    public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
        Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet<EncodedResource>(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
        if (!currentResources.add(encodedResource)) {
            throw new BeanDefinitionStoreException(
                    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        }
        try {
            InputStream inputStream = encodedResource.getResource().getInputStream();
            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
            finally {
                inputStream.close();
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        }
        finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
    }

可以看到其内部是通过 HashSet<EncodedResource> currentResources 这个HashSet集合进行重复加载相同配置文件的检测的,具体操作自然是检查新加入的 EncodedResourcehashCode() 是否重复

/* EncodedResource.class*/
    @Override
    public int hashCode() {
        return this.resource.hashCode();
    }

前面说过, ClassPathResource 的哈希值就是 String 表示的配置文件路径的哈希值,这就意味着如果重复加载了路径相同的xml资源则会抛出 Cyclic Loading 异常

/* ClassPathResource.class */
    @Override
    public int hashCode() {
        return this.path.hashCode();
    }

最后的工作交给了 doLoadBeanDefinitions() 方法

/* XmlBeanDefinitionReader.class */
    protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) throws BeanDefinitionStoreException {
        try {
            Document doc = doLoadDocument(inputSource, resource);
            return registerBeanDefinitions(doc, resource);
        }
        catch (...) {
            ...
        }
    }

下面我们来分析 doLoadDocument() 方法

/* XmlBeanDefinitionReader.class */
    protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
        return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
                getValidationModeForResource(resource), isNamespaceAware());
    }

this.documentLoader 就是前面提到的 DefaultDocumentLoader ,利用 javax.xml 类库完成配置文件到 Document 对象的转换

/* XmlBeanDefinitionReader.class */
    private DocumentLoader documentLoader = new DefaultDocumentLoader();

DefaultDocumentLoader 需要 EntityResolverValidationMode 的辅助进行配置文件到 Document 的转换

/* XmlBeanDefinitionReader.class */
    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 的作用是什么呢?请看下面这个简单的配置文件

<?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="com.bean.MyTestBean" />
</beans>

DefaultDocumentLoader 在使用 javax.xml 内置库去解析xml文件的过程中,当遇到命名空间对应的 schema 文件时

(本例即 http://www.springframework.org/schema/beans/spring-beans.xsd ),它会去访问这个URL尝试获得 schema 文件内容,

然而网络环境是很复杂的,在不同地域不同的机器上,难以保证网络的畅通性,如果网络一直拥塞则解析xml过程就会一直失败。

为了避免依赖不可靠的网络,用户可以编写自定义的 EntityResolver 来为解析过程提供其他途径获取 schema 文件内容。

所谓 schema 就是一组规则,用来定义xml命名空间下能够合法出现的标签和每个标签合法的属性。

比如<beans>命名空间对应的 schema —— spring-beans.xsd 就定义了<beans>标签的合法属性 default-autowiredefault-lazy-init 等等,

以及<beans>下能够出现的标签<bean>、<import>等以及它们各自合法的属性。

我们重点来关注下 ResourceEntityResolver 的父类 DelegatingEntityResolverresolveEntity() 方法

/* DelegatingEntityResolver.class */
    @Override
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
        if (systemId != null) {
            if (systemId.endsWith(DTD_SUFFIX)) {
                return this.dtdResolver.resolveEntity(publicId, systemId);
            }
            else if (systemId.endsWith(XSD_SUFFIX)) {
                return this.schemaResolver.resolveEntity(publicId, systemId);
            }
        }
        return null;
    }

systemId 就是 "http://www.springframework.org/schema/beans/spring-beans.xsd" ,然后具体解析过程交给 schemaResolver

/* DelegatingEntityResolver.class */
    public DelegatingEntityResolver(ClassLoader classLoader) {
        this.dtdResolver = new BeansDtdResolver();
        this.schemaResolver = new PluggableSchemaResolver(classLoader);
    }

在子类 ResourceEntityResolver 构造时会调用上面的构造器,可以看到 schemaResolver 被赋值为 PluggableSchemaResolver ,我们进入它的 resolveEntity() 方法

/* PluggableSchemaResolver.class */
    
    1. public static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas";
    
    2. public PluggableSchemaResolver(ClassLoader classLoader) {
        this.classLoader = classLoader;
        this.schemaMappingsLocation = DEFAULT_SCHEMA_MAPPINGS_LOCATION;
    }
    
       @Override
    3. public InputSource resolveEntity(String publicId, String systemId) throws IOException {
        if (systemId != null) {
            String resourceLocation = getSchemaMappings().get(systemId);
            if (resourceLocation != null) {
                Resource resource = new ClassPathResource(resourceLocation, this.classLoader);
                try {
                    InputSource source = new InputSource(resource.getInputStream());
                    source.setPublicId(publicId);
                    source.setSystemId(systemId);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation);
                    }
                    return source;
                }
                catch (FileNotFoundException ex) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex);
                    }
                }
            }
        }
        return null;
    }

getSchemaMappings() 方法会根据 META-INF/spring.schemas 文件中的内容找到对应 systemId 的物理文件位置,然后就能获得对应的文件内容。以下是我本地 spring-beans-4.3.14.RELEASE.jar 中的 spring/schemas 文件的部分截图

Spring IOC容器分析(二) 解析XML配置文件的前期工作

可以看到真正的规则文件都位于 org.springframework.beans.factory.xml 包下,所以这就避免了网络环境的不可靠导致解析xml失败。

使用 javax.xml 类库进行解析工作所需要的 EntityResolverValidation Mode 还剩下一个需要分析

/* XmlBeanDefinitionReader.class */
    1. private int validationMode = VALIDATION_AUTO;
    
    2. 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;
    }

validation mode 有四种 —— VALIDATION_NONE , VALIDATION_AUTO , VALIDATION_DTD , VALIDATION_XSD ,在上面的方法中只有在验证模式为 VALIDATION_AUTO 时才需要进一步检测。 Validation ModeXmlBeanDefinitionReader 中初始化了为 AUTO 模式,用户可以调用 XmlBeanDefinitionReader.setValidationMode() 进行设置

/* XmlBeanDefinitionReader.class */
    
    1. private final XmlValidationModeDetector validationModeDetector = new XmlValidationModeDetector();
    
    2. protected int detectValidationMode(Resource resource) {
        ...
        try {
            return this.validationModeDetector.detectValidationMode(inputStream);
        }
        catch (IOException ex) {
            ...
        }
    }

继续分析 ValidationModeDetector.detectValidationMode()

/* XmlValidationModeDetector.class */
    public int detectValidationMode(InputStream inputStream) throws IOException {
        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();
        }
    }

可以看到核心的分析在于 hasDoctype() 方法,这个方法很无脑...如果xml文件的非注释内容的第一行含有" DOCTYPE "字符串,则认为是 VALIDATION_DTD 模式,否则是 VALIDATION_XSD 模式

获得了这些必要信息后,就可以将Xml文件解析为 Document 对象了

/* XmlBeanDefinitionReader.class */
    protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
        return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
                getValidationModeForResource(resource), isNamespaceAware());
    }

Ⅴ. 渐渐清晰的案情脉络

解析Xml文件是个复杂的过程,而现在我们才刚刚把解析的前期工作 —— 将Xml文件解析为 Document 对象讲完。为了更好地理解整个解析过程,我制作了一份时序图给大家提供参考。

然而很多细节是没法体现在图中的,要想理解代码逻辑就必须要 Debug 代码步步跟进

Spring IOC容器分析(二) 解析XML配置文件的前期工作

后面的篇章就是重点了 —— 遍历 Document 的元素,注册 Bean Definition

如果读者对本文有疑问,欢迎在评论区留言交流

原文  https://segmentfault.com/a/1190000014585880
正文到此结束
Loading...