尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Spring源码解读:IoC容器、AOP与循环依赖三级缓存机制

发布时间:2026/9/26 14:07:45

资讯中心
01
ARTICLE

Spring源码解读:IoC容器、AOP与循环依赖三级缓存机制

Spring源码解读:IoC容器、AOP与循环依赖三级缓存机制
Spring源码我前前后后啃了三轮从最初对着IDE发呆到后来能顺着一条调用链摸到容器启动的每一个细节中间踩了无数坑也攒下不少心得。这篇东西不打算复述源码里的类名和方法就想聊聊我在看Spring源码时的一些启迪和思路——怎么入手、怎么拆解、怎么把源码真正变成自己的东西。如果你正准备开始啃Spring源码或者读了一部分感觉卡住了这文章应该能帮上忙。1. 源码阅读的底层心法从“看代码”切换到“读设计”很多人打开Spring源码的第一反应是去记类名、背方法试图把每一行都看懂。我第一轮就是这么干的结果在一个深不见底的调用链里迷路了两个星期最后除了记住几个类名什么都没留下。后来我换了个思路——把阅读源码当成一场和作者的对话。你不需要逐行读你要读的是作者的“决策现场”。Spring框架发展了二十多年每一层抽象、每一个设计模式背后都是当年真实业务需求的沉淀。比如BeanFactory和ApplicationContext为什么要分两层因为前者是最小化的IoC容器契约后者是面向应用开发者的完整功能集合。这不是代码冗余而是接口隔离原则的教科书级示范。我个人的阅读策略分三步走先跑起来再读起来。写一个最简单的Spring Boot应用打断点看调用栈让执行流程“可视化”。先主线再支线。第一遍只追踪核心流程比如bean创建所有旁路逻辑比如事件发布、AOP织入全部跳过。先画图再对照。用自己的话画出时序图和类关系图再和源码对照找自己的理解偏差。这套方法的核心逻辑是源码阅读不是“记忆任务”而是“模型构建任务”。你的目标不是在脑子里复制一份Spring源码而是建立一套关于Spring如何工作的心智模型。注意千万不要试图从第一行代码读到最后一个类那是反人类的。Spring源码几十万行就算是核心容器模块也有几万行逐行阅读等于自虐。你只需要把“主线剧情”读懂支线内容用到再查。还有个很实用的技巧读源码时准备一个“问题清单”。每读到一个“为什么这样设计”的疑问就记下来然后带着问题去找答案。带着问题阅读的效率是漫无目的阅读的好几倍。我读DefaultListableBeanFactory的时候就记了二十多个问题有些在源码里找到了答案有些是翻官方文档才想明白的。2. 三大核心模块的正确打开方式2.1 IoC容器别急着看实现先理解“定位、加载、注册”三段论IoC容器是整个Spring的地基。网上讲IoC的文章多如牛毛但大多数只停留在“控制反转是什么”的层面没有深入到容器的内部工作机理。我读源码时的体会是容器初始化可以拆成三个步骤——定位、加载、注册。先说定位。BeanDefinition从哪来可能是XML文件可能是ComponentScan扫描出来的也可能是Bean方法定义的。ClassPathBeanDefinitionScanner的doScan方法就是干这个的。它把指定包路径下的所有类扫一遍用ASM元数据分析类上的注解只要类上有Component及其派生注解就生成一个ScannedGenericBeanDefinition。然后是加载。这里的“加载”不是指把类加载进JVM而是把类的元数据解析成BeanDefinition对象。BeanDefinition你可以理解成Bean的“生产图纸”里面记录了类的全限定名、作用域、是否懒加载、初始化方法等所有信息。AnnotationConfigApplicationContext的register方法会把配置类转成AnnotatedGenericBeanDefinition再通过ConfigurationClassPostProcessor这个BeanFactoryPostProcessor处理配置类拆解出所有的Bean方法和Import引入的类。最后是注册。DefaultListableBeanFactory里维护了一个核心字段beanDefinitionMap注册就是把这个“生产图纸”放进Map里。源码里的registerBeanDefinition方法做了三件事校验、放入Map、更新缓存。这里的缓存包括beanDefinitionNames列表和manualSingletonNames集合用于后续查询和实例化。这三个阶段理解透了你对IoC容器的认识就超过了90%的CRUD程序员。面试时如果被问“Spring容器启动时发生了什么”你能说出定位、加载、注册这三段式流程再展开每个阶段的类名和方法基本上是稳的。补充一个容易踩的坑Configuration类本身也会被注册成BeanDefinition而且它的BeanName是类的全限定名#全限定名的形式。如果你写代码时手动给配置类起了别名会导致配置类的处理顺序和你预期不一致进而引发Bean方法不生效的诡异问题。2.2 依赖注入构造器、Setter、字段注入源码视角下谁优谁劣依赖注入是IoC容器最核心的“行为”。大家平时都用Autowired但它背后到底是怎么工作的源码里AutowiredAnnotationBeanPostProcessor这个类是处理Autowired的核心。这个BeanPostProcessor在Bean实例化后、初始化前调用postProcessProperties方法扫描当前Bean的所有字段和方法参数找到标注了Autowired的地方然后通过DefaultListableBeanFactory的resolveDependency方法去容器里找对应的Bean。如果找不到且requiredtrue直接抛异常如果找到多个候选Bean会继续按Qualifier或字段名精确匹配。从源码视角看三种注入方式的差别字段注入AutowiredFieldElement这种内部类处理的代码最简洁但会造成依赖隐藏——读者不看字段列表根本不知道这个类依赖了什么。单元测试时想mock依赖只能靠反射很痛苦。Setter注入AutowiredMethodElement处理好处是可以后续重新配置但多写不少样板代码。构造器注入Spring官方推崇的方式。不可变性是最大优势——依赖一旦注入就不能再变配合final字段能保证这个类永远处于完整状态。Spring 4.3以后如果类只有一个构造器Autowired都可以省略源码里会在AutowiredAnnotationBeanPostProcessor的determineCandidateConstructors中自动推断。我自己的项目底线是核心依赖用构造器注入可选依赖用Setter注入字段注入一概不用。这不是洁癖是长期维护多个Spring项目后的血泪教训。字段注入的类写单元测试时太痛苦了。还有个小细节值得留意resolveDependency里有一个ObjectFactory和ObjectProvider的处理分支这是解决“延迟依赖”和“多实例选择”的关键。ObjectProvider.getIfAvailable()这种API就是走这个分支实现的源码层面看其实就是一个延迟加载的代理逻辑。2.3 AOP代理对象的诞生现场AOP模块是Spring里最有“魔法感”的部分。我一读AnnotationAwareAspectJAutoProxyCreator就明白了所谓“魔法”本质上就是“在合适的时机创建代理对象”。核心逻辑在哪AbstractAutoProxyCreator的postProcessAfterInitialization方法会在Bean初始化完成后判断是否需要代理。判断依据是当前Bean是否匹配任何Pointcut。如果匹配就会调用createProxy方法通过ProxyFactory创建代理对象。这里牵出一个重要机制代理创建的时机。如果这个Bean内部有循环依赖那代理创建会被提前——AbstractAutoProxyCreator的getEarlyBeanReference方法专门处理这种情况。如果你看到源码里有earlyProxyReferences这个缓存就是在解决“循环依赖时AOP代理如何不失效”的问题。另一个值得琢磨的点是Spring AOP默认用JDK动态代理还是CGLIB源码里DefaultAopProxyFactory的createAopProxy方法给了答案——如果目标类实现了接口默认JDK动态代理否则用CGLIB。Spring Boot 2.x开始默认proxyTargetClasstrue强制走CGLIB。为什么因为JDK动态代理只能代理接口方法实际开发中很多场景需要代理具体类的方法。实操心得在调试AOP相关问题时先确认两个事实——目标Bean是否真的走了代理创建方法、代理是JDK还是CGLIB。分别看DynamicAdvisedInterceptor和CglibAopProxy$DynamicAdvisedInterceptor的intercept方法这才是真正执行切面逻辑的地方。别在Aspect注解的类里死磕那里什么都看不到。3. 用一个经典问题串联源码循环依赖的三级缓存3.1 为什么要三级缓存两级行不行三级缓存是Spring面试的钉子户问题也是阅读源码时最容易懵的点。先亮出源码结论DefaultSingletonBeanRegistry里定义了三个Map。singletonObjects一级缓存存放完全创建好的单例Bean。earlySingletonObjects二级缓存存放“提前暴露”的Bean这些Bean已经实例化但还没完成属性填充。singletonFactories三级缓存存放ObjectFactory用于生成Bean的早期引用。为什么非要三级很多文章说“为了循环依赖”这个说法太粗糙。准确地说是为了在循环依赖发生时确保最终注入的是代理对象而非原始对象。想一想这个场景A依赖BB依赖A且A需要AOP代理。如果没有三级缓存在B创建时提前暴露A的原始实例那么B里的A引用就是原始对象等A完成代理创建后B手里的A就没有增强逻辑了。三级缓存的出现就是为了让代理创建时机延后但代理效果前置。具体流程A实例化后刚放入三级缓存时它的singletonFactories里存的是一个getEarlyBeanReference的lambda。如果这时候B来获取A会走getSingleton的早期暴露逻辑执行getEarlyBeanReference创建A的早期引用。如果A需要代理这个逻辑会提前创建代理对象放进二级缓存如果不需要代理返回的就是原始实例。之后A完成完整的初始化流程getSingleton发现二级缓存已有对象直接把二级缓存的内容提升到一级缓存。两级行不行理论上可以——在二级缓存中直接存原始对象就能解决循环依赖。但如果A需要AOP代理两级缓存方案下无法保证B拿到的是代理对象。三级缓存的巧妙之处在于它存的不是对象而是ObjectFactory这个工厂在真正需要早期引用时才执行兼顾了循环依赖和AOP代理两件事。3.2 源码现场getSingleton方法里发生了什么DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)是三级缓存的核心入口源码逻辑拆开看就几个分支protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 第一步从一级缓存拿 Object singletonObject this.singletonObjects.get(beanName); // 拿不到且当前Bean正在创建中 if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { // 加锁保证并发安全 synchronized (this.singletonObjects) { // 第二步从二级缓存拿 singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { // 第三步从三级缓存拿ObjectFactory ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { // 执行工厂方法得到早期引用 singletonObject singletonFactory.getObject(); // 提升到二级缓存 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; }注意这个synchronized锁——很多人没注意到三级缓存要考虑并发问题。Spring容器在单线程环境下问题不大但在多线程环境下两个线程同时创建Bean时可能发生竞争。加锁保证同一个BeanName的早期引用在并发场景下不会被创建多次。再往下追singletonFactories里存的lambda是在doCreateBean中注册的// AbstractAutowireCapableBeanFactory.doCreateBean 中的关键代码 boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 添加三级缓存lambda传入的是getEarlyBeanReference方法 addSingletonFactory(beanName, () - getEarlyBeanReference(beanName, mbd, bean)); }这段代码的earlySingletonExposure含义是“是否提前暴露单例”。三个条件缺一不可必须是单例、容器允许循环引用默认允许、当前Bean正在创建中。getEarlyBeanReference里就会调用SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReferenceAOP代理逻辑正是在这里提前介入的。思考题如果你在配置里设置了allowCircularReferencesfalse循环依赖直接抛异常三级缓存完全不生效。这个配置项就是AbstractAutowireCapableBeanFactory里的allowCircularReferences字段默认trueSpring Boot里可以通过SpringApplication.setAllowCircularReferences(false)关闭。3.3 为什么构造器循环依赖救不了三级缓存能解决的循环依赖有一个前提Bean已经完成了实例化内存分配只是属性还没填充好。A在实例化后立即暴露早期引用B才能拿到不完整的A继续初始化。构造器循环依赖的场景是A的构造器需要BB的构造器需要A。这时候A连实例化都没完成三级缓存里还没放任何东西自然救不了。源码层面doCreateBean的createBeanInstance环节就会因为找不到B而抛BeanCurrentlyInCreationException。所以面试如果被问“三级缓存能不能解决所有循环依赖”正确的回答是只能解决Setter注入和字段注入的循环依赖构造器注入的循环依赖无解。Spring官方也建议用设计重构来避免循环依赖而不是依赖缓存机制兜底。4. 源码阅读的正确姿势工具、断点与主线拆分4.1 调试环境搭建与断点位置选择工欲善其事必先利其器。我调试Spring源码用的方案是源码直接下载、Gradle构建、IDEA断点调试。Spring Framework的源码托管在GitHub上用git clone拉下来后切到对应版本分支用IDEA打开后会识别为Gradle项目等依赖下载完就能跑了。调试入口很重要。如果你用Spring Boot最省事的方式是直接调试自己的启动类在SpringApplication.run()入口打上断点然后一路Step Into。但这里有个致命问题——如果从启动类开始逐行跟进你会在Spring Boot的启动流程里绕半天很久都进不到核心的refresh()方法。我的经验是直接在AbstractApplicationContext.refresh()这个方法上打条件断点等它执行到的时候再深入。refresh()是容器启动的中枢里面有invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization等关键子步骤对应着不同阶段的工作。想读BeanDefinition加载就在第一步断点想读Bean实例化就在finishBeanFactoryInitialization里断点。另一个实用的断点位置是DefaultSingletonBeanRegistry.getSingleton方法。这个方法是Bean创建的必经之路各种作用域的Bean都从这里取。打断点后配合条件表达式比如beanName.equals(userService)可以精确追踪某个Bean的完整生命周期。4.2 如何拆主线、记笔记、验证理解前面说过不要逐行读但怎么拆主线我的做法是按“问题驱动”拆。比如我想搞懂“Spring容器启动时Bean是什么时候被创建出来的”这根主线就清晰了refresh()→finishBeanFactoryInitialization()→preInstantiateSingletons()→getBean()→doGetBean()→getSingleton()→createBean()→doCreateBean()。每个环节都有明确的输入输出我画一张时序图标上关键方法、关键参数和关键缓存变动这张图就是我的“主线地图”。记笔记的方式我用的是“异常驱动笔记法”和“对照验证法”。异常驱动笔记法是从问题出发记录“遇到什么问题 → 源码里对应哪段逻辑 → 解决办法是什么”。比如我遇到过NoSuchBeanDefinitionException顺着异常栈追到了DefaultListableBeanFactory.getBeanDefinition发现是BeanDefinition没注册就理解了beanDefinitionMap在什么时候会找不到key。对照验证法是把自己的理解和源码对照。我每读完一个核心流程会先不看源码用代码注释或流程图复述一遍。如果哪一步卡住了说明理解有偏差回到源码重新看。有一次我复述Bean生命周期时漏掉了BeanPostProcessor的postProcessBeforeInitialization重新对照源码才发现initializeBean方法里这两个回调的顺序极其重要顺序反了可能导致PostConstruct和InitializingBean执行时机错位。实操心得用画图工具别用文字写流程。流程图在梳理复杂调用链时的效率比文字笔记高一个量级。画图的过程本身就是强制思考的过程你会发现很多“以为自己懂了但画不出来”的盲区。5. 从源码到实战读Spring源码带来的认知升级5.1 面试中的降维打击读Spring源码给你带来的最大红利其实不是“我知道了那个方法叫什么”而是“我能在没看过源码的情况下用逻辑推导出Spring应该怎么做”。举个例子面试官问“Spring如何管理Bean的作用域”。没读过源码的人只能背singleton、prototype、request、session。读过源码的人可以展开说默认的scope是singleton单例Bean存在singletonObjects里prototype的Bean每次getBean都走一遍createBean不缓存RequestScope和SessionScope是通过SimpleThreadScope和SessionScope类实现的它们实现了Scope接口。甚至可以说出Scope接口的get方法签名和Spring MVC是如何把HTTP请求作用域映射到当前线程的。再比如“MyBatis的Mapper接口为什么能被自动注入”。如果你读过Spring源码自然知道这是FactoryBean的经典应用——MapperFactoryBean实现了FactoryBean接口用getObject方法返回动态代理对象。这个思路比背一万遍MyBatis配置都管用因为面试官要的正是“你理解了Spring的扩展机制”。我对Spring源码的认知升级真正体现在“能自己排查问题”上。以前遇到Bean创建失败可能只能问百度现在看一眼异常栈基本能判断是BeanDefinition没注册、是BeanPostProcessor处理有问题、还是循环依赖配置不当。这种能力不是面试背题能获得的是真正DEBUG过多遍源码的肌肉记忆。5.2 项目架构设计的手感提升读Spring源码还有一个隐性收益架构设计能力。Spring的代码风格是业界顶级的对“可扩展性”和“稳定性”的平衡几乎做到了极致。举一个真实例子。我以前写业务代码喜欢在Service层堆工具方法一个Service类动辄上千行。读了Spring源码之后我开始有意识地把“核心流程”和“旁路逻辑”拆分模仿BeanPostProcessor的机制把可扩展的钩子方法暴露出去让子类或外部类可以在不修改主流程的前提下增强功能。这种“模板方法策略模式”的组合拳就是Spring最常用的套路。还有一次我接手一个老项目发现里面有大量Service互相注入形成了循环依赖的蛛网。以前我可能直接加Lazy糊弄过去但读了三级缓存源码之后我思考出真正的解法把公共依赖抽到一个独立的上下文模块里从结构上消除循环依赖。这完全是源码给的启发——你理解了循环依赖的本质是“创建顺序问题”自然会想到调整依赖结构而不是靠框架兜底。注意源码阅读最大的陷阱是“读完之后什么都懂了但什么都用不上”。我自己的体会是每读完一个核心模块必须强迫自己找到一个实际的业务场景去套用哪怕只是“从Redis换成自定义缓存注册表”这种小改动。没有应用场景的源码阅读就是一场脑力体操还不如去跑步。5.3 保持怀疑与独立思考读源码的过程里我也发现Spring有些设计确实有历史包袱。比如ApplicationContext继承了好几个接口ConfigurableApplicationContext里有十多个方法有些方法在现在的Spring Boot场景里根本用不到。你要学会区分“设计之美”和“历史遗留”。再比如Spring的单例模式是“不完全的单例”——getSingleton返回的对象和JVM层面的静态单例不同它没有类加载器级别的唯一性约束。如果你在多ClassLoader环境下部署每个ClassLoader都会有自己的Spring容器同一个Bean会被创建多份。这不是Spring的bug是设计边界——“单例”指的是“Spring容器内的单例”。这些认知让你在阅读别人写的源码时不会盲目崇拜而是带着“这段设计好在哪里、坏在哪里、如果要重来你会怎么设计”的视角去看。源码阅读最终读的不是代码是作者的决策和对权衡取舍的思考。6. 常见问题排查实录源码视角下的经典案例6.1 循环依赖秒懂排查法循环依赖问题我遇到太多次了。先说排查步骤再说源码依据。异常信息类似BeanCurrentlyInCreationException或“The dependencies of some of the beans in the application context form a cycle”。拿到这个异常第一反应不是去看配置而是确认循环链用IDEA的依赖分析工具或者直接看异常里列出的Bean链。源码层面的依据在DefaultSingletonBeanRegistry里一个Bean在创建期间会被放进singletonsCurrentlyInCreation这个Set里如果创建A时发现依赖B而B又回头依赖A此时getSingleton检查isSingletonCurrentlyInCreation(A)为true就会抛异常。实战建议优先检查是否存在构造器注入的循环依赖这种无解必须重构Setter注入带Lazy注解可以临时绕过循环依赖但列表里其实是想说“慎用”优先级最高的方案永远是结构上消除环。6.2 拦截器不生效的问题定位技巧AOP相关的疑难杂症排查思路比问题本身重要。有一次我在项目里写了一个Aspect发现切面方法完全不执行。排查过程是这样的先确认切面类有没有被Spring容器识别——打断点在AnnotationAwareAspectJAutoProxyCreator的findEligibleAdvisors方法看看返回的ListAdvisor是不是空的。如果是空的检查Aspect注解是否有拼写错误、切面类是否被Spring扫描到。如果有了Advisor再看目标Bean是否走了代理创建——打断点在AbstractAutoProxyCreator.wrapIfNecessary看返回的对象是不是代理对象。最后发现问题是目标类没有实现接口而Spring Boot主类里没有配置proxyTargetClasstrue。老版本的Spring Boot确实有这个问题默认JDK动态代理、只代理接口新版本默认强制CGLIB后这个坑少了很多。但这个排查思路是通用的先确认Advisor匹配再确认代理创建最后确认切面执行。6.3 BeanPostProcessor执行顺序不对的坑自定义BeanPostProcessor的时候最容易出问题的就是执行顺序。有一次我在两个BeanPostProcessor里都重写了postProcessAfterInitialization其中一个总是不生效。排查后发现原因Spring容器对实现了PriorityOrdered、Ordered接口的BeanPostProcessor会先排序执行普通BeanPostProcessor排后面。源码里PostProcessorRegistrationDelegate的registerBeanPostProcessors方法就是干这个的——先注册PriorityOrdered再注册Ordered最后注册普通的。所以如果你想让自定义的BeanPostProcessor尽早执行可以实现PriorityOrdered并返回一个较小的order值或者简单点用Order注解标注。但要记住BeanPostProcessor本身不能依赖其他Bean它在普通Bean创建前就需要实例化所以这个类的依赖注入要注意。排查一个通用技巧遇到任何“为什么我的代码没生效”的问题断点永远比日志快。特别是Spring这种框架级代码日志输出范围太大断点直接落在源码关键方法上一眼就能看出执行路径是否走了你预期的分支。7. 后续还能往哪里深入看完IoC、DI、AOP和循环依赖Spring源码的“主线”其实已经通关了。后续可以往这些方向继续Spring事务从Transactional注解开始读TransactionInterceptor和AbstractPlatformTransactionManager理解事务的传播行为、隔离级别、回滚机制在源码层面是怎么实现的。这里面有一个很有意思的点Spring事务失效的场景比如自调用、private方法在源码里都能找到明确解释。Spring MVC从DispatcherServlet的doDispatch开始追踪一个HTTP请求从进入到返回的全流程。HandlerMapping如何定位处理器、HandlerAdapter如何执行处理器、ViewResolver如何解析视图每条链路都有大量细节。Spring Boot自动配置从EnableAutoConfiguration入手读AutoConfigurationImportSelector.getCandidateConfigurations看看Spring Boot是怎么从spring.factories文件加载上百个自动配置类的。对理解“约定优于配置”的落地非常有帮助。Spring的Conditional机制ConditionalOnClass、ConditionalOnProperty这些注解源码里是SpringBootCondition在做判断。理解了Condition机制你就明白为什么有些自动配置在某些依赖缺失时静默跳过这也是排查Spring Boot“为什么没生效”问题的钥匙。我个人在项目里的进一步实践是结合Spring源码的思路去阅读MyBatis、Spring Security这些生态组件的源码。有了Spring容器的基础再看这些框架如何通过BeanDefinitionRegistryPostProcessor注册自己的Bean定义、如何用FactoryBean生成代理对象、如何通过BeanPostProcessor介入Bean生命周期就特别顺了。它们是Spring扩展点的最佳教学案例。最后分享一个体会读源码不要贪多求快一周读通一个核心流程就已经是巨大收获。我第二轮读Spring花了一个月只啃了Bean生命周期但就是这一个月把doCreateBean里的每一步都摸透了后面读AOP、读事务、读Spring Boot都是在这个基础上平推的。地基打得稳上层建筑才能盖得高这个道理放在源码阅读上再适用不过了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。