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、参考
- spring 官方文档 5.2.3.RELEASE:https://docs.spring.io/spring-framework/docs/5.2.3.RELEASE/spring-framework-reference/core.html
- Spring源码深度解析(第2版),郝佳,P30-P33
- 死磕Spring系列:【死磕 Spring】----- IOC 之 加载 Bean
- 芋道源码:http://svip.iocoder.cn/Spring/IoC-load-BeanDefinitions/(死磕 Spring系列的略微修改)