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

news2024/11/13 13:08:23

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:使用ClassPathBeanDefinitionScannerscanCandidateComponents方法扫描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;
}




如上面的源码所示,这个方法主要是需要调用SmartInstantiationAwareBeanPostProcessorgetEarlyBeanReference方法来提前处理一下尚未创建完成的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 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1135540.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

腾讯云价格计算器有用过的吗?好用!

腾讯云服务器价格计算器可以一键计算出云服务器的精准报价&#xff0c;包括CVM实例规格价格、CPU内存费用、公网带宽收费、存储系统盘和数据盘详细费用&#xff0c;腾讯云百科txybk.com分享腾讯云价格计算器链接入口、使用方法说明&#xff1a; 腾讯云服务器价格计算器 打开腾…

ArcGIS Maps SDK for JS:关闭地图边框(v4.27)

文章目录 1 问题描述2 解决方案 1 问题描述 近期&#xff0c;将ArcGIS Api for JS v4.16更新到了ArcGIS Maps SDK for JS v4.27&#xff0c;原本去除地图的css代码失效了。v4.27需要用.esri-view-surface--touch-none::after控制边框属性。 下面为没有关闭地图边框的效果图。…

电脑录屏快捷键,轻松提升录屏效率

“想问问大家&#xff0c;电脑录屏有快捷键吗&#xff1f;每次都要去定位搜索才能打开&#xff0c;来来回回花费的时间太多&#xff0c;要是有快捷键就方便多了&#xff0c;有人知道电脑录屏的快捷键是什么吗。” 电脑录屏已经成为人们日常学习和工作中不可或缺的一部分&#…

WIFI7协议概述

简介 支持6G频段的320M带宽&#xff0c;提供更快地速度&#xff0c;OFDMA并发数提高至148(wifi6为74)&#xff0c;最大连接终端数对比wifi6提升2倍 支持多链路连接&#xff0c;提供不同的延迟服务 支持rtwt&#xff0c;进行更加细化的节电管理 支持4096QAM高阶调制技术&…

中小型企业选择CRM系统时应该注意哪些?

如今市面上充斥着各种各样的CRM客户管理系统&#xff0c;尽管功能说的天花乱坠&#xff0c;中小企业选型时还是应该以自身需求为主。下面是中小企业选型CRM系统的几个要点&#xff0c;大家可以根据以下需求来筛选。 1、明确自身需求 决定企业选择哪一个CRM系统的前提应是需求…

盘点国产ChatGPT十大模型

什么是ChatGPT ChatGPT是一种基于OpenAI的GPT&#xff08;Generative Pre-trained Transformer&#xff09;模型的聊天机器人。GPT是一种基于深度学习的自然语言处理模型&#xff0c;它使用了Transformer架构来处理文本数据。GPT模型通过在大规模文本数据上进行预训练&#xff…

Clickhouse实时数仓建设

1.概述 Clickhouse是一个开源的列式存储数据库&#xff0c;其主要场景用于在线分析处理查询&#xff08;OLAP&#xff09;&#xff0c;能够使用SQL查询实时生成分析数据报告。今天&#xff0c;笔者就为大家介绍如何使用Clickhouse来构建实时数仓&#xff0c;来满足一些实时性要…

Docker系列---【mysql容器手动停止后,重启服务器,mysql容器被删掉了,如何恢复mysql数据?】...

mysql容器手动停止后&#xff0c;重启服务器&#xff0c;mysql容器被删掉了&#xff0c;如何恢复mysql数据&#xff1f; 1.问题描述 为了快速搭建数据库&#xff0c;我使用了docker搭建数据库&#xff0c;由于服务器资源紧张&#xff0c;我想先把mysql容器停掉&#xff0c;启动…

Elsevier (爱思唯尔) 期刊 投稿流程与注意点

&#x1f604; Elsevier (爱思唯尔) 期刊投稿流程中还是遇到了不少问题的&#xff0c;本篇文章总结一些说明文档和提交要点。 ⭐ LaTex 模板说明 & 投稿流程与准备 latex模版和投稿流程相关参考说明可看下面几个网址&#xff0c;总结的非常全面了&#xff1a; Elsevier&am…

Geoda-双变量空间自相关

Geoda-双变量空间自相关https://mp.weixin.qq.com/s/cOkgBCf5ljlVJkWoIwkzxw 之前空间自相关—莫兰指数中简单写了一下如何在ArcGIS中计算莫兰指数&#xff0c;本次简要演示在Geoda中计算双变量空间自相关的步骤。案例数据是武汉市资源环境承载力指数&#xff08;RECC&#xf…

Golang关键字-select

一、Select解决什么问题&#xff1f; 在Golang中&#xff0c;两个协程之间通信Channel&#xff08;图一&#xff09;&#xff0c;在接受协程中通过代码表示即为<ch&#xff1b;如果协程需要监听多个Channel&#xff0c;只要有其中一个满足条件&#xff0c;就执行相应的逻辑&…

尚硅谷Flume(仅有基础)

q 1 概述 1.1 定义 Flume 是Cloudera 提供的一个高可用的&#xff0c;高可靠的&#xff0c;分布式的海量日志采集、聚合和传输的系统。Flume 基于流式架构&#xff0c;灵活简单。 Flume最主要的作用就是&#xff0c;实时读取服务器本地磁盘的数据&#xff0c;将数据写入到HD…

易点易动固定资产管理系统:高效盘点海量固定资产的得力助手

固定资产是企业重要的财务资源之一&#xff0c;盘点是保证固定资产准确性和完整性的关键环节。然而&#xff0c;对于拥有海量固定资产的企业来说&#xff0c;传统的手工盘点方式效率低下且容易出错。为了解决这一难题&#xff0c;易点易动固定资产管理系统应运而生。本文将深入…

AM@第二类换元法积分

文章目录 abstract第一类换元法第二类换元法分析定理&#x1f47a;证明第二类换元公式的应用 倒代换三角恒等化去根式其他使用第二换元法情形例 附加积分公式表例 附 abstract 第二类换元法(简称第二换元法)的原理和应用 第一类换元法 通过变量代换 u ϕ ( x ) u\phi(x) uϕ…

GoLong的学习之路(十)语法之函数

书接上回&#xff0c;上回书说到&#xff0c;结构体&#xff0c;一言之重在于体。一体之重在于经。经之重甚于骨。这张就说go的经络—函数。 文章目录 函数函数如何定义参数可变参数 返回值多返回值 函数类型与变量 高阶函数函数作为参数函数作为返回值匿名函数闭包defer语句底…

虹科 | 解决方案 | 非道路移动机械诊断方案

虹科Pico汽车示波器为卡车、拖拉机、叉车、船只、联合收割机、挖掘机开发了专用的测试附件和软件测试菜单&#xff0c;比如 24 V 电池、Bosch Denoxtronic、J1939 通信、发动机和液压传动系统以及部件测试等。我们为从事重型车辆和非道路移动机械的维护与诊断的朋友&#xff0c…

通用表表达式查询

1.方法&#xff1a; 1.1普通变量创建 with 表名&#xff08;列名&#xff09; as&#xff08;select 内容&#xff09; 语义&#xff1a;创建一张表 列名和内容11对应 和临时表的区别&#xff0c;这个类似变量&#xff0c;变量和常量的区别 后面可以影响前面&#xff1a; 1…

通天之网:卫星互联网与跨境电商的数字化未来

在当今数字化时代&#xff0c;互联网已经成为商业的核心。跨境电商&#xff0c;作为在线商业的一部分&#xff0c;一直在寻求新的途径来拓宽其边界。近年来&#xff0c;卫星互联网技术的发展已经成为这一领域的重要驱动力&#xff0c;不仅将互联网带到了全球各个角落&#xff0…

DSP 开发例程: led_flash

此例程实现在 EVM6678L 开发板控制 LED 闪烁. 使用了 SYS/BIOS 和 MCSDK PDK TMS320CC6678 两个组件. 例程源码可从我的 gitee.com 仓库上克隆或下载. 目录 创建工程源码编辑main.cplatform_osal.capp.cfg 编译调试使用 板载仿真器使用 外部仿真器 创建工程 点击菜单: File | N…

51单片机实验:数码管动态显示00-99

1、实验要求 利用STC89C52RC单片机开发板实现&#xff1a;使用2位数码管循环显示00-99&#xff0c;每次间隔1s&#xff0c;并且当计数到20时&#xff0c;则蜂鸣器鸣响1次。 2、实验分析 程序实现分析&#xff1a; 1、定义数码管位选引脚&#xff08;P2.4、P2.5、P2.6、…