图文并茂,揭秘 Spring 的 Bean 的加载过程
目录
Spring 作为 Ioc 框架,实现了依赖注入,由一个中心化的 Bean 工厂来负责各个 Bean 的实例化和依赖管理。各个 Bean 可以不需要关心各自的复杂的创建过程,达到了很好的解耦效果。
我们对 Spring 的工作流进行一个粗略的概括,主要为两大环节:
我们假设所有的配置和扩展类都已经装载到了 ApplicationContext 中,然后具体的分析一下 Bean 的加载流程。
思考一个问题,抛开 Spring 框架的实现,假设我们手头上已经有一套完整的 Bean Definition Map,然后指定一个 beanName 要进行实例化,需要关心什么?即使我们没有 Spring 框架,也需要了解这两方面的知识:
Spring 进行了抽象和封装,使得作用域和依赖关系的配置对开发者透明,我们只需要知道当初在配置里已经明确指定了它的生命周期和依赖了谁,至于是怎么实现的,依赖如何注入,托付给了 Spring 工厂来管理。
Spring 只暴露了很简单的接口给调用者,比如 getBean :
那我们就从 getBean 方法作为入口,去理解 Spring 加载的流程是怎样的,以及内部对创建信息、作用域、依赖关系等等的处理细节。
上面是跟踪了 getBean 的调用链创建的流程图,为了能够很好地理解 Bean 加载流程,省略一些异常、日志和分支处理和一些特殊条件的判断。
从上面的流程图中,可以看到一个 Bean 加载会经历这么几个阶段(用绿色标记):
整个流程最为复杂的是对循环依赖的解决方案,后续会进行重点分析。
而在我们解析完配置后创建的 Map,使用的是 beanName 作为 key。见 DefaultListableBeanFactory:
BeanFactory.getBean 中传入的 name,有可能是这几种情况:
为了能够获取到正确的 BeanDefinition,需要先对 name 做一个转换,得到 beanName。
见 AbstractBeanFactory.doGetBean :
如果是 alias name ,在解析阶段,alias name 和 bean name 的映射关系被注册到 SimpleAliasRegistry 中。从该注册器中取到 beanName。见 SimpleAliasRegistry.canonicalName :
如果是 factorybean name ,表示这是个工厂 bean,有携带前缀修饰符 的,直接把前缀去掉。见 BeanFactoryUtils.transformedBeanName :
我们从配置文件读取到的 BeanDefinition 是 GenericBeanDefinition 。它的记录了一些当前类声明的属性或构造参数,但是对于父类只用了一个 parentName 来记录。
接下来会发现一个问题,在后续实例化 Bean 的时候,使用的 BeanDefinition 是 RootBeanDefinition 类型而非 GenericBeanDefinition 。这是为什么?
答案很明显,GenericBeanDefinition 在有继承关系的情况下,定义的信息不足:
为了能够正确初始化对象,需要完整的信息才行 。需要递归 合并父类的定义 :
见 AbstractBeanFactory.doGetBean :
在判断 parentName 存在的情况下,说明存在父类定义,启动合并。如果父类还有父类怎么办?递归调用,继续合并。
见 AbstractBeanFactory.getMergedBeanDefinition 方法:
每次合并完父类定义后,都会调用 RootBeanDefinition.overrideFrom 对父类的定义进行覆盖,获取到当前类能够正确实例化的 全量信息 。
什么是循环依赖?
举个例子,这里有三个类 A、B、C,然后 A 关联 B,B 关联 C,C 又关联 A,这就形成了一个循环依赖。如果是方法调用是不算循环依赖的,循环依赖必须要持有引用。
循环依赖根据注入的时机分成两种类型:
如果是构造器循环依赖,本质上是无法解决的 。比如我们准调用 A 的构造器,发现依赖 B,于是去调用 B 的构造器进行实例化,发现又依赖 C,于是调用 C 的构造器去初始化,结果依赖 A,整个形成一个死结,导致 A 无法创建。
如果是设值循环依赖,Spring 框架只支持单例下的设值循环依赖 。Spring 通过对还在创建过程中的单例,缓存并提前暴露该单例,使得其他实例可以引用该依赖。
Spring 不支持原型模式的任何循环依赖 。检测到循环依赖会直接抛出 BeanCurrentlyInCreationException 异常。
使用了一个 ThreadLocal 变量 prototypesCurrentlyInCreation 来记录当前线程正在创建中的 Bean 对象,见 AbtractBeanFactory#prototypesCurrentlyInCreation :
在 Bean 创建前进行记录,在 Bean 创建后删除记录。见 AbstractBeanFactory.doGetBean :
见 AbtractBeanFactory.beforePrototypeCreation 的记录操作:
见 AbtractBeanFactory.beforePrototypeCreation 的删除操作:
为了节省内存空间,在单个元素时 prototypesCurrentlyInCreation 只记录 String 对象,在多个依赖元素后改用 Set 集合。这里是 Spring 使用的一个节约内存的小技巧。
了解了记录的写入和删除过程好了,再来看看读取以及判断循环的方式。这里要分两种情况讨论。
这两个地方的实现略有不同。
如果是构造函数依赖的,比如 A 的构造函数依赖了 B,会有这样的情况。实例化 A 的阶段中,匹配到要使用的构造函数,发现构造函数有参数 B,会使用 BeanDefinitionValueResolver 来检索 B 的实例。见 BeanDefinitionValueResolver.resolveReference :
我们发现这里继续调用 beanFactory.getBean 去加载 B。
如果是设值循环依赖的的,比如我们这里不提供构造函数,并且使用了 @Autowire 的方式注解依赖(还有其他方式不举例了):
加载过程中,找到无参数构造函数,不需要检索构造参数的引用,实例化成功。接着执行下去,进入到属性填充阶段 AbtractBeanFactory.populateBean ,在这里会进行 B 的依赖注入。
为了能够获取到 B 的实例化后的引用,最终会通过检索类 DependencyDescriptor 中去把依赖读取出来,见 DependencyDescriptor.resolveCandidate :
发现 beanFactory.getBean 方法又被调用到了。
在这里,两种循环依赖达成了同一 。无论是构造函数的循环依赖还是设置循环依赖,在需要注入依赖的对象时,会继续调用 beanFactory.getBean 去加载对象,形成一个递归操作。
而每次调用 beanFactory.getBean 进行实例化前后,都使用了 prototypesCurrentlyInCreation 这个变量做记录。按照这里的思路走,整体效果等同于 建立依赖对象的构造链 。
prototypesCurrentlyInCreation 中的值的变化如下:
调用判定的地方在 AbstractBeanFactory.doGetBean 中,所有对象的实例化均会从这里启动。
判定的实现方法为 AbstractBeanFactory.isPrototypeCurrentlyInCreation :
所以在原型模式下,构造函数循环依赖和设值循环依赖,本质上使用同一种方式检测出来。Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。
Spring 也不支持单例模式的构造循环依赖 。检测到构造循环依赖也会抛出 BeanCurrentlyInCreationException 异常。
和原型模式相似,单例模式也用了一个数据结构来记录正在创建中的 beanName。见 DefaultSingletonBeanRegistry :
会在创建前进行记录,创建化后删除记录。
见 DefaultSingletonBeanRegistry.getSingleton
记录和判定的方式见 DefaultSingletonBeanRegistry.beforeSingletonCreation :
这里会尝试往 singletonsCurrentlyInCreation 记录当前实例化的 bean。我们知道 singletonsCurrentlyInCreation 的数据结构是 Set,是不允许重复元素的, 所以一旦前面记录了,这里的 add 操作将会返回失败 。
比如加载 A 的单例,和原型模式类似,单例模式也会调用匹配到要使用的构造函数,发现构造函数有参数 B,然后使用 BeanDefinitionValueResolver 来检索 B 的实例,根据上面的分析,继续调用 beanFactory.getBean 方法。
所以拿 A,B,C 的例子来举例 singletonsCurrentlyInCreation 的变化,这里可以看到和原型模式的循环依赖判断方式的算法是一样:
单例模式下,构造函数的循环依赖无法解决,但设值循环依赖是可以解决的 。
这里有一个重要的设计: 提前暴露创建中的单例 。
我们理解一下为什么要这么做。
还是拿上面的 A、B、C 的的设值依赖做分析,
= 1. A 创建 - A 构造完成,开始注入属性,发现依赖 B,启动 B 的实例化
= 2. B 创建 - B 构造完成,开始注入属性,发现依赖 C,启动 C 的实例化
= 3. C 创建 - C 构造完成,开始注入属性,发现依赖 A
重点来了,在我们的阶段 1中, A 已经构造完成,Bean 对象在堆中也分配好内存了,即使后续往 A 中填充属性(比如填充依赖的 B 对象),也不会修改到 A 的引用地址。
所以,这个时候是否可以 提前拿 A 实例的引用来先注入到 C ,去完成 C 的实例化,于是流程变成这样。
= 3. C 创建 - C 构造完成,开始注入依赖,发现依赖 A,发现 A 已经构造完成,直接引用,完成 C 的实例化。
= 4. C 完成实例化后,B 注入 C 也完成实例化,A 注入 B 也完成实例化。
这就是 Spring 解决单例模式设值循环依赖应用的技巧。流程图为:
为了能够实现单例的提前暴露。Spring 使用了三级缓存,见 DefaultSingletonBeanRegistry :
这三个缓存的区别如下:
从 getBean("a") 开始,添加的 SingletonFactory 具体实现如下:
可以看到如果使用该 SingletonFactory 获取实例,使用的是 getEarlyBeanReference 方法,返回一个未初始化的引用。
读取缓存的地方见 DefaultSingletonBeanRegistry :
先尝试从 singletonObjects 和 singletonFactory 读取,没有数据,然后尝试 singletonFactories 读取 singletonFactory,执行 getEarlyBeanReference 获取到引用后,存储到 earlySingletonObjects 中。
这个 earlySingletonObjects 的好处是,如果此时又有其他地方尝试获取未初始化的单例,可以从 earlySingletonObjects 直接取出而不需要再调用 getEarlyBeanReference 。
从流程图上看,实际上注入 C 的 A 实例,还在填充属性阶段,并没有完全地初始化。等递归回溯回去,A 顺利拿到依赖 B,才会真实地完成 A 的加载。
获取到完整的 RootBeanDefintion 后,就可以拿这份定义信息来实例具体的 Bean。
具体实例创建见 AbstractAutowireCapableBeanFactory.createBeanInstance ,返回 Bean 的包装类 BeanWrapper,一共有三种策略:
使用工厂方法创建,会先使用 getBean 获取工厂类,然后通过参数找到匹配的工厂方法,调用实例化方法实现实例化,具体见 ConstructorResolver.instantiateUsingFactoryMethod :
使用有参构造函数创建,整个过程比较复杂,涉及到参数和构造器的匹配。为了找到匹配的构造器,Spring 花了大量的工作,见 ConstructorResolver.autowireConstructor :
使用无参构造函数创建是最简单的方式,见 AbstractAutowireCapableBeanFactory.instantiateBean :
我们发现这三个实例化方式,最后都会走 getInstantiationStrategy().instantiate(...) ,见实现类 SimpleInstantiationStrategy.instantiate :
虽然拿到了构造函数,并没有立即实例化。因为用户使用了 replace 和 lookup 的配置方法,用到了动态代理加入对应的逻辑。如果没有的话,直接使用反射来创建实例。
创建实例后,就可以开始注入属性和初始化等操作。
但这里的 Bean 还不是最终的 Bean。返回给调用方使用时,如果是 FactoryBean 的话需要使用 getObject 方法来创建实例。见 AbstractBeanFactory.getObjectFromBeanInstance ,会执行到 doGetObjectFromFactoryBean :
实例创建完后开始进行属性的注入,如果涉及到外部依赖的实例,会自动检索并关联到该当前实例。
Ioc 思想体现出来了。正是有了这一步操作,Spring 降低了各个类之间的耦合。
属性填充的入口方法在 AbstractAutowireCapableBeanFactory.populateBean 。
可以看到主要的处理环节有:
如果我们的 Bean 需要容器的一些资源该怎么办?比如需要获取到 BeanFactory、ApplicationContext 等等。
Spring 提供了 Aware 系列接口来解决这个问题。比如有这样的 Aware:
Spring 在初始化阶段,如果判断 Bean 实现了这几个接口之一,就会往 Bean 中注入它关心的资源。
见 AbstractAutowireCapableBeanFactory.invokeAwareMethos :
在 Bean 的初始化前或者初始化后,我们如果需要进行一些增强操作怎么办?
这些增强操作比如打日志、做校验、属性修改、耗时检测等等。Spring 框架提供了 BeanPostProcessor 来达成这个目标。比如我们使用注解 @Autowire 来声明依赖,就是使用 AutowiredAnnotationBeanPostProcessor 来实现依赖的查询和注入的。接口定义如下:
实现该接口的 Bean 都会被 Spring 注册到 beanPostProcessors 中, 见 AbstractBeanFactory :
只要 Bean 实现了 BeanPostProcessor 接口,加载的时候会被 Spring 自动识别这些 Bean,自动注册,非常方便。
然后在 Bean 实例化前后,Spring 会去调用我们已经注册的 beanPostProcessors 把处理器都执行一遍。
这里使用了责任链模式,Bean 会在处理器链中进行传递和处理。当我们调用 BeanFactory.getBean 的后,执行到 Bean 的初始化方法 AbstractAutowireCapableBeanFactory.initializeBean 会启动这些处理器。
自定义初始化有两种方式可以选择:
见 AbstractAutowireCapableBeanFactory.invokeInitMethods :
Bean 已经加载完毕,属性也填充好了,初始化也完成了。
在返回给调用者之前,还留有一个机会对 Bean 实例进行类型的转换。见 AbstractBeanFactory.doGetBean :
抛开一些细节处理和扩展功能,一个 Bean 的创建过程无非是:
获取完整定义 - 实例化 - 依赖注入 - 初始化 - 类型转换。
作为一个完善的框架,Spring 需要考虑到各种可能性,还需要考虑到接入的扩展性。
所以有了复杂的循环依赖的解决,复杂的有参数构造器的匹配过程,有了 BeanPostProcessor 来对实例化或初始化的 Bean 进行扩展修改。
先有个整体设计的思维,再逐步击破针对这些特殊场景的设计,整个 Bean 加载流程迎刃而解。
Spring Boot是如何扫描和加载bean的
从SpringApplication#refreshContext方法开始追踪,找到
AbstractApplicationContext#invokeBeanFactoryPostProcessors方法,继续深入找到ConfigurationClassParser#doProcessConfigurationClass方法,就是从这里开始,根据ComponentScan配置的路径去加载需要spring管理的类。下面详细讲讲加载过程:
第一步,根据ComponentScan配置的路径去查找所有class文件,具体的方法在PathMatchingResourcePatternResolver#doRetrieveMatchingFiles,目的是扫描路径下每一个目录里的class文件。
第二步,过滤出使用了@Component注解的类,具体的方法在ClassPathScanningCandidateComponentProvider#scanCandidateComponents。
第三步,将第二步筛选出来的注册到bean工厂的注册表缓存中,具体的方法在ClassPathBeanDefinitionScanner#doScan
第四步,加载类里面的@Bean注解信息,并存放在配置缓存configurationClasses中,具体的方法在ConfigurationClassParser#processConfigurationClass
第五步,将第四步加载的@Bean信息注册到bean工厂的注册表缓存里,具体方法在
ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod
第六步,在refreshContext方法里有个finishBeanFactoryInitialization方法,在里面找到DefaultListableBeanFactory#preInstantiateSingletons方法,这里bean工厂将所有注册的bean都生成了实例。
bean工厂生成bean实例的步骤可以参考下面的文章
Spring Bean的生命周期:
springboot怎样加载bean
Spring文件进行别配置其servlet-name没指定init-param属性系统自寻找spring配置文件[servlet-name]-servlet.xml 需要载入spring相关配置文件首先加载ContextLoaderListener类再指定context-param指定spring配置文件使用逗号别隔各文件使用便配置...
spring容器已经启动,我怎么动态的加载里面的某个bean
[引文]通常在struts2+spring的例子上,要想使用spring的Bean,可以在applicationContext.xml加上如下配置
bean id="springBean" scope="prototype" class=""
property name="name" value="chen"//beanbean id="myAction" scope="prototype" class=""
property name="springBean" ref="springBean"//bean如果是j2ee应用,启动web应用时将会自动加载ApplicationContext实例(Spring容器负责创建Bean实例)
一旦struts2的myAction实例化,其中的SpringBean也会被自动注入进来,从而达到使用SpringBean的目的。
[问题]但是仍有需要通过代码来调用SpringBean的情况:
1)对于不是由spring创建管理的类,如在java 代码中直接使用new去创建一个对象,并且想在这个对象中使用
SpringBean;因为这个对象并不是由Spring容器创建管理的类,所以即使它有setter方法,容器的springBean也不会被注入。
2)动态更改springBean中的属性值,如在代码运行时,name值需要发生变动;
3)对于一个独立的应用程序[解决]定义一个非Spring容器创建管理的类
public class NonSpringClass implements ServletContextAware {
private SpringBean springBean;
//如果 testGetBean不是被Spring容器创建管理,即使它有setter方法,容器的springBean也不会被注入。
public void setSpringBean(SpringBean springBean){this.springBean=springBean;}//利用ApplicationContext 从spring容器中获得springBean;
//Spring有两个核心接口BeanFactory和ApplicationContext,其中ApplicationContext是BeanFactory的子接口,
//它们代表了Spring容器,Spring容器是产生Bean的工厂,用于管理容器中的Bean。
public NonSpringClass (){//ApplicationContext acx = new ClassPathXmlApplicationContext("applicationContext.xml");
ApplicationContext acx = new FileSystemXmlApplicationContext("src/WEB-INF/applicationContext.xml");
springBean=(SpringBean)acx.getBean("springBean");}}调用这个类的代码:
import com.NonSpringClass;
public class TestCode {
private NonSpringClass nonSpringClass;
//这样nonSpringClass里将包含Spring容器创建的springBean}}
Spring 注入 Bean 的 7 种方式,还有谁不会?
我们谈到Spring的时候一定会提到IOC容器、DI依赖注入,Spring通过将一个个类标注为Bean的方法注入到IOC容器中,达到了控制反转的效果。那么我们刚开始接触Bean的时候,一定是使用xml文件,一个一个的注入,就例如下面这样。
我们的项目一般很大的话,就需要成千上百个Bean去使用,这样写起来就很繁琐。那么Spring就帮我们实现了一种通过注解来实现注入的方法。只需要在你需要注入的类前面加上相应的注解,Spring就会帮助我们扫描到他们去实现注入。
xml扫描包的方式
一般情况下,注入Bean有一个最直白,最易懂的方式去实现注入,下面废话先不多说,先贴代码。
另外,Spring 系列面试题和答案全部整理好了,微信搜索Java面试库小程序,可以在线刷题。
Bean类
Configuration类
Test类
与xml有一点不同,这里在Test中,实例化的不再是 ClassPathXmlApplicationContext ,而是获取的 AnnotationConfigApplicationContext 实例。
上面的代码中MyBean也就是我们需要Spring去管理的一个Bean,他只是一个简单的类。而MyConfiguration中,我们首先用 @Configuration 注解去标记了该类,这样标明该类是一个Spring的一个配置类,在加载配置的时候会去加载他。
在MyConfiguration中我们可以看到有一个方法返回的是一个MyBean的实例,并且该方法上标注着 @Bean 的注解,标明这是一个注入Bean的方法,会将下面的返回的Bean注入IOC。
推荐一个 Spring Boot 基础教程及实战示例:
我们在生成一个Bean实例的时候,可以使用Bean的构造方法将Bean实现注入。直接看代码
Bean类
AnotherBean类
Configuration类
这里我们可以发现,和一般方式注入的代码不一样了,我们来看看新的注解都是什么意思:
@AutoWired
简单粗暴,直接翻译过来的意思就是自动装配:wrench:,还不理解为什么叫自动装配:wrench:?看了下一个注解的解释你就知道了。若是在这里注入的时候指定一个Bean的id就要使用 @Qualifier 注解。
@Component(默认单例模式)
什么??这翻译过来是零件,怎么感觉像是修 汽车 ??是的,Spring管理Bean的方法就是修 汽车 的方式。我们在需要将一个类变成一个Bean被Spring可以注入的时候加上注解零件 @Conmonent ,那么我们就可以在加载Bean的时候把他像零件一样装配:wrench:到这个IOC 汽车 上了
在这里我们还有几个其他的注解也可以实现这个功能,也就是细化的 @Component :
@ComponentScan("")
还是翻译,零件扫描,我们去看看括号里的“零件仓库”里面,哪些“零件”(类)需要被装载,Spring就会去扫描这个包,将里面所有标注了 @Component 的类进行注入。
这里的通过构造方法进行注入就很好理解了,我们在装配MyBean这个零件的时候,突然发现他必须在AnotherBean的基础上才能安装到IOC里面,那么我们就在每次装配MyBean的时候自动装配:wrench:一个AnotherBean进去。举个:chestnut:吧:
还是以 汽车 为例,我们在踩油门出发之前,是不是必须发车??这里的AutoWired的内容就像发车,你不发车,这个油门你踩断都没有用,他都不会走。
我们可以在一个属性的set方法中去将Bean实现注入,看代码吧
MyBean类
Configuration类 和 Test类
同上一个,就不贴了
这里我们发现在setter方法上我们有一个 @AutoWired ,与上面不同的是,我们不会在实例化该类时就自动装配:wrench:这个对象,而是在显式调用setter的时候去装配。
我们前面两种注入的方式诸如时间不同,并且代码较多,若是通过属性,即就是
这里我们可以看到我们这个类中需要使用AnotherBean这个实例对象,我们可以通过@AutoWired去自动装配它。
MyBeanList类
MyConfiguration类
这里我们将MyBeanList进行了注入,对List中的元素会逐一注入。
MyConfiguration类
注入与List中泛型一样的类型,会自动去匹配类型,及时这里没有任何List的感觉,只是String的类型,但他会去通过List的Bean的方式去注入。
同样这里也具有两种方式去注入Map类型Bean,且第二种的优先值高于第一种
以上就是Bean通过注解注入的几种方式,大家可以对比着xml注入的方式去看。
Spring 标签加载bean的原理是怎样的
Spring管理bean:
1.从web.xml里面配置的ContextLoaderListener开始。
2.ContextLoaderListener继承ContextLoader
3.执行ContextLoaderListener的contextInitialized方法,获得servletContext.
4.把servletContext传入ContextLoader的initWebApplicationContext(ServletContext servletContext)方法并执行。
5.通过ContextLoader.createWebApplicationContext(servletContext)获得this.context(WebApplicationContext)实例。
默认情况下这个context就是XmlWebApplicationContext。
这个XmlWebApplicationContext继承一个AbstractApplicationContext类。
再执行ContextLoader.configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)方法,
执行AbstractApplicationContext的.refresh();方法
[java] view plain copy
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
logger.warn("Exception encountered during context initialization - cancelling refresh attempt", ex);
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
}
这个方法完成了WebApplicationContext里面的beanfactory的初始化和bean载入,beanfactorypostprocessor的调用,beanpostprocessor的注册,ApplicationEvent的监听和注册,non-lazy-init的bean的初始化。
换言之,已经把该准备的都准备好了,只需要有请求来获取bean,就根据情况或返回已经初始化的bean或进行bean的Instantiation 和 Initialization。