Spring框架的开发过程中,循环依赖问题常常让开发者们很头疼。今天这篇文章将通过生动的案例和清晰的流程图,带你深入了解Spring的三级缓存机制,让你轻松搞懂它是如何巧妙解决循环依赖问题的。

一、生活场景中的循环依赖困境

在生活里,循环依赖的情况其实很常见。打个比方,你去公司上班,进入公司大楼需要刷工牌,可办理工牌的时候,工作人员却要求你先登记工位号;而要分配工位号,又得先出示工牌,这就陷入了一种“死循环”。

在Spring框架的开发中,也会出现类似的情况。比如说下面这段代码:

@Service public class ServiceA { @Autowired private ServiceB serviceB; } @Service public class ServiceB { @Autowired private ServiceA serviceA; } 

这里ServiceA依赖ServiceB,ServiceB又依赖ServiceA,形成了循环依赖。那么,Spring是怎么处理这个棘手问题的呢?答案就在于它的三级缓存机制。

二、Spring三级缓存结构大揭秘

在深入了解Spring解决循环依赖的过程前,我们先来认识一下Spring容器内部的三级缓存结构。简单来说,这三级缓存就像是三个不同功能的“仓库”。

  • 一级缓存(成品库)singletonObjects,这个“仓库”存放的是已经完全初始化好的Bean,就像是生产线上已经完工的产品,可以直接使用。
  • 二级缓存(半成品库)earlySingletonObjects,存放提前暴露的原始对象,也就是还没完全组装好的“半成品”。
  • 三级缓存(对象工厂库)singletonFactories,存放生成对象的工厂,这些工厂就像是产品的生产模具,用来生产对应的对象。

三、循环依赖解决流程深度剖析

(一)场景设定

还是以ServiceA依赖ServiceB,ServiceB又依赖ServiceA为例,看看Spring具体是怎么做的。

(二)详细创建流程

  1. 开始创建ServiceA:首先,在内存里为ServiceA开辟一块空间,这就是实例化ServiceA的过程。
  2. 接着,把ServiceA的ObjectFactory(可以理解为生产ServiceA的“模具”)放到三级缓存中。
  3. 给ServiceA填充属性的时候,发现它需要ServiceB。
  4. 于是,开始创建ServiceB,同样先实例化ServiceB,再把ServiceB的ObjectFactory放入三级缓存。
  5. 给ServiceB填充属性时,又发现它需要ServiceA。
  6. 这时,从三级缓存里获取ServiceA的ObjectFactory,通过ObjectFactory.getObject()得到ServiceA的早期引用,这就好比是拿到了一个还没完全做好,但已经有了基本框架的ServiceA。
  7. 把这个早期引用的ServiceA放到二级缓存中,同时把三级缓存里对应的ServiceA的ObjectFactory清除掉。
  8. 等ServiceB完成属性注入和初始化后,就把它放入一级缓存。
  9. 最后,ServiceA继续完成剩下的属性注入和初始化工作,也被放入一级缓存。

(三)关键步骤图解

在这个过程中,ServiceA和ServiceB在三级缓存、二级缓存和一级缓存之间不断交互。简单来说,就是先在三级缓存注册ObjectFactory,当需要注入时,从三级缓存查询对应的工厂,得到ObjectFactory后执行getObject()方法,获取早期引用并转移到二级缓存,等两个Bean都完成初始化后,最终放入一级缓存。

四、为何需要三级缓存

(一)两级缓存的弊端

有些同学可能会问,为什么不只用两级缓存呢?我们通过一个使用AOP代理的Bean的例子来看看。假设有这样一个ServiceA:

@Service public class ServiceA { @Autowired private ServiceB serviceB; @Transactional // 需要生成代理 public void method() {} } 

如果采用两级缓存方案,在第一次从缓存获取时就直接创建代理对象。但要是后续在初始化过程中,Bean的状态发生了变化,就会导致代理对象和原始对象的状态不一致,出现问题。

(二)三级缓存的优势

相比之下,三级缓存的优势就很明显了。它可以延迟代理对象的生成时机,保证最终放入容器的对象是完整的代理对象,避免了上述问题。

(三)各级缓存的职责分工

不同级别的缓存,职责各不相同:

  • 一级缓存:存储完整的Bean实例,在整个应用运行期间都存在,主要作用是提供最终可以使用的Bean。
  • 二级缓存:存放原始对象的早期引用,从对象创建开始,到初始化完成前存在,主要用于解决循环依赖问题。
  • 三级缓存:存放生成对象的ObjectFactory,从实例化后到放入二级缓存前存在,它的主要作用是支持AOP等需要后置处理的情况。

五、常见问题答疑解惑

(一)构造器注入为啥解决不了循环依赖

看下面这段构造器注入的代码:

// 构造器注入示例 @Service public class ServiceA { private final ServiceB serviceB; public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; } } 

使用构造器注入时,对象在实例化阶段就必须完成依赖注入。也就是说,ServiceA实例化需要ServiceB,ServiceB实例化又需要ServiceA,双方都无法完成实例化,只能陷入死循环。

(二)三级缓存中的设计模式

在Spring的三级缓存机制中,运用了多种设计模式:

  • 工厂模式:通过ObjectFactory来延迟对象的创建,就像前面提到的“模具”,需要的时候才用它生产对象。
  • 外观模式:AbstractBeanFactory统一处理缓存,把复杂的缓存操作封装起来,对外提供统一的接口。
  • 代理模式:处理AOP等需要生成代理的情况,保证对象在使用时具备正确的代理功能。

六、最佳实践与避坑要点

(一)尽量避免循环依赖

虽然Spring能解决循环依赖,但最好还是尽量避免出现这种情况:

  • 优先使用@Autowired进行注入,而不是构造器注入。
  • 定期运行mvn dependency:analyze命令,检查项目中的依赖关系,及时发现潜在的循环依赖。
  • 对代码进行重构,引入中间层,打破循环依赖的局面。

(二)调试技巧

在开发过程中,如果想要查看缓存的状态,可以使用下面的代码:

// 查看缓存状态 DefaultSingletonBeanRegistry registry = (DefaultSingletonBeanRegistry)context.getAutowireCapableBeanFactory(); System.out.println("一级缓存:" + registry.getSingletonNames()); 

(三)性能优化

为了让项目运行得更高效,还可以从以下几个方面进行性能优化:

  • 合理设置Bean的作用域,根据实际需求选择合适的作用域,避免不必要的资源浪费。
  • 避免过度使用@Autowired,减少不必要的依赖注入。
  • 及时清理项目中不再需要的Bean,释放内存资源。

七、总结与思考

Spring的三级缓存机制通过提前暴露半成品对象的方式,巧妙地解决了循环依赖的难题。这种设计不仅体现了用空间换时间的优化策略,还做到了关注点分离,让每级缓存的职责都很明确,同时灵活运用了延迟加载的技术。不过,需要注意的是,三级缓存也不是万能的,它并不能解决所有的循环依赖问题。但只要我们深入理解了它的机制原理,就能在开发过程中更好地应对类似的抽象问题。