ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring三级缓存为何需要三级?循环依赖与AOP代理的深度解析

Spring三级缓存为何需要三级?循环依赖与AOP代理的深度解析 Spring的三级缓存是Java面试里被问得最多、也背得最糊涂的问题之一。很多人能说出“一级缓存存完整Bean二级缓存存半成品三级缓存存ObjectFactory”但一旦被追问“为什么必须是三级两级不够吗”往往就卡壳了。这个问题的答案不在缓存名字里而在Bean创建的完整生命周期里。我排查线上问题时也踩过类似的坑AOP增强明明生效了某个Bean里注入的却还是原始对象最后一路追到DefaultSingletonBeanRegistry才意识到对三级缓存的理解停留在表面有多难受。这篇文章我会从一个典型的循环依赖创建过程讲起拆解一级、二级、三级缓存各自承担的角色重点解释AOP代理如何把第三级缓存“逼”出来再走一遍源码和实际报错最后给出开发和面试都能用的避坑建议。准备面试的开发者可以看正被循环依赖问题折磨的上班族也可以直接跳到第5章。1. 先说循环依赖为什么创建顺序会成为Spring的难题1.1 一个最简单的OrderService与UserService循环依赖先看最经典的场景Service public class OrderService { Autowired private UserService userService; } Service public class UserService { Autowired private OrderService orderService; }容器要创建OrderService发现它需要UserService创建UserService又发现它需要OrderService。两个Bean互相引用形成闭环。如果顺序处理不好你创建我、我创建你永远没有终点。这种互相依赖在业务上其实很常见订单要查用户信息用户要查自己的订单业务模型天然就是双向的。框架要做的是在不死循环的前提下让双方都能拿到对方实例并且保证容器里这个Bean最终只有一个。1.2 Bean创建的三阶段与“半成品”窗口Spring创建单例Bean核心其实只有三步实例化instantiate调用构造器或工厂方法创建一个原始对象此时属性还是空的。属性填充populate执行依赖注入把Autowired、Resource这些属性填进去。初始化initialize执行Aware回调、BeanPostProcessor前置处理、PostConstruct、InitializingBean、init-method以及BeanPostProcessor后置处理。用盖房子类比实例化是打地基属性填充是砌墙初始化是装修。循环依赖的问题在于OrderService开始“砌墙”时需要UserIdCard不对是需要UserService这个“邻居”但UserService还在地基阶段甚至刚开始“打地基”。要打破这个僵局最简单粗暴的思路是允许某个Bean在“半成品”状态被提前暴露给别人。OrderService虽然还没装修完但先把它交给UserService用等UserService建完再回来接着装修OrderService。这就是早期引用early reference的由来。1.3 为什么构造器循环依赖连三级缓存都救不了这里必须强调一个边界Spring的三级缓存只能解决setter注入和字段注入引起的循环依赖解决不了构造器注入的循环依赖。原因很简单。构造器注入发生在实例化阶段也就是“打地基”这一步。OrderService构造时就需要UserService可此刻OrderService还没真正实例化完成三级缓存根本没有机会把OrderService放进去UserService自然拿不到它。所以你会看到这类报错Requested bean is currently in creation: Is there an unresolvable circular reference?遇到构造器循环依赖别指望三级缓存兜底用Lazy延迟其中一个参数或者重新设计依赖关系才是正路。这个点后面第5章还会展开。2. Spring三级缓存结构拆解一级、二级、三级分别干了什么2.1 三级缓存一张表讲清楚Spring的“三级缓存”不是三个独立的高级组件而是定义在DefaultSingletonBeanRegistry里的三个Map缓存层级字段名存储内容写入时机移除时机一级缓存singletonObjects完整的单例Bean也就是最终成品Bean初始化完成addSingleton时容器销毁或手动移除二级缓存earlySingletonObjects提前暴露的早期引用可能是原始对象也可能是代理对象从三级缓存的ObjectFactory中取出结果后Bean进入一级缓存时三级缓存singletonFactoriesObjectFactory工厂用于延迟生成早期引用Bean实例化完成、属性填充之前被二级缓存取代时很多人的误区是把它理解成一条流水线Bean先放三级再升二级最后升一级。实际上三个缓存不是“依次存放”的它们是不同状态下Bean的落脚点。没有发生循环依赖时Bean实例化完、初始化完直接进入一级缓存不会碰二三级。只有发生循环依赖某个Bean在创建途中被其他Bean引用才会从三级缓存里取出工厂生成早期引用放进二级缓存。最终创建完成后如果二级缓存里有这个Bean会把它移入一级缓存同时清理二三级缓存中的记录。2.2 创建阶段的缓存流转顺序我用一个具体流程说明。假设先创建AA又依赖B容器创建AA实例化完成此时A还是个半成品。在A属性填充之前框架把A对应的ObjectFactory放进三级缓存。A填充属性时发现需要B就去创建B。B实例化完成同样把B的ObjectFactory放进三级缓存。B填充属性时发现需要A此时A还在创建中一级缓存没有二级缓存没有但三级缓存里有A的工厂。调用A的ObjectFactory.getObject()得到A的早期引用可能是A的原始对象也可能是A的代理对象放入二级缓存并移除A的三级缓存。B拿到A的早期引用继续完成自己的属性填充和初始化然后B进入一级缓存。回到AA拿到B的实例继续完成自己的属性填充和初始化然后A进入一级缓存。所以二级缓存存在的直接意义是缓存“已经从三级工厂里取出来的早期引用”。它避免同一个Bean被反复调用ObjectFactory也避免多个线程同时getObject时拿到多个不同的代理对象。这一点在并行创建Bean的极端场景下尤其重要。2.3 三个缓存之间是“升级”不是“替换”再强调一下二级缓存是“三级工厂执行结果”的缓存一级缓存是“最终成品”的缓存。三级缓存里保存的ObjectFactory不是成品本身而是一个可以生成早期引用的配方。为什么配方这么重要因为Spring无法预知这个Bean以后会不会被AOP代理。如果在实例化完成时就决定要不要代理那等于在Bean还没经历完整的生命周期前就强制它做最终决策。Spring用的是“按需决策”只有当别的Bean真的需要引用它时才调用工厂生成那个最终一致的代理对象。提示理解三级缓存的关键是记住“工厂”和“对象”的区别。二级缓存只能放对象三级缓存放的是延迟生成对象的工厂。正是这个“延迟”留出了AOP代理介入的机会。3. 核心矛盾AOP代理为什么逼出了第三级缓存3.1 没有AOP时两级缓存为什么能跑如果所有Bean都不做AOP代理只保留一级和二级缓存其实完全可以解决循环依赖A实例化完把原始A放入二级缓存。B需要A从二级缓存拿到原始A。B创建完成进入一级缓存。A继续创建完成初始化把原始A放入一级缓存。最后容器里的A和B持有的A都是同一个原始对象没有问题。网上很多人说“两级缓存够了”他们的论证基础是在这个前提下成立的。但Spring是通用框架不可能假设所有Bean都不需要代理。Spring AOP、事务、异步、缓存等功能都依赖Bean的代理对象。一旦引入代理两级缓存就露馅了。3.2 有AOP后两级缓存暴露出的“身份不一致”假设A需要被CGLIB代理。Spring AOP默认在Bean初始化完成后的BeanPostProcessor后置处理阶段用wrapIfNecessary生成代理对象。正常流程是A初始化完成生成代理A放入一级缓存。但在循环依赖场景里B在A还没初始化时就引用了A。如果二级缓存里存的是原始AB最终持有的是原始A。等A初始化完容器里一级缓存存的是代理A。同一个Bean容器里最终版本是代理AB里注入的却是原始A。后果是什么B调用A的方法时所有通过代理实现的增强逻辑全部失效事务不生效、权限校验不生效、异步不生效。更严重的是如果A有内部方法调用原始对象还可能产生自调用问题。你排查了半天发现只是注入到了一个循环依赖的Bean里非常痛苦。3.3 ObjectFactory才是延迟代理的关键三级缓存把ObjectFactory放在实例化完成后属性填充之前boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }这里最关键的就是getEarlyBeanReference。当B需要A时会调用A的ObjectFactory.getObject()进入getEarlyBeanReference方法。这个方法会遍历所有SmartInstantiationAwareBeanPostProcessor给它们机会提前处理A。在Spring AOP自动代理创建器下getEarlyBeanReference会执行wrapIfNecessary提前生成A的代理对象。也就是说代理在“被需要的那一刻”就产生了而不是等到A初始化完成后才产生。这样B拿到的就是代理AA最终进入一级缓存的也是同一个代理A身份完全一致。3.4 源码验证getEarlyBeanReference与earlyProxyReferences看Spring源码里的关键逻辑。DefaultSingletonBeanRegistry中的getSingleton方法protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { synchronized (this.singletonObjects) { singletonObject this.singletonObjects.get(beanName); if (singletonObject null) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null) { ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }这段代码做了三件事一级缓存有就直接返回成品。一级没有且Bean正在创建中就去二级缓存看。二级也没有就去三级缓存找工厂执行工厂把结果升级到二级缓存并删除三级缓存。再看getEarlyBeanReferenceprotected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject bean; if (!mbd.isSynthetic() hasInstantiationAwareBeanPostProcessors()) { for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) { exposedObject bp.getEarlyBeanReference(exposedObject, beanName, mbd); } } return exposedObject; }AbstractAutoProxyCreator重写了这个方法public Object getEarlyBeanReference(Object bean, String beanName) { Object cacheKey getCacheKey(bean.getClass(), beanName); this.earlyProxyReferences.put(cacheKey, Boolean.TRUE); return wrapIfNecessary(bean, beanName, cacheKey); }earlyProxyReferences这个集合非常关键。它在提前代理时做了标记然后在正常初始化结束后postProcessAfterInitialization里会判断if (this.earlyProxyReferences.remove(cacheKey) ! null) { return bean; } return wrapIfNecessary(bean, beanName, cacheKey);翻译成人话就是这个Bean已经在循环依赖阶段提前代理过了初始化结束后就别再代理一次。这样既保证了代理对象提前产生又保证了代理只做一次。注意AOP代理只能用一次不能重复代理。没有earlyProxyReferences这个标记同一个Bean可能出现“代理套代理”的问题最终类型对不上注入直接失败。4. 源码级走读A/B循环依赖的完整执行链路4.1 DefaultSingletonBeanRegistry中的缓存查找逻辑先记住这个入口Spring在doGetBean方法中获取单例Bean时会先调用getSingleton(beanName)做一次缓存查询。这个查询底层就是走带allowEarlyReference的逻辑它会依次查一级、二级、三级缓存。只有三级缓存也查不到才真正进入创建流程。所以循环依赖发生时B需要A的那一刻不会傻乎乎去重新创建A而是能通过三级缓存拿到A的早期引用。如果你在自己的代码里模拟这个逻辑至少要有三个Map以及一个“当前Bean是否正在创建中”的集合否则无法判断该不该从三级缓存取。4.2 doCreateBean中的提前暴露时机doCreateBean方法在Bean实例化完成后有一段核心代码boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }时间点非常重要它发生在属性填充之前。为什么必须这么早因为循环依赖发生的时间点正是A填充属性、B填充属性彼此需要对方的时候。如果等到A填充结束再暴露三级缓存B引用A时就找不到工厂循环依赖还是解不了。所以框架必须在“一实例化完、还没开始填充属性”时就暴露工厂。allowCircularReferences默认是true但Spring Boot 2.6开始默认关掉了循环依赖支持。这表示框架正在引导开发者主动消除循环依赖而不是依赖这种“半成品引用”机制。4.3 一次循环依赖的完整时序我用一个带代理的判断走一遍调用getBean(a)一级缓存无开始创建A。A实例化完成A是原始对象。A满足earlySingletonExposure把A的ObjectFactory放入三级缓存。A开始属性填充发现需要B转入getBean(b)。B同样实例化完成把B的ObjectFactory放入三级缓存。B开始属性填充发现需要A调用getSingleton(a, true)。一级缓存无A二级缓存无A三级缓存有A的工厂。执行工厂进入getEarlyBeanReference。如果A需要AOP代理这里生成代理A并标记A已提前代理。代理A放入二级缓存A的三级缓存被移除。B拿到代理A完成属性填充、初始化进入一级缓存。回到AA也拿到B完成属性填充。A进入初始化阶段BeanPostProcessor后置处理时判断A已经提前代理过不再二次代理。A最终以代理A的身份进入一级缓存二级缓存和三级缓存中的中间数据被清理。整个过程中B拿到的是代理A容器里最终存的也是代理A身份完全统一AOP增强不会失效。4.4 去掉第三级缓存后的两个替代方案如果非要改成两级缓存理论上只有两条路但都有硬伤。方案一二级缓存存原始对象。这就是前面说的身份不一致问题最终容器里放代理AB里还是原始AAOP逻辑悄悄失效。方案二二级缓存直接存代理对象。那就必须在Bean实例化完成后立刻对每个Bean做AOP代理即使这个Bean根本不会被循环依赖引用也要提前代理。这不仅白白增加代理开销还会打乱正常生命周期里多个BeanPostProcessor的执行顺序。比如某些后置处理器需要在Bean初始化完成后才能获取完整上下文提前代理可能导致增强条件不满足。所以Spring选择用第三级缓存存ObjectFactory把“要不要代理、生成什么代理”延迟到真正发生循环依赖的那一刻。这是成本最低、又能保证代理唯一性的方案。5. 实战问题与排查技巧5.1 构造器循环依赖报错怎么解报错信息最常见的是The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | orderService ↑ ↓ | userService └─────┘这段话说明检测到了循环依赖。如果两个Bean都是通过构造器注入互相依赖三级缓存救不了因为实例化阶段还没暴露工厂。解决办法在其中一个构造参数上加Lazy延迟真实依赖解析。把其中一个改成setter注入或字段注入。重新设计依赖引入中间层把双向依赖改成单向依赖。我自己的经验是新项目尽量都走构造器注入出现循环依赖时直接暴露设计问题。老项目太多字段注入改成构造器注入工作量大那就先用Lazy过渡后续再重构。5.2 注入的对象不是代理对象是怎么回事如果你确认AOP生效但某个Bean里注入的A不是代理对象大概率是循环依赖发生在AOP代理生成之前或者你自定义的BeanPostProcessor执行顺序不对。排查时先确认这个注入方是否和A形成了循环依赖。如果形成了循环依赖Spring确实会通过三级缓存提前代理但前提是我们使用的是标准Spring AOP的自动代理创建器。如果用了自定义的SmartInstantiationAwareBeanPostProcessor或者某个后置处理器在getEarlyBeanReference阶段什么都没做那B拿到的还是原始A。此时先检查有没有自定义InstantiationAwareBeanPostProcessor再看它是否实现了getEarlyBeanReference。多数情况下这个坑来自团队里有人手写代理逻辑破坏了Spring默认行为。5.3 Spring Boot 2.6后循环依赖默认关闭Spring Boot 2.6开始官方把spring.main.allow-circular-references默认值改成了false。也就是说你现在新建项目出现循环依赖会直接启动失败不再像旧版本那样自动用三级缓存兜底。要临时解开可以配spring.main.allow-circular-referencestrue但我不建议你这么干。三级缓存解决循环依赖属于“能用但不好”的机制。它让Bean以半成品身份被注入一旦后期增强逻辑变了行为会变得难以预测。把循环依赖消除后代码的依赖方向清晰了AOP、事务、单元测试都更好做。提示听到“循环依赖启动报错”先看日志里有没有AllowCircularReferences字样。如果是Spring Boot 2.6默认就是拒绝与其改配置不如改代码。5.4 Transactional与循环依赖的坑Transactional也是通过AOP代理实现的。当循环依赖存在时Spring会在getEarlyBeanReference阶段提前创建事务代理所以大多数情况下事务还能生效。但隐患在于事务代理通常会绑定事务管理器、拦截器等底层依赖。如果一个Bean在循环依赖中被提前代理而它依赖的某些基础设施还没有完全准备好就可能出现代理创建成功但增强逻辑缺失的情况。线上表现为“没报错但事务就是不回滚”。遇到这种问题不要盯着三级缓存源码较劲先把循环依赖拆掉。通常一个循环依赖拆掉后Transactional的异常会一起消失。5.5 给面试和排查的一句话总结如果面试官问你“为什么用三级缓存而不是两级”可以按这个思路回答循环依赖需要在Bean未完成初始化时提前暴露引用。没有AOP时两级缓存足够但Spring AOP要求代理对象在最终容器和注入方中保持一致。两级缓存如果存原始对象会和最终代理对不上如果直接存代理对象所有Bean都会被提前强制代理破坏正常生命周期。三级缓存用ObjectFactory延迟生成早期引用只在真正发生循环依赖时才决定是否生成代理并且能保证代理只生成一次。这个答案既讲清了机制也说清楚了取舍比单纯背三个Map的存储内容高一个层次。我个人的体会是三级缓存的源码其实不难难的是理解它背后“延迟决策”的思想。Spring的大量设计都在做类似的事能延迟就延迟能按需就按需不做无谓的提前操作。你在排查循环依赖问题时多试几次把allow-circular-references关掉、用Lazy破环再回头看三级缓存会有完全不一样的感觉。它不是一个需要“绕过去”的bug而是Spring在复杂代理场景下给出的一个平衡方案。
返回列表