高频面试题:一张图彻底搞懂Spring循环依赖,2024年最新掌门一对一面试需要准备什么

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

图片

上图中,BeanA类依赖了BeanB类,BeanB类依赖了BeanC类,BeanC类依赖了BeanA类,如此,也形成了一个依赖闭环。再比如:

图片

上图中,自己引用了自己,自己和自己形成了依赖关系。同样也是一个依赖闭环。那么,如果出现此类循环依赖的情况,会出现什么问题呢?

2 循环依赖问题复现


2.1 定义依赖关系

我们继续扩展前面的内容,给ModifyService增加一个属性,代码如下:

@GPService

public class ModifyService implements IModifyService {

@GPAutowired private QueryService queryService;

}

给QueryService增加一个属性,代码如下:

@GPService

@Slf4j

public class QueryService implements IQueryService {

@GPAutowired private ModifyService modifyService;

}

如此,ModifyService依赖了QueryService,同时QueryService也依赖了ModifyService,形成了依赖闭环。那么这种情况下会出现什么问题呢?

2.2 问题复现

我们来运行调试一下之前的代码,在GPApplicationContext初始化后打上断点,我们来跟踪一下IoC容器里面的情况,如下图:

图片

启动项目,我们发现只要是有循环依赖关系的属性并没有自动赋值,而没有循环依赖关系的属性均有自动赋值,如下图所示:

图片

这种情况是怎么造成的呢?我们分析原因之后发现,因为,IoC容器对Bean的初始化是根据BeanDefinition循环迭代,有一定的顺序。这样,在执行依赖注入时,需要自动赋值的属性对应的对象有可能还没初始化,没有初始化也就没有对应的实例可以注入。于是,就出现我们看到的情况。

3 使用缓存解决循环依赖问题


图片

3.1 定义缓存

具体代码如下:

// 循环依赖的标识—当前正在创建的实例bean

private Set singletonsCurrectlyInCreation = new HashSet();

//一级缓存

private Map<String, Object> singletonObjects = new HashMap<String, Object>();

// 二级缓存: 为了将成熟的bean和纯净的bean分离. 避免读取到不完整的bean.

private Map<String, Object> earlySingletonObjects = new HashMap<String, Object>();

3.2 判断循环依赖

增加getSingleton()方法:

/**

  • 判断是否是循环引用的出口.

  • @param beanName

  • @return

*/

private Object getSingleton(String beanName,GPBeanDefinition beanDefinition) {

//先去一级缓存里拿,

Object bean = singletonObjects.get(beanName);

// 一级缓存中没有, 但是正在创建的bean标识中有, 说明是循环依赖

if (bean == null && singletonsCurrentlyInCreation.contains(beanName)) {

bean = earlySingletonObjects.get(beanName);

// 如果二级缓存中没有, 就从三级缓存中拿

if (bean == null) {

// 从三级缓存中取

Object object = instantiateBean(beanName,beanDefinition);

// 然后将其放入到二级缓存中. 因为如果有多次依赖, 就去二级缓存中判断. 已经有了就不在再次创建了

earlySingletonObjects.put(beanName, object);

}

}

return bean;

}

3.3 添加缓存

修改getBean()方法,在getBean()方法中添加如下代码:

//Bean的实例化,DI是从而这个方法开始的

public Object getBean(String beanName){

//1、先拿到BeanDefinition配置信息

GPBeanDefinition beanDefinition = regitry.beanDefinitionMap.get(beanName);

// 增加一个出口. 判断实体类是否已经被加载过了

Object singleton = getSingleton(beanName,beanDefinition);

if (singleton != null) { return singleton; }

// 标记bean正在创建

if (!singletonsCurrentlyInCreation.contains(beanName)) {

singletonsCurrentlyInCreation.add(beanName);

}

//2、反射实例化newInstance();

Object instance = instantiateBean(beanName,beanDefinition);

//放入一级缓存

this.singletonObjects.put(beanName, instance);

//3、封装成一个叫做BeanWrapper

GPBeanWrapper beanWrapper = new GPBeanWrapper(instance);

//4、执行依赖注入

populateBean(beanName,beanDefinition,beanWrapper);

//5、保存到IoC容器

factoryBeanInstanceCache.put(beanName,beanWrapper);

return beanWrapper.getWrapperInstance();

}

3.4 添加依赖注入

修改populateBean()方法,代码如下:

private void populateBean(String beanName, GPBeanDefinition beanDefinition, GPBeanWrapper beanWrapper) {

try {

//ioc.get(beanName) 相当于通过接口的全名拿到接口的实现的实例

field.set(instance,getBean(autowiredBeanName));

} catch (IllegalAccessException e) {

e.printStackTrace();

continue;

}

}

4 循环依赖对AOP创建代理对象的影响


4.1 循环依赖下的代理对象创建过程

我们都知道Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说Spring最终给我们放进容器里面的是一个代理对象,而非原始对象。

这里我们结合循环依赖,再分析一下AOP代理对象的创建过程和最终放进容器内的动作,看如下代码:

@Service

public class MyServiceImpl implements MyService {

@Autowired

private MyService myService;

@Transactional

@Override

public Object hello(Integer id) {

return “service hello”;

}

}

此Service类使用到了事务,所以最终会生成一个JDK动态代理对象Proxy。刚好它又存在自己引用自己的循环依赖的情况。跟进到Spring创建Bean的源码部分,来看doCreateBean()方法:

protected Object doCreateBean( … ){

// 如果允许循环依赖,此处会添加一个ObjectFactory到三级缓存里面,以备创建对象并且提前暴露引用

// 此处Tips:getEarlyBeanReference是后置处理器SmartInstantiationAwareBeanPostProcessor的一个方法,

// 主要是保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象

// AOP自动代理创建器此方法里会创建的代理对象

// Eagerly cache singletons to be able to resolve circular references

// even when triggered by lifecycle interfaces like BeanFactoryAware.

boolean earlySingletonExposure = (mbd.isSingleton() &&

this.allowCircularReferences &&

isSingletonCurrentlyInCreation(beanName));

if (earlySingletonExposure) { // 需要提前暴露(支持循环依赖),注册一个ObjectFactory到三级缓存

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

}

// 如果发现自己被循环依赖,会执行上面的getEarlyBeanReference()方法,从而创建一个代理对象从三级缓存转移到二级缓存里

// 注意此时候对象还在二级缓存里,并没有在一级缓存。并且此时可以知道exposedObject仍旧是原始对象 populateBean(beanName, mbd, instanceWrapper);

exposedObject = initializeBean(beanName, exposedObject, mbd);

// 经过这两大步后,exposedObject还是原始对象

// 注意:此处是以事务的AOP为例

// 因为事务的AOP自动代理创建器在getEarlyBeanReference()创建代理后,

// initializeBean() 就不会再重复创建了,二选一,下面会有详细描述)

// 循环依赖校验(非常重要)

if (earlySingletonExposure) {

// 前面讲到因为自己被循环依赖了,所以此时候代理对象还存放在二级缓存中

// 因此,此处getSingleton(),就会把代理对象拿出来

// 然后赋值给exposedObject对象并返回,最终被addSingleton()添加进一级缓存中

// 这样就保证了我们容器里缓存的对象实际上是代理对象,而非原始对象

Object earlySingletonReference = getSingleton(beanName, false);

if (earlySingletonReference != null) {

// 这个判断不可少(因为initializeBean()方法中给exposedObject对象重新赋过值,否则就是是两个不同的对象实例)

if (exposedObject == bean) {

exposedObject = earlySingletonReference;

}

}

}

}

以上代码分析的是代理对象有自己存在循环依赖的情况,Spring用三级缓存很巧妙的进行解决了这个问题。

4.2 非循环依赖下的代理对象创建过程

如果自己并不存在循环依赖的情况,Spring的处理过程就稍微不同,继续跟进源码:

protected Object doCreateBean( … ) {

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

最后

码字不易,觉得有帮助的可以帮忙点个赞,让更多有需要的人看到

又是一年求职季,在这里,我为各位准备了一套Java程序员精选高频面试笔试真题,来帮助大家攻下BAT的offer,题目范围从初级的Java基础到高级的分布式架构等等一系列的面试题和答案,用于给大家作为参考

以下是部分内容截图
架构面试专题及架构学习笔记导图.png

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

最后

码字不易,觉得有帮助的可以帮忙点个赞,让更多有需要的人看到

又是一年求职季,在这里,我为各位准备了一套Java程序员精选高频面试笔试真题,来帮助大家攻下BAT的offer,题目范围从初级的Java基础到高级的分布式架构等等一系列的面试题和答案,用于给大家作为参考

以下是部分内容截图
[外链图片转存中…(img-Rt3pOwWG-1713193124688)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-XDt6XON5-1713193124688)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值