Spring源码阅读笔记03:xml配置读取

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Spring源码阅读笔记03:xml配置读取

木瓜芒果   2020-02-29 我要评论

  前面的文章介绍了IOC的概念,Spring提供的bean容器即是对这一思想的具体实现,在接下来的几篇文章会侧重于探究这一bean容器是如何实现的。在此之前,先用一段话概括一下bean容器的基本工作原理。顾名思义,bean容器的作用是替我们管理bean对象(简单的Java类对象)的。不管框架如何强大,还是需要我们程序员来告诉其一些必要信息的(比如要管理的bean对象的类相关信息、是否开启组件扫描等),这些我们称之为对Spring框架的配置,目前主流的配置方式是通过使用配置文件或注解。配置好之后,框架就需要将这些配置读取并保存到内存中(其实就是保存在对象里面)。经过这一步转化之后,Spring框架就能够帮助我们加载指定的类,然后将其实例化并且缓存起来以供需要的时候直接使用,这就是容器。当我们将容器关闭时,Spring框架会将之前创建的所有相关对象全部销毁,并释放资源。

  如上只是简单介绍了一下Spring提供的bean容器的基本工作原理,从中能够了解大体流程即可,真实的容器其工作原理远远比这复杂。本文主要总结Spring对配置的读取以及将配置转化保存到内存这部分,并且配置获取这部分的源码也只限于对xml配置文件的读取。

  上面说到的配置读取及初始化的功能对应前面文章中的代码看起来只有区区一行,如下:

BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("beans.xml"));

  这行代码做了两件事情:

  • 将xml配置文件封装成Resource;
  • 初始化BeanFactory;

1. 配置文件封装

  Spring的配置文件读取功能是封装在ClassPathResource中,对应前面的代码就是new ClassPathResource("bean.xml"),那么ClassPathResource又是做什么的呢?这个需要从头说起。

  其实呢Spring将其内部使用到的资源的获取方式独立抽取出来,通过Resource接口来封装底层资源,其接口定义如下:

public interface InputStreamSource {
    InputStream getInputStream() throws IOException;
}

public interface Resource extends InputStreamSource {
    boolean exists();
    boolean isReadable();
    boolean isOpen();
    URL getURL() throws IOException;
    URI getURI() throws IOException;
    File getFile() throws IOException;
    long contentLength() throws IOException;
    long lastModified() throws IOException;
    Resource createRelative(String relativePath) throws IOException;
    String getFilename();
    String getDescription();
}

  InputStreamSource是一个接口,它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象,该接口定义任何能返回InputStream的类,比如file普通文件、Classpath下的资源文件和ByteArray等。

  Resource接口用于抽象所有Spring内部使用到的底层资源:File、URL、Classpath等,其定义了一系列方法:

  • 首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen);
  • 另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法;
  • 为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative();
  • 在错误处理中需要详细地打印出错的资源文件,因而Resource还提供了getDescription()方法用于在错误处理中打印信息;

  Spring中对不同来源的资源文件类型都有相应的Resource实现:文件(FileSystemResource)、Classpath资源( ClassPathResource)、URL资源(UrIResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。

  Resource接口的作用是消除底层资源访问的差异,允许程序以一致的方式来访问不同的底层资源,而其实现是非常简单的,以getInputStream()方法实现为例,ClassPathResource中的实现方式是直接调用class或者classLoader提供的底层方法getResourceAsStream,而对于FileSystemResource的实现其实更简单,直接使用FileInputStream对文件进行实例化。

// ClasspathResource.java
public InputStream getInputStream() throws IOException {
    InputStream is;
    if (this.clazz != null) {
        is = this.clazz.getResourceAs你Stream(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;
}

// FileSystemResource
public InputStream getInputStream()throws IOException{
    return new FileInputStream(this.file);
}

  这样就可以将资源统一转化成InputStream供后续使用了,而前面示例代码中使用的xml配置文件是属于什么类型的Resource呢?其实从源码中我们就不难发现是属于ClassPathResource的,而new ClassPathResource("bean.xml")这句代码的内部实现就不细说了,无非就是初始化配置文件路径。

  一句话总结,Spring通过Resource接口抽象所有的资源,在容器启动的第一步就是将资源文件映射成Resource对象,以供后续通过流的方式来获取资源。

  现在配置文件封装到Resource中之后,后续Spring在初始化BeanFactory的过程中就可以方便地调用其getInputStream()方法来获取其对应的流了,然后做进一步转化。

public XmlBeanFactory(Resource resource) throws BeansException {
    this(resource, null);
}

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

  这段代码主要作用是初始化BeanFactory,其中this.reader.loadBeanDefinitions(resource)就是资源加载的真正实现,也是我们接下来的分析重点。

 

2. 转换成beanDefinition

  我们来看一下loadBeanDefinitions()方法具体的内部实现:

public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource));
}

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if (logger.isInfoEnabled()) {
        logger.info("Loading XML bean definitions from " + encodedResource.getResource());
    }
    // 通过属性来记录已经加载的资源
    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 {
        // 从encodeResource中获取封装的Resource对象并再次从Resouce中获取其中的inputStream
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
            // InputSource这个类并不来自于Spring,它来自org.xml.sax.InputSource
            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();
        }
    }
}

  这一部分其实还只是数据准备阶段,主要做了如下三件事情:

  • 封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装;
  • 获取输入流。从Resource中获取对应的InputStream并构造InputSource;
  • 通过构造好的InputSource实例和Resource实例继续调用方法:doLoadBeanDefinitions,这是真正的核心处理部分;
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {
    try {
        int validationMode = getValidationModeForResource(resource);
        Document doc = this.documentLoader.loadDocument(
                inputSource, getEntityResolver(), this.errorHandler, validationMode, isNamespaceAware());
        return registerBeanDefinitions(doc, resource);
    }
    catch (BeanDefinitionStoreException ex) {
        throw ex;
    }
    。。。catch若干异常
}

  不考虑其中异常类的代码,这段代码其实只做了三件事:

  • 获取XML文件的验证模式;
  • 加载XML文件,并得到对应的Document;
  • 根据返回的Document注册Bean信息;

   这3个步骤支撑着整个Spring容器部分的实现基础,尤其是第3步对配置文件的解析,逻辑非常的复杂,这里我们只分析第2步和第3步。

 2.1 获取Document

  XmIBeanFactoryReader类对于文档读取并没有亲力亲为,而是委托给了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);
}

  首先创建DocumentBuilderFactory,再通过DocumentBuilderFactory创建DocumentBuilder,然后解析inputSource来返回Document对象。这部分是JDK提供的功能,有兴趣的可以自行搜索,此处就不再赘述。

2.2 解析及注册BeanDefinitions

  当把文件转换为Document后,接下来的提取及注册bean就是重头戏。继续上面的分析,当程序已经拥有XML文档文件的Document实例对象时,就会被引入下面这个方法:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    // 使用DefaultBeanDefinitionDocumentReader实例化BeanDefinitionDocumentReader
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    // 将环境变量设置其中
    documentReader.setEnvironment(getEnvironment());
    // 在实例化BeanDefinitionReader时候会将BeanDefinitionRegistry传入,默认使用继承自DefaultListableBeanFactory的子类
    // 记录统计前BeanDefinition的加载个数
    int countBefore = getRegistry().getBeanDefinitionCount();
    // 加载及注册bean
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    // 记录本次加载的BeanDefinition个数
    return getRegistry().getBeanDefinitionCount() - countBefore;
}

   在这个方法中,加载及注册bean的逻辑是委托给BeanDefinitionDocumentReader指向的类来处理,这很好地应用了面向对象中单一职责的原则。BeanDefinitionDocumentReader是一个接口,其实例化的工作是在 createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader的registerBeanDefinitions()方法后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册:

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
    this.readerContext = readerContext;
    logger.debug("Loading bean definitions");
    Element root = doc.getDocumentElement();
    doRegisterBeanDefinitions(root);
}

  终于到了核心逻辑的底部doRegisterBeanDefinitions(root),如果说之前一直是XML加载解析的准备阶段,那么doRegisterBeanDefinitions算是真正地开始进行解析了。

protected void doRegisterBeanDefinitions(Element root) {
    // 处理profile属性
    String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
    if (StringUtils.hasText(profileSpec)) {
        String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
        if (!getEnvironment().acceptsProfiles(specifiedProfiles)) {
            return;
        }
    }
    // 专门处理解析
    BeanDefinitionParserDelegate parent = this.delegate;
    this.delegate = createDelegate(this.readerContext, root, parent);
    // 解析前处理,留给子类实现
    preProcessXml(root);
    parseBeanDefinitions(root, this.delegate);
    // 解析后处理,留给子类实现
    postProcessXml(root);
    
    this.delegate = parent;
}

  这里首先是对profile的处理,然后开始进行解析,preProcessXml(root)和postProcessXml(root)方法是空实现,留待用户继承DefaultBeanDefinitionDocumentReader后需要在Bean解析前后做一些处理时重写这两个方法。跟踪代码进入parseBeanDefinitions(root, this.delegate):

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // 对beans的处理
    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);
    }
}

  在Spring的XML配置里面有两大类Bean声明,一种是默认的,如:

<bean id="test"class="test.TestBean"/>

  另一类就是自定义的,如:

<tx: annotation-driven/>

  而两种方式的读取及解析差别是非常大的,如果采用 Spring默认配置,Spring当然知道该怎么做,但是如果是自定义的,那么就需要用户实现一些接口及配置了。对于根节点或者子节点如果是默认命名空间的话则采用parseDefaultElement方法进行解析,否则使用delegate. parseCustomElement方法对自定义命名空间进行解析。而判断是否默认命名空间还是自定义命名空间的办法其实是使用node. getNamespaceURI()获取命名空间,并与Spring中固定的命名空间http://www.Springframework.org/schema/beans进行比对。如果一致则认为是默认,否则就认为是自定义。

 

3. 总结

  本文主要总结Spring对配置(xml配置文件)的读取以及将配置转化保存到内存这部分,对xml配置的读取主要是将其转换成Resource,而将配置转化保存则主要是从Resource中获取InputStream并将其解析转化成BeanDefinition等对象保存起来。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们