Spring Bean 装配过程是一个老生常谈的话题,从最开始的通过 xml 形式装配 bean,到后来使用注解来装配 bean 以及使用 SpringBoot 和 SpringCloud 来进行开发,Spring 在整个过程中也进行了不断的演化和进步。不管是最初的 Spring 还是基于 Spring 开源的 SpringBoot、亦或是 SpringCloud,它们都是基于 Spring 的转变过来的,可能在 Spring 的基础上做了一些封装,但是本质上还是 Spring。
原本就没有那么多自动化的事情,只是 Spring 将实现的细节全部都隐藏在框架内部了。只有真正理解了 Spring,那么其实理解 SpringBoot 或者是 SpringCloud 只是一个水到渠成的事情。
起步
基于 Spring 版本 5.2.5
新建一个 maven 工程,导入如下几个 jar 包
1 | xml复制代码<dependency> |
在 classpath 路径下新建 applicationContext.xml 文件,配置一个新的 bean
1 | xml复制代码<?xml version="1.0" encoding="UTF-8" ?> |
添加完毕后,工程示意图如下
通过 XML 形式装载 Bean
一个简单且基础获取 Bean 对象的代码示例如下:
1 | java复制代码public static void main(String[] args) { |
接下来我们逐行来分析 Bean 是如何被 Spring 容器所装载并缓存的。
1. 定义资源对象
1 | java复制代码Resource resource = new ClassPathResource("applicationContext.xml"); |
将 classpath 下的 applicationContext.xml 文件转换成 Resource 文件。Resource 也是 Spring 提供的一种资源接口,除了我们示例中使用的 ClassPathResource 外,Spring 也提供了其他形式的 Resource 实现类。
进入 new ClassPathResource(“applicationContext.xml”) 构造方法,看看构造方法做了什么处理?
1 | java复制代码public ClassPathResource(String path) { |
发现该构造方法调用了自身的另一个构造方法
1 | java复制代码public ClassPathResource(String path, @Nullable ClassLoader classLoader) { |
通过该构造方法,我们可以知道该构造方法初始了一个类加载器。如果类加载器不为空,则赋值成默认的类加载器。如果类加载器为空,则通过 ClassUtils.getDefaultClassLoader() 方法获取一个默认的类加载器。而我们传入的类加载器显然为 null,则 Spring 会去自动获取默认的类加载器。
1 | java复制代码public static ClassLoader getDefaultClassLoader() { |
通过 getDefaultClassLoader 方法我们可以知道,Spring 在获取类加载器做了如下三件事:
- 获取当前线程的类加载器,如果存在,则返回。不存在则往下执行
- 获取加载当前类的类加载器,如果存在,则返回。不存在则往下执行
- 如果以上两步均没有获取到类加载器,则获取系统级类加载器/应用类加载器。
这里很好的利用了一个回退机制,用一个通俗的话语来解释回退机制就是退而求其次。先获取最合适的 xxx。如果获取不到,再获取其次合适的 xx。如果还是获取不到,就再退一步获取 x。
2. 初始化 BeanFactory
接下来我们看示例代码中的第二行代码的实现
1 | java复制代码DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory(); |
看看 new DefaultListableBeanFactory() 方法做了什么?
1 | java复制代码/** |
一个空的实现,但是调用了父类的构造方法,跟进一步,看看父类的构造方法做了什么事情。
1 | java复制代码public AbstractAutowireCapableBeanFactory() { |
同样,该构造方法也调用了父类的构造方法,跟进父类的构造方法一探究竟。
1 | java复制代码public AbstractBeanFactory() { |
空实现,没啥好看的。看看 AbstractAutowireCapableBeanFactory 里面的另外三个方法。通过方法的名称我们可以大致猜出,这是为了忽略某些特定的依赖接口。
1 | java复制代码private final Set<Class<?>> ignoredDependencyInterfaces = new HashSet<>(); |
没有太多的复杂逻辑,只是将某些特定的 class 对象放进了一个 set 集合中,标记这些接口应该被忽略,或许这个 set 集合会在后面的某一处使用到。但是注意,只有 BeanFactory 的接口应该被忽略。
3. 定义 BeanDefinitionReader 对象
接下来我们看第三行代码的实现
1 | java复制代码BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory); |
通过该行代码,我们定义了一个 Bean 定义的读取器,将第二步生成的 defaultListableBeanFactory 对象传入我们定义的读取器。
1 | java复制代码/** |
该方法是为给定的 BeanFactory 创建一个 BeanDefinitionReader。这里我们可以看到构造方法的入参类型是 BeanDefinitionRegistry 类型,为什么我们定义的 DefaultListableBeanFactory 也能传入进去?很显然我们的 DefaultListableBeanFactory 实现了该接口。我们看看 DefaultListBeanFactory 的继承图。
XMLBeanDefinitionReader 的构造方法调用了父类的构造方法,跟进去父类的构造方法看看。
1 | java复制代码protected AbstractBeanDefinitionReader(BeanDefinitionRegistry registry) { |
该构造方法做了如下的几件事情
- 给当前类的 registry 类型赋值
- 根据传入进来的参数来获取对象的 ResourceLoader
- 根据传入进来的参数来获取当前的环境
1 | csharp复制代码public PathMatchingResourcePatternResolver() { |
显然传入进来的 BeanDefinitionRegistry 不是 ResourceLoader 的实现类,这个我们从类的继承图中可以看出来。因此当前类的 ResourceLoader 为 new PathMatchingResourcePatternResolver();
,该方法获取了默认的 ResourceLoader。
1 | java复制代码public PathMatchingResourcePatternResolver() { |
前面做了这么多的准备工作,接下来开始真正从我们定义好的 applicationContext.xml 来加载 Bean 的定义。该功能通过 beanDefinitionReader.loadBeanDefinitions(resource); 来实现。
4. 从给定的资源文件中加载 BeanDefinition
看看 beanDefinitionReader.loadBeanDefinitions(resource); 是如何加载 Bean 的定义的。
Spring 对方法名称的命名比较有讲究,基本上可以做到见名之意。通过方法名我们就可以知道 loadBeanDefinitions 是加载 BeanDefinition,但是注意:这里使用了复数,说明这里加载的 BeanDefinition 可能会存在多个。
1 | java复制代码@Override |
该方法的主要功能是为了从指定的 Resource 文件中加载 BeanDefinition,该方法的返回值是返回 BeanDefinition 的数量。此处 Spring 将传入的 Resource 对象封装成了一个 EncodedResource 对象。顾名思义我们知道该对象只是对 Resource 进行了封装,其中除了包含指定的 Resource 资源外,还包含了编码信息,进入 EncodedResource 源码看看。
1 | java复制代码public EncodedResource(Resource resource) { |
可以看到,Charset 和 encoding 是互斥的属性。显然我们这里仅仅只传入了 Resource 对象,那么默认的 encoding 了 charset 均为空。
接下来看看 loadBeanDefinitions(EncodedResource e) 的具体实现。
1 | java复制代码public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { |
resourcesCurrentlyBeingLoaded 是一个 ThreadLocal 对象,首先先从 resourcesCurrentlyBeingLoaded 中获取当前的 encodedResource,如果获取出来的为空,则初始化一个 new HashSet<EncodedResource>
对象,将其放置到 resourcesCurrentlyBeingLoaded 对象中。接下来判断该对象是否在 resourcesCurrentlyBeingLoaded 中的 set 集合中已经存在,如果存在,则抛出 BeanDefinitionStoreException 异常,那么这个异常会在何时出现呢?我们可以尝试将 applicationContext.xml 进行改造一下。
1 | xml复制代码<!-- 通过 import 组件导入自身的配置文件 --> |
很显然这里面造成了一个循环引用,执行至此必然会抛出异常。
这里使用了一种巧妙的方式,通过 Set 集合不能有重复数据的特性来判断 applicationContext.xml 文件中的定义是否出现了循环导入。
接下来看看 doLoadBeanDefinitions 的具体实现
1 | java复制代码protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) |
显然 doLoadBeanDefinitions 做了两件事情
- 从给定的 Resource 资源中读取 XML 文件中的内容,该方法返回了一个 Document 对象
- 通过 Document 对象和给定的 Resource 资源中注册 bean 的定义
对于 doLoadDocument 方法的读取,实际上就是读取 XML 里面的内容,并返回一个 Document 对象。这部分就不跟源码进去看了,有兴趣可以自己搜索一下 XML 解析相关的内容。
下面看看 registerBeanDefinitions 相关的源码,看看是如何从 document 中获取到注册到 bean 的定义的。
1 | java复制代码public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { |
看看如何获取一个 Bean 定义文档读取器(BeanDefinitionDocumentReader)
1 | java复制代码private Class<? extends BeanDefinitionDocumentReader> documentReaderClass = |
这里看到了调用了 Spring 自身提供的一个 BeanUtils.instantiateClass 方法。传入了 DefaultBeanDefinitionDocumentReader 的 class 文件,稍加思考我们便可以知道该方法是通过反射的方式生成了 BeanDefinitionDocumentReader 这个对象的实例。下面去 BeanUtils.instantiateClass 源码验证一下。
1 | java复制代码public static <T> T instantiateClass(Class<T> clazz) throws BeanInstantiationException { |
看看重载的 instantiateClass 方法,一目了然,全部都是反射相关的内容。
1 | java复制代码public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException { |
注意在该方法的头部调用了 ReflectionUtils.makeAccessible(ctor); 方法,该方法即表明了即使你提供了私有的构造方法,Spring 也能帮你将对象创建出来(反射的内容),看到最后的 return,很明显 BeanUtils.instantiateClass 就是通过反射的方式生成了对象。
- 如果没有提供构造方法,则采用默认的构造方法
- 如果提供了私有的构造方法,则设置 accessible 属性为 true,再调用反射生成对象的实例
通过以上的方式可以看出,Spring 是想尽了一切办法在帮我们正常创建一个对象。
看看传入的 DefaultBeanDefinitionDocumentReader 的声明
看到这些红框中的内容,是不是感觉到非常熟悉,这不就是我们在 applicationContext.xml 中定义的一个个标签么?原来这些东西都被 DefaultBeanDefinitionDocumentReader 写死在代码中了。
接下来我们回到 registerBeanDefinitions 这个方法的实现。
我们已经知道 createBeanDefinitionDocumentReader 是通过反射的方式生成了一个 BeanDefinitionDocumentReader 对象。下面我们看看方法的第二行做了什么事情。
getRegistry().getBeanDefinitionCount(); 先看看 getRegistry() 这个方法。这个方法基本上都不用考虑,肯定是获取到了我们传入进来的 defaultListableBeanFactory 对象。
1 | java复制代码private final BeanDefinitionRegistry registry; |
返回了成员变量 registry,那么这个 registry 在哪里赋值的呢?看看我们在示例代码中的第三步 new XMLBeanDefinitionReader() 中就知道了,在该类的构造方法中,我们赋值了 registry 这个成员变量的值。
接着看看 getBeanDefinitionCount 的实现
1 | java复制代码/** Map of bean definition objects, keyed by bean name. */ |
就是返回了 beanDefinitionMap 这个 concurrentHashMap 的大小。该变量的定义为 Map<String, BeanDefinition>
类型,是一个以 bean 名称为 key,BeanDefinition 为 value 的 Map 对象。
结合上面的分析,那么 int countBefore = getRegistry().getBeanDefinitionCount(); 返回的实际上是未加载之前的 BeanDefinition 的数量。
接着看 registerBeanDefinitions 的第三行实现。documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
通过文档读取器开始从文档中注册 bean 的定义。
看看具体实现
1 | java复制代码@Override |
从 doRegisterBeanDefinitions 中的注释我们知道,
- 该方法可能会导致递归,如果我们在 applicationContext.xml 配置了引用其他
<beans>
- 该方法使用了典型的 delegate。就是我自己要做某件事,我自己不做,让其他类帮我去做。
看看方法的 preProcessXml(root),这个方法是一个空实现,
接着看看 postProcessXml(root),这个方法也是一个空实现
这是一种典型的模板方法设计模式。可以看到该方法被定义成了 protected,留作子类去实现。核心的解析逻辑在 parseBeanDefinitions 方法中。
1 | java复制代码/** |
该方法解析了文档最顶层的标签元素,例如 bean,import 等等,除了解析 Spring 规定的标签节点外,还解析了自定义的标签元素。自定义的标签我们很少用到,着重看一下解析默认的标签元素。跟到 parseDefaultElement 中看看。
1 | java复制代码private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { |
之前我们说到 doRegisterBeanDefinitions 方法会导致递归,在该方法的最后一行得到了验证。如果里面定义了 <beans>
类型的标签的话(嵌套 beans)
这里说明一下,早在我们介绍 loadBeanDefinitions 方法中,Spring 利用了一个 Set 集合来探测是否存在循环的 import 导入配置文件,如果出现了循环的 import 导入,Spring 会在 loadBeanDefinitions 中抛出异常。这种出现必然是有原因的,我们跟到 importBeanDefinitionResource 中看看 Spring 是如何处理 import 这种标签的。
1 | java复制代码protected void importBeanDefinitionResource(Element ele) { |
不管 import 标签的 resource 属性配置的是绝对路径还是相对路径,我们在代码中不难发现,两个分支中都调用了 loadBeanDefinitions 这个方法。者都会导致 Spring 在解析 import 标签的同时去判断是否 import 循环的 xml 文件引用,也从侧面验证了如果循环 import 了,Spring 将会抛出异常。
对于 alias 标签的处理我们并不关心,在实际应用中这样处理少之又少,我们这里选择跳过。直接看 processBeanDefinition 这个方法的实现。
1 | java复制代码protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { |
该方法的主要作用便是从给定的 Bean Element 标签中解析出 BeanDefinition 并将其放入到给定的 registry 中,也就是我们声明的 DefaultListableBeanFactory 中。看看 delegate.parseBeanDefinitionElement 是如何解析的。
1 | java复制代码@Nullable |
该方法做了如下几件事
- 获取 id 属性和别名属性以及 class 属性,如果没有名称,则将别名的第个元素作为 bean 的名称
- 解析 BeanDefinition,返回一个 AbstractBeanDefinition 对象
- 判断 AbstractBeanDefinition 中是否包含 bean 的名称,如果不包含,则给该 bean 生成一个 bean 的名称
- 返回包装好的一个 BeanDefinitionHolder 对象,该对象包含了 xml 中配置的 bean 的所有属性,以及 bean 的名称和别名数组。
显然重点在第二步中,如何返回一个 AbstractBeanDefinition 对象。看看 parseBeanDefinitionElement 这个方法的实现。
1 | java复制代码@Nullable |
看看如何创建一个 BeanDefinition
1 | java复制代码public static AbstractBeanDefinition createBeanDefinition( |
首先通过 new 出来了一个 GenericBeanDefinition 对象,然后根据是否存在 classLoader 对象来判断是否应该给该对象设置 class 对象或者 className 名称,最后将 GenericBeanDefinition 返回。
parseBeanDefinitionAttributes 方法源码如下:
1 | java复制代码public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, |
其实也是很简单,就是解析 bean 标签中的其他属性,分别为 createBeanDefinition 返回的 BeanDefinition 对象的属性赋值。可能有人有疑问了,我们在 bean 标签中并没有配置其他的属性,但是部分属性还是存在默认值的。
这里的属性定义其实就是跟 applicationContext.xml 中的 bean 标签是对应上的。
另外还有其他的两个方法我们要关心一下
1 | java复制代码/** |
至此,我们这里便返回了一个完整的 BeanDefinitionHolder 对象。
该 BeanDefinitionHolder 中包含了从 xml 文件中解析出来的 BeanDefinition 对象和 beanName 属性。
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder) 该行就是对我们返回的 BeanDefinitionHolder 装饰一下,也就是看看是否需要添加其他额外的属性,最后返回依然是一个 BeanDefinitionHolder 对象。
最后重头戏来了,通过了 BeanDefinitionReaderUtils 的 registerBeanDefinition 方法向 registry 中注册了一个 BeanDefinitionHolder 对象,看看是如何注册的。
1 | java复制代码public static void registerBeanDefinition( |
重点在于我们调用了 registry 的 registerBeanDefinition 方法,registerBeanDefinition 有多个实现,而显然我们应该查看 DefaultListableBeanFactory 的实现。
1 | java复制代码@Override |
到这里,我们所有的在 xml 中定义的 bean 对象都已经被解析出来了,所有的 bean 都被存放在 registry 中的 beanDefinitionMap 中,它是一个 concurrentHashMap,它的 key 是 beanName,value 是关于该 bean 的全部定义,其中包含 className/class, scope, init-method … 等等属性。至此整个 bean 的加载过程也就结束了。但是注意:此时我们的 bean 并没有被创建。那么该 bean 是在什么时候被创建的呢?
Bean 的创建过程
通过上面的过程,我们可以知道以上的三行代码 Spring 从 applicationContext.xml 文件中加载了 bean 的定义,并存放到了 beanDefinitionMap 中,此时我们的 bean 对象并没有被初始化。
下面来看看 defaultListableBeanFactory.getBean 方法。看看是如何实现的。点进去我们发现 Spring 的 BeanFactory 为我们提供了各种各样的 getBean 方法。但是他们的本质都是调用了 doGetBean 方法。我们直接去看 doGetBean 方法做了什么事情。
看看 doGetBean 的源码实现 (doGetBean 的源码非常多,因为源码太多的原因,这里删除了一些无用的日志逻辑)
1 | java复制代码protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { |
以上的代码做了如下几件事情
1.根据 beanName 去单例的缓存中检查是否已经存在该 Bean 对象
那么检查是如何进行的呢?通过 getSingleton 可以一窥究竟。
1 | java复制代码// 传入的 allowEarlyReference 为 true |
可以看到从当前的 singletonObjects 对象中获取了 singletonObject 对象,singletonObject 对象为一个 ConcurrentHashMap 对象,用来缓存 SpringIOC 容器初始化过后的 bean。并且只会缓存 scope 属性为单例的 bean,prototype 属性的 bean 不会缓存。
如果缓存对象为空,并且当前对象处于正在创建的时候,就开始处理循环引用的问题。如果当前缓存的对象不为空,那么直接返回当前缓存的 singletonObject;
这里涉及到一个循环引用的问题,后面单开文章来进行讲解。此处我们主要分析 bean 的创建过程。
显然我们这里是第一次获取,所以 singletonObjects 这个 ConcurrentHashMap 中并不存在该对象的实例。
本文转载自: 掘金