写点什么

带着问题去分析:Spring Bean 生命周期 | 京东物流技术团队

  • 2023-10-26
    北京
  • 本文字数:5909 字

    阅读完需:约 19 分钟

带着问题去分析:Spring Bean 生命周期 | 京东物流技术团队

1: Bean 在 Spring 容器中是如何存储和定义的

Bean 在 Spring 中的定义是_org.springframework.beans.factory.config.BeanDefinition_接口,BeanDefinition 里面存储的就是我们编写的 Java 类在 Spring 中的元数据,包括了以下主要的元数据信息:


1:Scope(Bean 类型):包括了单例 Bean(Singleton)和多实例 Bean(Prototype)


2:BeanClass: Bean 的 Class 类型


3:LazyInit:Bean 是否需要延迟加载


4:AutowireMode:自动注入类型


5:DependsOn:Bean 所依赖的其他 Bean 的名称,Spring 会先初始化依赖的 Bean


6:PropertyValues:Bean 的成员变量属性值


7:InitMethodName:Bean 的初始化方法名称


8:DestroyMethodName:Bean 的销毁方法名称


同时 BeanDefinition 是存储到_org.springframework.beans.factory.support.DefaultListableBeanFactory 类中维护的 BeanDefinitionMap_中的,源码如下:


Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
复制代码


了解了 BeanDefinition 的基础信息和存储位置后,接下来看看创建好的 Bean 实例是存储在什么地方的,创建好的 Bean 是存储在:_org.springframework.beans.factory.support.DefaultSingletonBeanRegistry_类中的


_singletonObjects_中的,Key 是 Bean 的名称,Value 就是创建好的 Bean 实例:


Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
复制代码


了解了基本信息之后,就可以带着下面两个关键问题去分析 Spring Bean 的生命周期了:


1:Java 类是如何被 Spring 扫描从而变成 BeanDefinition 的?


2:BeanDefinition 是如何被 Spring 加工创建成我们可以直接使用的 Bean 实例的?

2:Java 类是如何被 Spring 扫描成为 BeanDefinition 的?

在 Spring 中定义 Bean 的方式有非常多,例如使用 XML 文件、注解,包括:@Component@Service@Configuration等,下面就以 @Component 注解为例来探究 Spring 是如何扫描我们的 Bean 的。我们知道使用@Component注解来标记 Bean 是需要配合 @ComponentScan 注解来使用的,而我们的主启动类上标注的 @SpringBootApplication 注解中就默认继承了 @ComponentScan 注解


@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication
复制代码


所以最初的问题就转化成了**@ComponentScan**注解是如何在 Spring 中运作的

2.1 @ComponentScan 注解是如何运作的

在 Spring 框架中,这个注解对应的处理类是_ComponentScanAnnotationParser,这个类的 parse_方法是主要的处理逻辑,这个方法简要处理逻辑如下:


1:获取 @ComponentScan 注解中的 basePackage 属性,若没有则默认为该注解所标注类所在的包路径


2:使用 ClassPathBeanDefinitionScanner scanCandidateComponents 方法扫描 classpath:+basePackage+/*.class**下的所有类资源文件


3:最后循环判断扫描的所有类资源文件,判断是否包含 @Component 注解,若有则将这些类注册到 beandefinitionMap 中


自此,我们代码里写的 Java 类,就被 Spring 扫描成 BeanDefinition 存储到了 BeanDefinitionMap 中了,扫描的细节大家可以去看看这个类的源码

3:Spring 如何创建我们的 Bean 实例的

Spring 把我们编写的 Java 类扫描成 BeanDefinition 之后,就会开始创建我们的 Bean 实例了,Spring 将创建 Bean 的方法交给了_org.springframework.beans.factory.support.AbstractBeanFactory#getBean_方法,接下来就来看看 getBean 方法是如何创建 Bean 的


getBean 方法的调用逻辑如下:getBean--> doGetBean --> createBean --> doCreateBean,最终 Spring 会使用 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean 方法来创建 Bean,创建 Bean 实例的主要逻辑分为了四个部分:创建 Bean 实例,填充 Bean 属性,初始化 Bean,销毁 Bean,接下来我们分别对这个四个部分进行探究

3.1 创建 Bean 实例

创建 Bean 实例的方法入口如下:


org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#__createBeanInstance


if (instanceWrapper == null) {    instanceWrapper = createBeanInstance(beanName, mbd, args);}
复制代码


这个方法的主要逻辑是:推断出创建该 Bean 的构造器方法和参数,然后使用 Java 反射去创建 Bean 实例

3.2 填充 Bean 属性值 populateBean 方法

在这个方法中,主要是解析 Bean 需要注入的成员属性,然后将这些属性注入到该 Bean 中,如果该 Bean 有依赖的其他 Bean 则会优先去创建依赖的 Bean,然后返回来继续创建该 Bean,注意这里就会产生 Bean 创建的循环依赖问题,在本文的第 6 节中会详细说明

3.4:初始化 Bean(initializeBean 方法)

初始化 Bean 主要包括了四个部分:

3.4.1:invokeAwareMethods

在这个方法中主要调用实现的 Aware 接口中的方法,包括了 BeanNameAware.setBeanName,BeanClassLoaderAware.setBeanClassLoader,BeanFactoryAware.setBeanFactory,这三个方法



Aware 接口的功能:通过调用 Aware 接口中的 set 方法,将 Spring 容器中对应的 Bean 注入到正在创建的 Bean 中

3.4.2:调用前置处理方法:applyBeanPostProcessorsBeforeInitialization

在这个方法中主要是获取 Spring 容器中所有实现了_org.springframework.beans.factory.config.BeanPostProcessor 接口的的实现类,然后循环调用 postProcessBeforeInitialization_方法来加工正在创建的 Bean



所以在这个方法中我们可以自定义_BeanPostProcessor_来扩展 Bean 的功能,实现自己的加工逻辑

3.4.3:调用 Bean 相关的初始化方法:

3.4.3.1 如果是 InitializingBean 则调用 afterPropertiesSet 方法



在这个流程中,Spring 框架会判断正在创建的 Bean 是否实现了 InitializingBean 接口,如果实现了就会调用_afterPropertiesSet_方法来执行代码逻辑。


3.4.3.2 调用自定义初始化方法:initMethod


在这个流程中主要调用我们自定义的初始化方法,例如在 xml 文件中配置的_init-method 和 destory-method 或者使用注解配置的 @Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")_ 方法



3.4.3.3:调用后置处理方法:applyBeanPostProcessorsAfterInitialization


在这个方法中主要是获取 Spring 容器中所有实现了_org.springframework.beans.factory.config.BeanPostProcessor 接口的的实现类,然后循环调用 postProcessAfterInitialization_来加工正在创建的 Bean



在这个方法中我们可以自定义_BeanPostProcessor_来扩展 Bean 的功能,实现自己的加工逻辑

4:注册 Bean 销毁方法

在这里主要是注册 Bean 销毁时 Spring 回掉的方法例如:


1:xml 文件中配置的 destroy-method 方法或者_@Bean 注解中配置的 destroyMethod_方法


2:_org.springframework.beans.factory.DisposableBean 接口中的 destory_方法

5:总结

到这里,从我们编写的 Java 类到 Spring 容器中可使用的 Bean 实例的创建过程就完整的梳理完成了,了解 Bean 的创建过程能够使我们更加熟悉 Bean 的使用方法,同时我们也可以在创建 Bean 的过程中新增自己的处理逻辑,从而实现将自己的组件接入 Spring 框架

6:Spring 循环依赖的解决方法

Spring 在创建 Bean 实例的时候,有时避免不了我们编写的 Java 类存在互相依赖的情况,如果 Spring 对这种互相依赖的情况不做处理,那么就会产生创建 Bean 实例的死循环问题,所以 Spring 对于这种情况必须特殊处理,下面就来探究 Spring 是如何巧妙处理 Bean 之间的循环依赖问题

6.1 暴露钩子方法 getEarlyBeanReference

首先对于单实例类型的 Bean 来说,Spring 在创建 Bean 的时候,会提前暴露一个钩子方法来获取这个正在创建中的 Bean 的地址引用,其代码如下:




如上面的代码所示,此时会在_singletonFactories 这个 Map 中提前储存这个钩子方法 singletonFactory_,从而能够提前对外暴露这个 Bean 的地址引用,那么为什么获取地址引用需要包装成复杂的方法呢?下面会解释

6.2 其他 Bean 获取提前暴露的 Bean 的地址引用

当其他 Bean 需要依赖正在创建中的 Bean 的时候,就会调用 getSingleton 方法来获取需要的 Bean 的地址引用



如上诉代码所示,在获取 Bean 的时候会从三个地方来获取


1:singletonObjects :这个是存放已经完全创建完成的 Bean 实例的 Map


2:earlySingletonObjects :这个是存放用提前暴露的钩子方法创建好的 Bean 实例的 Map


3:singletonFactories :这个是用来存放钩子方法的 Map


当获取依赖的 Bean 的时候,就会调用钩子方法 getEarlyBeanReference 来获取提前暴露的 Bean 的引用,这个方法的源码如下:


protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {    Object exposedObject = bean;    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {        for (BeanPostProcessor bp : getBeanPostProcessors()) {            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);            }        }      }    return exposedObject;}
复制代码


如上面的源码所示,这个方法主要是需要调用 SmartInstantiationAwareBeanPostProcessor getEarlyBeanReference 方法来提前处理一下尚未创建完成的 Bean,而 getEarlyBeanReference 方法有逻辑的实现类只有一个**org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator,**这个类就是创建 Aop 代理的类,其代码如下:


public Object getEarlyBeanReference(Object bean, String beanName) {    Object cacheKey = this.getCacheKey(bean.getClass(), beanName);    //提前标记这个bean已经创建过代理对象了    this.earlyProxyReferences.put(cacheKey, bean);    //按条件创建代理对象    return this.wrapIfNecessary(bean, beanName, cacheKey);}
复制代码


如上面的代码所示,这段代码的主要目标就是判断提前暴露的 Bean 是否需要做动态代理,需要的话就会返回提前暴露的 Bean 的动态代理对象


那么这里为什么要去判断是否需要动态代理呢?考虑下面这种情况


1:如果这里不返回这个 Bean 的动态代理对象,但是这个 Bean 在后续的初始化流程中会存在动态代理:


举例:这里假设 A 依赖 B,B 又依赖 A,此时 B 正在获取 A 提前暴露的引用,如果这时将 A 本身的地址引用返回给 B,那么 B 里面就会保存 A 原始的地址引用,当 B 创建完成后,程序返回去创建 A 时,结果 A 在初始化的流程(initializingBean)中发生了动态代理,那么这时 Spring 容器中实际使用的是 A 的动态代理对象,而 B 却持有了原始 A 的引用,那么这时容器中就会存在 A 原始的引用以及 A 的动态代理的引用,从而产生歧义,这就是为什么需要提前去判断是否需要创建动态代理的原因,__这个原因的问题在于填充属性 populateBean 流程在初始化流程(initializingBean)之前,而创建动态代理的过程在初始化流程中

6.3 判断 Bean 的地址是否发生变化

Spring 在 Bean 初始化之后,又判断了一下 Bean 初始化之后的地址是否发生了变化,其代码逻辑如下所示:


if (earlySingletonExposure) {    Object earlySingletonReference = getSingleton(beanName, false);    //判断是否触发了提前创建bean的逻辑(getEarlyBeanReference)    //如果有其他bean触发了提前创建bean的逻辑,那么这里就不为null    if (earlySingletonReference != null) {        //判断引用地址是否发生了变化        if (exposedObject == bean) {            exposedObject = earlySingletonReference;  }    }}
复制代码


那么这里为什么需要在初始化之后继续判断 Bean 的地址是否发生了变化呢?


这是因为,如果存在循环依赖,同时 Bean 在初始化的流程(initializingBean)中又发生了额外的动态代理,例如,除了在**getEarlyBeanReference 中发生的动态代理之外,还有额外的动态代理发生了,也就是发生了两次动态代理,那么这时 Bean 的地址与 getEarlyBeanReference 流程中产生的 Bean 的地址就不一样了,**这时如果不处理这种情况,又会出现 Spring 容器中同时存在两种不同的引用对象,又会造成歧义,所以 Spring 需要避免这种情况的存在

6.4 如果 Bean 地址发生变化则判断是否存在强依赖的 Bean

Spring 在 Bean 的创建过程中如果出现了上诉 6.3 节的情况时,Spring 采取了下面的方法进行处理:


else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {    //获取该Bean依赖的Bean    String[] dependentBeans = getDependentBeans(beanName);    Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);    //去除因为类型检查而创建的Bean(doGetBean方法typeCheckOnly参数来控制)    for (String dependentBean : dependentBeans) {        if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {      actualDependentBeans.add(dependentBean);        }    }    //如果去除因为类型检查而创建的bean之外还存在依赖的bean    //(这些剩下的bean就是spring实际需要使用的)那么就会抛出异常,阻止问题出现    if (!actualDependentBeans.isEmpty()) {  throw new BeanCurrentlyInCreationException(beanName,            "Bean with name '" + beanName + "' has been injected into other beans [" +            StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +      "] in its raw version as part of a circular reference, but has eventually been " +            "wrapped. This means that said other beans do not use the final version of the " +            "bean. This is often the result of over-eager type matching - consider using " +            "'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");    }}
复制代码


上面这段代码就是 Spring 处理上诉情况的逻辑,首先明确的是 Spring 不允许上诉情况发生,Spring 对于 Bean 的引用地址发生变化的情况,Spring 首先会判断依赖的 Bean 是否已经完全创建完毕,如果存在完全创建完成的 Bean 就会直接抛出异常,因为这些完全创建完成的依赖 Bean 中持有的引用已经是旧地址引用了


具体的处理逻辑是:先拿到该 Bean 所有依赖的 Bean,然后从这些 Bean 中排除仅仅是因为类型检查而创建的 Bean,如果排除这些 Bean 之后,还有依赖的 Bean,那么这些 Bean 就是可能存在循环依赖并且是强依赖的 Bean(这些 Bean 中持有的引用地址是老地址,所以会存在问题),Spring 就会及时抛出异常,避免发生问题


作者:京东物流 钟磊

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

发布于: 16 分钟前阅读数: 5
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
带着问题去分析:Spring Bean 生命周期 | 京东物流技术团队_spring_京东科技开发者_InfoQ写作社区