Spring5源码分析(007)——IoC篇之加载 BeanDefinition(的大致流程)

注:《Spring5源码分析》汇总可参考:Spring5源码分析(002)——博客汇总 


  本文主要讲述加载 BeanDefinition 的大致流程,并没有涉及太多的细节,概括来说就是:XML Resource --> EncodedResource --> org.xml.sax.InputSource --> XML Document --> BeanDefinition。目录结构如下:

1、回顾创建 IoC 容器的大概过程

  回到上一篇文章提到的创建 IoC 容器的大概过程(这里重新复制一下,也可以回头看看笔者的上一篇介绍:Spring5源码分析(006)——IoC篇之核心类DefaultListableBeanFactory和XmlBeanDefinitionReader):

Resource resource = new ClassPathResource("beans.xml"); // (1.1)
// ClassPathResource resource = new ClassPathResource("beans.xml"); // (1.2)
// Resource[] resources = PathMatchingResourcePatternResolver.getResources(locationPattern); // (1.3),需要遍历获取 BeanDefinition
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); // (2)
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); // (3)
reader.loadBeanDefinitions(resource); // (4)
  • 1、获取资源:资源的定位,可以是直接创建并提供资源,或者通过 PathMatchingResourcePatternResolver 对指定的资源路径进行解析定位然后获取对应的(多个)资源;
  • 2、获取 BeanFactory:这里使用默认的 DefaultListableBeanFactory;
  • 3、根据 BeanFactory 创建一个 BeanDefinitionReader 实例对象,即资源解析器;
  • 4、装载资源:也即是 bean 的加载过程,解析并装配成 BeanDefinition,然后就可以根据 BeanDefinition 做各种事情了。

注:如果 (4) 中直接使用的是 BeanDefinitionReader.loadBeanDefinitions(String location) ,则最终是通过 (1.3) 定位获取到资源,即本文开头的那个方法中的如下这一行(参考:Spring5源码分析(005)——IoC篇之统一资源加载Resource和ResourceLoader),然后再对 Resource[] resources 遍历调用 reader.loadBeanDefinitions(resource),一样的效果。

  整个处理流程总结起来的话,其实分为这几个步骤:先是资源的定位(获取资源),之后就是对资源进行解析并装配成 BeanDefinition,然后就是将 BeanDefinition 注册好(Map 存储),最后便可以根据注册的 BeanDefinition 来进行各种操作了。

  • 资源的定位和获取:这里所谓的资源,很大程度上就是指 bean 的配置元数据了,包括我们举例的 bean 的 XML 配置信息。容器的初始化的第一步,就是先找到这些配置资源,这部分讲解参考:Spring5源码分析(005)——IoC篇之统一资源加载Resource和ResourceLoader
  • 资源的装载:也即是对资源进行解析并装配成 BeanDefinition。这部分时通过 BeanDefinitionReader 读取、解析 Resource 配置资源,将用户定义的所有的 <bean /> 转换成 IoC 容器内部的数据结构 BeanDefinition。
  • 注册:将解析好的 BeanDefinition 通过接口 BeanDefinitionRegistry (实际上是实现类 DefaultListableBeanFactory )进行注册,其内部是使用一个 BeanDefinition Map 来实现,进行 BeanDefinition 的维护。
    • 默认情况下,这里仅仅是 BeanDefinition 的维护,并不涉及到依赖注入和 bean 的直接创建,bean 实例化发生在应用第一次调用 getBean 时。
    • 如果 bean 定义中设置了 lazyinit = false,那么这个 bean 的依赖注入会在容器初始化的时候完成。
/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

2、DefaultListableBeanFactory 初始化

在进入正戏前,先看看 new DefaultListableBeanFactory() 里面处理了什么事情,它调用了父类的构造器,如下:

/**
 * Create a new DefaultListableBeanFactory.
 */
public DefaultListableBeanFactory() {
    super();
}

看下父类 AbstractAutowireCapableBeanFactory 的构造器:

/**
 * Create a new AbstractAutowireCapableBeanFactory.
 */
public AbstractAutowireCapableBeanFactory() {
    super();
    ignoreDependencyInterface(BeanNameAware.class);
    ignoreDependencyInterface(BeanFactoryAware.class);
    ignoreDependencyInterface(BeanClassLoaderAware.class);
}

这里看看 ignoreDependencyInterface 方法是什么情况:

/**
 * Ignore the given dependency interface for autowiring.
 * <p>This will typically be used by application contexts to register
 * dependencies that are resolved in other ways, like BeanFactory through
 * BeanFactoryAware or ApplicationContext through ApplicationContextAware.
 * <p>By default, only the BeanFactoryAware interface is ignored.
 * For further types to ignore, invoke this method for each type.
 * <p>忽略给定依赖接口的自动装配。
 * <p>这通常会被应用程序上下文用于注册通过其他方式解析的依赖项,如通过 BeanFactoryAware 注入的 BeanFactory,
 * 或通过 ApplicationContextAware 注入的 ApplicationContext。
 * <p>默认情况下,只有 BeanFactoryAware 接口被忽略。如果还要忽略其他类型,请为每个类型调用此方法。
 * @see org.springframework.beans.factory.BeanFactoryAware
 * @see org.springframework.context.ApplicationContextAware
 */
public void ignoreDependencyInterface(Class<?> ifc) {
    this.ignoredDependencyInterfaces.add(ifc);
}

/**
 * Dependency interfaces to ignore on dependency check and autowire, as Set of
 * Class objects. By default, only the BeanFactory interface is ignored.
 * <p>忽略依赖检查和自动装配的依赖接口集合。默认情况下,只忽略 BeanFactory 接口。
 */
private final Set<Class<?>> ignoredDependencyInterfaces = new HashSet<>();

可以看到,ignoreDependencyInterface 方法的主要功能是忽略给定接口的依赖检查和自动装配功能,这么做的目的是什么呢,会起到什么样的效果呢?

举例来说,当 A 中有属性 B ,那么当 Spring 在获取 A 的 Bean 的时候如果其属性 B 还没有初始化,那么 Spring 会自动初始化 B ,这也是 Spring 中提供的一个重要特性。但是,某些情况下,B 不会被初始化,其中的一种情况就是 B 实现了 BeanNameAware 接口。Spring 的 api doc 中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析 Application 上下文注册依赖,类似于 BeanFactory 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过 ApplicationContextAware 进行注入 。

3、loadBeanDefinitions(Resource resource)

回到重头戏 XmlBeanDefinitionReader.loadBeanDefinitions(resource),也即是 bean 的加载,看下 loadBeanDefinitions(resource) 的实际处理过程:

/**
 * Load bean definitions from the specified XML file.
 * <p>从指定的 XML 文件加载 bean definitions
 * @param resource the resource descriptor for the XML file
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 */
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
    return loadBeanDefinitions(new EncodedResource(resource));
}

EncodedResource 的作用:通过类名称,可以大致推断出这个类主要是用于对资源文件的编码(字符集)进行处理的。其中一个比较重要的方法是 Reader getReader(),用于读取(指定编码集的)字符流。EncodedResource 的源码也比较简单;

/**
 * Holder that combines a {@link Resource} descriptor with a specific encoding
 * or {@code Charset} to be used for reading from the resource.
 * <p>结合了指定用于资源读取的编码或者字符集和 Resource 描述符的 Holder
 * <p>Used as an argument for operations that support reading content with
 * a specific encoding, typically via a {@code java.io.Reader}.
 * <p>用作支持使用特定编码读取内容的操作(通常是通过java.io.Reader)的参数。
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @since 1.2.6
 * @see Resource#getInputStream()
 * @see java.io.Reader
 * @see java.nio.charset.Charset
 */
public class EncodedResource implements InputStreamSource {

    private final Resource resource;

    @Nullable
    private final String encoding;

    @Nullable
    private final Charset charset;

    // constructor here...

    // getter here...

    /**
     * Determine whether a {@link Reader} is required as opposed to an {@link InputStream},
     * i.e. whether an {@linkplain #getEncoding() encoding} or a {@link #getCharset() Charset}
     * has been specified.
     * @see #getReader()
     * @see #getInputStream()
     */
    public boolean requiresReader() {
        return (this.encoding != null || this.charset != null);
    }

    /**
     * Open a {@code java.io.Reader} for the specified resource, using the specified
     * {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
     * (if any).
     * <p>使用指定的字符集或编码(如果有的话)为指定的资源打开一个 java.io.Reader
     * @throws IOException if opening the Reader failed
     * @see #requiresReader()
     * @see #getInputStream()
     */
    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());
        }
    }

    /**
     * Open an {@code InputStream} for the specified resource, ignoring any specified
     * {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}.
     * <p>为指定的 resource 打开 InputStream,忽略任何字符编码(Charset 和 encoding)
     * @throws IOException if opening the InputStream failed
     * @see #requiresReader()
     * @see #getReader()
     */
    @Override
    public InputStream getInputStream() throws IOException {
        return this.resource.getInputStream();
    }
  // ...

}

loadBeanDefinitions(Resource resource) 只是做了封装 Resource 的操作,然后又委托给 loadBeanDefinitions(EncodedResource encodedResource) 进行实际的处理:

private final ThreadLocal<Set<EncodedResource>> resourcesCurrentlyBeingLoaded =
        new NamedThreadLocal<>("XML bean definition resources currently being loaded");
/**
 * Load bean definitions from the specified XML file.
 * <p>从指定的XML文件加载 bean definitions
 * @param encodedResource the resource descriptor for the XML file,
 * allowing to specify an encoding to use for parsing the file
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 */
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    Assert.notNull(encodedResource, "EncodedResource must not be null");
    if (logger.isTraceEnabled()) {
        logger.trace("Loading XML bean definitions from " + encodedResource);
    }
   // 1、记录已经加载的资源
    Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
    if (currentResources == null) {
        currentResources = new HashSet<>(4);
        this.resourcesCurrentlyBeingLoaded.set(currentResources);
    }
    if (!currentResources.add(encodedResource)) {
        throw new BeanDefinitionStoreException(
                "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
    }
    try {
           // 2、从 EncodedResource 中封装的 Resource 中获取对应的 InputStream
        InputStream inputStream = encodedResource.getResource().getInputStream();
        try {
                   // 封装成 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);// 3、移除已记录的资源
        if (currentResources.isEmpty()) {
            this.resourcesCurrentlyBeingLoaded.remove();
        }
    }
}

这里说明下处理流程:

  • 1、记录已经加载的资源:这里通过一个 ThreadLocal 属性 resourcesCurrentlyBeingLoaded 来获取当前线程在加载地资源,如果没有,则把当前资源添加进去,然后进行下一步地加载;如果资源已存在,则抛出异常 BeanDefinitionStoreException,从这个异常地描述说明 “Detected cyclic loading” 来看,其实时为了避免循环加载,出现死循环。因此最后加载成功后就把资源给移除了
  • 2、从 EncodedResource 中封装的 Resource 中获取对应的 InputStream 并构造 org.xml.sax.InputSource,通过 SAX 读取 XML 文件的方式来准备 InputSource 对象,最后调用真正的核心处理逻辑部分 doLoadBeanDefinitions(inputSource, encodedResource.getResource())

4、doLoadBeanDefinitions(InputSource inputSource, Resource resource)

这个是加载 bean 的核心逻辑,源码如下:

/**
 * Actually load bean definitions from the specified XML file.
 * <p>实际处理从指定的 XML 文件加载 bean definitions。
 * @param inputSource the SAX InputSource to read from
 * @param resource the resource descriptor for the XML file
 * @return the number of bean definitions found
 * @throws BeanDefinitionStoreException in case of loading or parsing errors
 * @see #doLoadDocument
 * @see #registerBeanDefinitions
 */
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
        throws BeanDefinitionStoreException {

    try {
        // 1、加载 XML 文件,并得到对应的Document(内部会先获取对 XML 文件的验证模式)
        Document doc = doLoadDocument(inputSource, resource);
        // 2、根据 Document 解析和注册 BeanDefinition
        int count = registerBeanDefinitions(doc, resource);
        if (logger.isDebugEnabled()) {
            logger.debug("Loaded " + count + " bean definitions from " + resource);
        }
        return count;
    }
    catch (BeanDefinitionStoreException ex) {
        throw ex;
    }
    catch (SAXParseException ex) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                "Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
    }
    catch (SAXException ex) {
        throw new XmlBeanDefinitionStoreException(resource.getDescription(),
                "XML document from " + resource + " is invalid", ex);
    }
    catch (ParserConfigurationException ex) {
        throw new BeanDefinitionStoreException(resource.getDescription(),
                "Parser configuration exception parsing XML from " + resource, ex);
    }
    catch (IOException ex) {
        throw new BeanDefinitionStoreException(resource.getDescription(),
                "IOException parsing XML document from " + resource, ex);
    }
    catch (Throwable ex) {
        throw new BeanDefinitionStoreException(resource.getDescription(),
                "Unexpected exception parsing XML document from " + resource, ex);
    }
}
/**
 * Actually load the specified document using the configured DocumentLoader.
 * <p>使用配置的 DocumentLoader 对指定 document 的进行实际的加载处理
 * @param inputSource the SAX InputSource to read from
 * @param resource the resource descriptor for the XML file
 * @return the DOM Document
 * @throws Exception when thrown from the DocumentLoader
 * @see #setDocumentLoader
 * @see DocumentLoader#loadDocument
 */
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
    return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
            getValidationModeForResource(resource), isNamespaceAware());
}

/**
 * Determine the validation mode for the specified {@link Resource}.
 * If no explicit validation mode has been configured, then the validation
 * mode gets {@link #detectValidationMode detected} from the given resource.
 * <p>Override this method if you would like full control over the validation
 * mode, even when something other than {@link #VALIDATION_AUTO} was set.
 * <p>确定指定资源的验证模式。如果没有配置显式的验证模式,则从给定资源检测获取验证模式。
 * <p>如果需要完全控制验证模式,请覆盖此方法,即使在设置了VALIDATION_AUTO以外的内容时也是如此。
 * @see #detectValidationMode
 */
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;
}
  • 1、调用 doLoadDocument(InputSource inputSource, Resource resource),加载 XML 文件,并得到对应的Document(内部会先获取对 XML 文件的验证模式)。
    • 获取 Document 又分为2个步骤:
    • 1.1、通过调用 getValidationModeForResource(Resource resource) 来获取指定 XML 资源的验证模式,也即是 xml 开头常见到的各种 DTD 和 XSD 了。
    • 1.2、通过调用 DocumentLoader.loadDocument(InputSource inputSource, EntityResolver entityResolver, ErrorHandler errorHandler, int validationMode, boolean namespaceAware) 来获取实际的 Document 实例。
  • 2、调用 registerBeanDefinitions(Document doc, Resource resource),根据 Document 解析和注册 BeanDefinition。

总的来说,在上面冗长的代码其实只做了三件事,这三件事的每一件都必不可少:

  • 1、获取对 XML 文件的验证模式。
  • 2、加载 XML 文件,并得到对应的 Document。
  • 3、解析返回的 Document 并注册 Bean 信息。

这 3 个步骤支撑着整个 Spring 容器部分的实现,尤其是第 3 步对配置文件的解析,逻辑非常的复杂,后面我们会继续对这三3个步骤进行分析讲解。下一篇将从获取 XML 文件的验证模式讲起。
(未完待续 ing。。。)

5、总结:加载 BeanDefinition 的大致流程

本篇其实只是初步讲述了加载 BeanDefinition 的大致流程,并没有深入讲解具体的加载过程,这里重新汇总如下:

  • l、封装资源文件:首先对参数 Resource 使用 EncodedResource 类进行封装。
  • 2、获取输入流:从 EncodedResource 中封装的 Resource 中获取对应的 InputStream 并构造 org.xml.sax.InputSource。
  • 3. 通过构造的 InputSource 实例和 Resource 实例继续调用函数 doLoadBeanDefinitions(InputSource inputSource, Resource resource) 进行实际的处理。
  • 4、doLoadBeanDefinitions 中的核心处理流程:
    • 4.1、获取对 XML 文件的验证模式。
    • 4.2、加载 XML 文件,并得到对应的 Document。
    • 4.3、解析返回的 Document 并注册 Bean 信息。

概括下就是:XML Resource --> EncodedResource --> org.xml.sax.InputSource --> XML Document --> BeanDefinition

6、参考

ioc

相关推荐