循环依赖解决方法、三级缓存、面试问题

解决办法

重新设计

当出现这种循环依赖时,很可能是在设计方面存在问题,没有把每个类的职责很好的区分开。应该尝试正确重新设计组件,以便其层次结构设计良好,并且不需要循环依赖项。‎

如果无法重新设计,那么可以考虑其他解决办法。

使用 setter/field 方法注入

上面说到,只有构造方法是在上下文加载时就要求被注入,容易出现依赖循环。所以可以用其他的方式进行依赖注入,setter 和 field 方法注入与构造方法不同,它们不会在创Bean时就注入依赖,而是在被需要时才注入。

setter方法注入

@Service
public class UserService {
    
    private DepartmentService departmentSerivce;
 
   
    @Autowired
    public void setDepartmentSerivce(DepartmentService departmentService) {
        this.departmentService = departmentService;
    }
 
    public List<Department> list() {
        retern departmentService.list();
    }    
 
}

另一个类也是同理使用setter方法注入

field方法注入

(使用@Autowired)

@Service
public class UserService {
    
    @Autowired
    private DepartmentService departmentSerivce;
 
 
    public List<Department> list() {
        retern departmentService.list();
    }    
 
}

使用 @Lazy延时加载

解决Spring 循环依赖的一个简单方法就是对一个Bean使用延时加载。也就是说:这个Bean并没有完全的初始化完,实际上他注入的是一个代理,只有当他首次被使用的时候才会被完全的初始化。
我们对 UserService 进行修改,结果如下:

@Service
public class UserService {
    
    private DepartmentService departmentSerivce;
 
    @Autowired
    public UserSerivce(@Lazy DepartmentService departmentService) {
        this.departmentService = departmentService;
    }
 
    public List<Department> list() {
        retern departmentService.list();
    }    
 
}

使用 @PostConstruct

打破循环的另一种方式是,在要注入的属性(该属性是一个bean)上使用 @Autowired,并使用@PostConstruct 标注在另一个方法,且该方法里设置对其他的依赖。

我们的Bean将修改成下面的代码:

@Service
public class UserService {
 
    @Autowired
    private DepartmentService departmentService;
 
    @PostConstruct
    public void init() {
        departmentService.setUserService(this);
    }
 
    public DepartmentService getDepartmentService() {
        return departmentService;
    }
}
@Service
public class DepartmentService {
 
    private UserService userService;
     
    private String message = "Hi!";
 
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
     
    public String getMessage() {
        return message;
    }
}

实现ApplicationContextAware and InitializingBean接口

如果一个Bean实现了ApplicationContextAware,该Bean可以访问Spring上下文,并可以从那里获取到其他的bean。实现InitializingBean接口,表明这个bean在所有的属性设置完后做一些后置处理操作(调用的顺序为init-method后调用);在这种情况下,我们需要手动设置依赖。

@Service
public class UserService implements ApplicationContextAware, InitializingBean {
 
    private DepartmentService departmentService;
 
    private ApplicationContext context;
 
    public DepartmentService getDepartmentService() {
        return departmentService;
    }
 
    @Override
    public void afterPropertiesSet() throws Exception {
        circB = context.getBean(DepartmentService.class);
    }
 
    @Override
    public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
        context = ctx;
    }
}
public class DepartmentService {
 
    private UserService userService;
 
    private String message = "Hi!";
 
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
 
    public String getMessage() {
        return message;
    }
}

Spring中单例Bean的三级缓存

在这里插入图片描述
第一级缓存〈也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象
第二级缓存: earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整)
第三级缓存: Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂

Spring中Bean的生命周期

在这里插入图片描述
在这里插入图片描述
大概的流程顺序(可以结合着源码看下,我就不贴了):

流程从 getBean 方法开始,getBean 是个空壳方法,所有逻辑直接到 doGetBean 方法中。
transformedBeanName 将 name 转换为真正的 beanName(name 可能是 FactoryBean 以 & 字符开头或者有别名的情况,所以需要转化下)。
然后通过 getSingleton(beanName) 方法尝试从缓存中查找是不是有该实例 sharedInstance(单例在 Spring 的同一容器只会被创建一次,后续再获取 bean,就直接从缓存获取即可)。
如果有的话,sharedInstance 可能是完全实例化好的 bean,也可能是一个原始的 bean,所以再经 getObjectForBeanInstance 处理即可返回。
当然 sharedInstance 也可能是 null,这时候就会执行创建 bean 的逻辑,将结果返回。

Bean初始化主要方法

在这里插入图片描述
getSingleton:希望从容器里面获得单例的bean,没有的话
doCreateBean: 没有就创建bean
populateBean: 创建完了以后,要填充属性
addSingleton: 填充完了以后,再添加到容器进行使用

具体说明

在这里插入图片描述
A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B
B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A放到一级缓存中。
在这里插入图片描述

为什么使用三级缓存

跟源码的时候,发现在创建 beanB 需要引用 beanA 这个“半成品”的时候,就会触发"前期引用",即如下代码:

ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {// 三级缓存有的话,就把他移动到二级缓存singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}

singletonFactory.getObject() 是一个接口方法,这里具体的实现方法在:

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;// 这么一大段就这句话是核心,也就是当bean要进行提前曝光时,// 给一个机会,通过重写后置处理器的getEarlyBeanReference方法,来自定义操作bean// 值得注意的是,如果提前曝光了,但是没有被提前引用,则该后置处理器并不生效!!!// 这也正式三级缓存存在的意义,否则二级缓存就可以解决循环依赖的问题 exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } }return exposedObject;}

这个方法就是 Spring 为什么使用三级缓存,而不是二级缓存的原因,它的目的是为了后置处理,如果没有 AOP 后置处理,就不会走进 if 语句,直接返回了 exposedObject ,相当于啥都没干,二级缓存就够用了。

所以又得出结论,这个三级缓存应该和 AOP 有关系,继续

在 Spring 的源码中getEarlyBeanReference 是 SmartInstantiationAwareBeanPostProcessor 接口的默认方法,真正实现这个方法的只有AbstractAutoProxyCreator 这个类,用于提前曝光的 AOP 代理。

@Overridepublic Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {Object cacheKey = getCacheKey(bean.getClass(), beanName);this.earlyProxyReferences.put(cacheKey, bean);// 对bean进行提前Spring AOP代理return wrapIfNecessary(bean, beanName, cacheKey);}

这么说有点干,来个小 demo 吧,我们都知道 Spring AOP、事务等都是通过代理对象来实现的,而事务的代理对象是由自动代理创建器来自动完成的。也就是说 Spring 最终给我们放进容器里面的是一个代理对象,而非原始对象,假设我们有如下一段业务代码:

@ServicepublicclassHelloServiceImplimplementsHelloService{@Autowiredprivate HelloService helloService;@Override@Transactionalpublic Object hello(){return"Hello JavaKeeper";}}

此 Service 类使用到了事务,所以最终会生成一个 JDK 动态代理对象 Proxy。刚好它又存在自己引用自己的循环依赖,完美符合我们的场景需求。

我们再自定义一个后置处理,来看下效果:

@ComponentpublicclassHelloProcessorimplementsSmartInstantiationAwareBeanPostProcessor{@Overridepublic Object getEarlyBeanReference(Object bean, String beanName)throws BeansException {System.out.println(“提前曝光了:”+beanName);return bean; }}

可以看到,调用方法栈中有我们自己实现的 HelloProcessor,说明这个 bean 会通过 AOP 代理处理。
在这里插入图片描述
再从源码看下这个自己循环自己的 bean 的创建流程:

protected Object doCreateBean( … ){…boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));// 需要提前暴露(支持循环依赖),就注册一个ObjectFactory到三级缓存if (earlySingletonExposure) { // 添加 bean 工厂对象到 singletonFactories 缓存中,并获取原始对象的早期引用//匿名内部方法 getEarlyBeanReference 就是后置处理器 // SmartInstantiationAwareBeanPostProcessor 的一个方法,// 它的功效为:保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }// 此处注意:如果此处自己被循环依赖了 那它会走上面的getEarlyBeanReference,从而创建一个代理对象从 三级缓存转移到二级缓存里// 注意此时候对象还在二级缓存里,并没有在一级缓存。并且此时后续的这两步操作还是用的 exposedObject,它仍旧是原始对象~~~ populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd);// 因为事务的AOP自动代理创建器在getEarlyBeanReference 创建代理后,initializeBean 就不会再重复创建了,二选一的)// 所以经过这两大步后,exposedObject 还是原始对象,通过 getEarlyBeanReference 创建的代理对象还在三级缓存呢 …// 循环依赖校验if (earlySingletonExposure) {// 注意此处第二个参数传的false,表示不去三级缓存里再去调用一次getObject()方法了~~~,此时代理对象还在二级缓存,所以这里拿出来的就是个 代理对象// 最后赋值给exposedObject 然后return出去,进而最终被addSingleton()添加进一级缓存里面去 // 这样就保证了我们容器里 最终实际上是代理对象,而非原始对象~~~~~ Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) { exposedObject = earlySingletonReference; } } … }}

自我解惑:

问:还是不太懂,为什么这么设计呢,即使有代理,在二级缓存代理也可以吧 | 为什么要使用三级缓存呢?

我们再来看下相关代码,假设我们现在是二级缓存架构,创建 A 的时候,我们不知道有没有循环依赖,所以放入二级缓存提前暴露,接着创建 B,也是放入二级缓存,这时候发现又循环依赖了 A,就去二级缓存找,是有,但是如果此时还有 AOP 代理呢,我们要的是代理对象可不是原始对象,这怎么办,只能改逻辑,在第一步的时候,不管3721,所有 Bean 统统去完成 AOP 代理,如果是这样的话,就不需要三级缓存了,但是这样不仅没有必要,而且违背了 Spring 在结合 AOP 跟 Bean 的生命周期的设计。

所以 Spring “多此一举”的将实例先封装到 ObjectFactory 中(三级缓存),主要关键点在 getObject() 方法并非直接返回实例,而是对实例又使用 SmartInstantiationAwareBeanPostProcessor 的 getEarlyBeanReference 方法对 bean 进行处理,也就是说,当 Spring 中存在该后置处理器,所有的单例 bean 在实例化后都会被进行提前曝光到三级缓存中,但是并不是所有的 bean 都存在循环依赖,也就是三级缓存到二级缓存的步骤不一定都会被执行,有可能曝光后直接创建完成,没被提前引用过,就直接被加入到一级缓存中。因此可以确保只有提前曝光且被引用的 bean 才会进行该后置处理。

protected Object getSingleton(String beanName, boolean allowEarlyReference) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {// 三级缓存获取,key=beanName value=objectFactory,objectFactory中存储 //getObject()方法用于获取提前曝光的实例 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {// 三级缓存有的话,就把他移动到二级缓存 singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName); } } } }return singletonObject;}boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {if (logger.isDebugEnabled()) { logger.debug(“Eagerly caching bean '” + beanName +“’ to allow for resolving potential circular references”); }// 添加 bean 工厂对象到 singletonFactories 缓存中,并获取原始对象的早期引用//匿名内部方法 getEarlyBeanReference 就是后置处理器// SmartInstantiationAwareBeanPostProcessor 的一个方法,// 它的功效为:保证自己被循环依赖的时候,即使被别的Bean @Autowire进去的也是代理对象~~~~ AOP自动代理创建器此方法里会创建的代理对象~~~ addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}

再问:AOP 代理对象提前放入了三级缓存,没有经过属性填充和初始化,这个代理又是如何保证依赖属性的注入的呢?

这个又涉及到了 Spring 中动态代理的实现,不管是cglib代理还是jdk动态代理生成的代理类,代理时,会将目标对象 target 保存在最后生成的代理 $proxy 中,当调用 p r o x y 方法时会回调 h . i n v o k e ,而 h . i n v o k e 又会回调目标对象 t a r g e t 的原始方法。所有,其实在 A O P 动态代理时,原始 b e a n 已经被保存在提前曝光代理中了,之后原始 b e a n 继续完成属性填充和初始化操作。因为 A O P 代理 proxy 方法时会回调 h.invoke,而 h.invoke 又会回调目标对象 target 的原始方法。所有,其实在 AOP 动态代理时,原始 bean 已经被保存在 提前曝光代理中了,之后 原始 bean 继续完成属性填充和初始化操作。因为 AOP 代理 proxy方法时会回调h.invoke,而h.invoke又会回调目标对象target的原始方法。所有,其实在AOP动态代理时,原始bean已经被保存在提前曝光代理中了,之后原始bean继续完成属性填充和初始化操作。因为AOP代理proxy中保存着 traget 也就是是 原始bean 的引用,因此后续 原始bean 的完善,也就相当于Spring AOP中的 target 的完善,这样就保证了 AOP 的属性填充与初始化了!

为什么用setter不用构造

Spring 解决循环依赖依靠的是 Bean 的“中间态”这个概念,而这个中间态指的是已经实例化,但还没初始化的状态。实例化的过程又是通过构造器创建的,如果 A 还没创建好出来,怎么可能提前曝光,所以构造器的循环依赖无法解决

面试

B 中提前注入了一个没有经过初始化的 A 类型对象不会有问题吗?

虽然在创建 B 时会提前给 B 注入了一个还未初始化的 A 对象,但是在创建 A 的流程中一直使用的是注入到 B 中的 A 对象的引用,之后会根据这个引用对 A 进行初始化,所以这是没有问题的。

Spring 是如何解决的循环依赖?

Spring 为了解决单例的循环依赖问题,使用了三级缓存。其中一级缓存为单例池(singletonObjects),二级缓存为提前曝光对象(earlySingletonObjects),三级缓存为提前曝光对象工厂(singletonFactories)。

假设A、B循环引用,实例化 A 的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了 B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖 A,这时候从缓存中查找到早期暴露的 A,没有 AOP 代理的话,直接将 A 的原始对象注入 B,完成 B 的初始化后,进行属性填充和初始化,这时候 B 完成后,就去完成剩下的 A 的步骤,如果有 AOP 代理,就进行 AOP 处理获取代理后的对象 A,注入 B,走剩下的流程。

为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?

如果没有 AOP 代理,二级缓存可以解决问题,但是有 AOP 代理的情况下,只用二级缓存就意味着所有 Bean 在实例化后就要完成 AOP 代理,这样违背了 Spring 设计的原则,Spring 在设计之初就是通过 AnnotationAwareAspectJAutoProxyCreator 这个后置处理器来在 Bean 生命周期的最后一步来完成 AOP 代理,而不是在实例化后就立马进行 AOP 代理。

参考文献:
https://baijiahao.baidu.com/s?id=1677105782547823661&wfr=spider&for=pc
https://baijiahao.baidu.com/s?id=1694034649427742142&wfr=spider&for=pc
https://blog.csdn.net/weixin_48306950/article/details/121639656

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不能吃辣的JAVA程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值