Skip to content

Spring 源码阅读方法 ​

S8-001选修高级约 90 分钟JDK 21持续学习 · 源码与开源

1. 本节定位 ​

读源码不是「从头读文件」,而是「带着问题做实验」 —— 本节的实证:用约 80 行复现 Spring 的三级缓存,把抽象概念变成可运行的代码 ✓✓:

text
场景:A 依赖 B,B 依赖 A(字段注入)—— 循环依赖
实测输出(每一步都对应 Spring 源码的类与方法名):
  实例化 a:A@7852e922(属性还没填)
    addSingletonFactory(a) → 三级缓存注册工厂(提前暴露)
    a.populateBean:需要 b → 递归 getBean
    实例化 b:B@232204a1(属性还没填)
      b.populateBean:需要 a → 递归 getBean
    getSingleton(a):三级缓存命中 → 提前暴露的引用升入二级缓存
  同一个 A 实例吗? true ✓✓

★ 代码:labs/java-deep/src/main/java/cn/kb/learn/deep/reading/MiniIocLesson.java ✓

2. 学习目标 ​

  • 掌握「三层阅读法」:跑起来 → 观察 → 最小复现 ★★★;
  • 会用「问题清单」驱动源码阅读(而不是通读)★★★;
  • 能说清三级缓存解决循环依赖的完整流程 ★★★;
  • 知道循环依赖在什么情况下 Spring 也救不了(构造器注入)★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

★ 两种读源码的方式,效率差十倍 ✗✓:

通读式(低效)✗问题驱动式(高效)✓✓
从入口类一路读下去,越读越晕一次只回答一个问题
追求「读懂全部」追求「能画出这条链路」
读完就忘用最小复现验证,忘不掉 ✓✓✓

★★ 一句话 ✓✓:读源码的产物不是「读过的页数」,而是「一张链路图 + 一个可运行的复现」 ✓。

5. 核心概念 ​

5.1 三层阅读法 ★★★ ​

text
① 跑起来:跑既有实验([S3-007](../../03-enterprise/spring/S3-007) 的 IocLesson/AopLesson),观察现象
② 观察:开 debug 日志 / 打断点 / 用 Arthas 看调用栈([S6-011](../../06-java-deep/jvm/S6-011))
③ 最小复现:把机制用 50-100 行写出来 —— **能复现,才算真懂** ✓✓✓

★ 为什么「最小复现」最有效 ✓✓✓:它强迫你回答每一个细节(缓存放什么、什么时候放、什么时候清)—— 读的时候「好像懂了」,写的时候才知道哪里不懂 ✓。

5.2 问题清单:Spring 的五个经典问题 ★★★ ​

问题关键类(Spring 源码)本知识库的复现
循环依赖怎么解决?DefaultSingletonBeanRegistry 的三级缓存本节 MiniIocLesson ✓✓
Bean 的生命周期?AbstractAutowireCapableBeanFactory.doCreateBean见 5.3 步骤清单 ✓
AOP 代理什么时候生成?AbstractAutoProxyCreator《Spring:AOP 与事务》 的失效场景实验 ✓
@Transactional 为什么失效?代理 + 自调用《Spring:AOP 与事务》 实测 ✓✓
事务传播行为?AbstractPlatformTransactionManager同上 ✓

★ 用法 ✓✓:一次挑一个问题,用三层阅读法走完 → 记一张链路图 → 下一个 ✓。

5.3 实测:三级缓存解决循环依赖(完整流程)★★★ ​

★ 三个缓存的职责(对应 Spring 的字段名) ✓✓✓:

缓存Spring 字段放什么为什么需要
一级singletonObjects成品 bean常规命中
二级earlySingletonObjects已提前暴露的引用避免重复调用工厂(保证单例)✓
三级singletonFactories工厂(可能是代理)支持 AOP:早期引用要能是代理 ✓✓

★ 实测流程(节选) ✓✓✓:

text
    实例化 a:A@7852e922(属性还没填)
      addSingletonFactory(a) → 三级缓存注册工厂(提前暴露)
      a.populateBean:需要 b → 递归 getBean
    实例化 b:B@232204a1(属性还没填)
      b.populateBean:需要 a → 递归 getBean
    getSingleton(a):三级缓存命中 → 提前暴露的引用升入二级缓存
  同一个 A 实例吗? true ✓✓(循环依赖被解开,且没有产生两个对象)

★★ 关键结论 ✓✓✓:三级缓存的作用是「提前暴露未完成品」 —— 一级放成品、二级放已暴露引用、三级放工厂(支持代理) ✓; 而构造器注入的循环依赖无法解决(实例都造不出来,无从暴露)✗✓ —— 复现代码里已注释该分支。

5.4 阅读工具与技巧 ★★ ​

工具用途
IDE 断点 + 调用栈看「谁调用了谁」(F7/F8 有策略地走)✓
Spring 的 logging.level.org.springframework=DEBUG看容器启动的关键步骤 ✓
Arthas jad / trace线上/无源码时反编译与追踪(《Arthas 与线上问题诊断》)✓
写复现最高效的「调试器」✓✓✓

6. 递进示例 ​

6.1 最小复现的骨架 ​

java
Object getSingleton(String name) {
    Object bean = singletonObjects.get(name);          // 一级
    if (bean == null) bean = earlySingletonObjects.get(name);   // 二级
    if (bean == null) {                                // 三级:工厂
        Supplier<Object> factory = singletonFactories.get(name);
        if (factory != null) {
            bean = factory.get();
            earlySingletonObjects.put(name, bean);
            singletonFactories.remove(name);
        }
    }
    return bean;
}

6.2 读源码的节奏 ​

text
一天一个问题(不贪多)→ 写链路图 → 写复现 → 讲给别人听(讲不清 = 没懂)✓✓

6.3 反例:抄一遍源码注释就当读过 ​

text
把 doCreateBean 的注释抄到笔记里 ✗(一周后全忘)
正解:**自己写一个 80 行的版本**,再回去对照真实源码找差异 ✓✓✓

7. 完整可运行代码与实测 ​

代码:labs/java-deep/src/main/java/cn/kb/learn/deep/reading/MiniIocLesson.java

运行:

powershell
cd labs
java -cp java-deep/target/classes cn.kb.learn.deep.reading.MiniIocLesson

实测输出(完整):

text
=== 场景:A 依赖 B,B 又依赖 A(字段注入)===
    实例化 a:A@7852e922(属性还没填)
      addSingletonFactory(a) → 三级缓存注册工厂(提前暴露)
      a.populateBean:需要 b → 递归 getBean
    实例化 b:B@232204a1(属性还没填)
      addSingletonFactory(b) → 三级缓存注册工厂(提前暴露)
      b.populateBean:需要 a → 递归 getBean
    getSingleton(a):三级缓存命中 → 提前暴露的引用升入二级缓存
    getBean(a):命中缓存,直接返回
      b 创建完成 → 进入一级缓存 ✓✓
      a 创建完成 → 进入一级缓存 ✓✓
  同一个 A 实例吗? true ✓✓(循环依赖被解开,且没有产生两个对象)

8. Debug 观察点 ​

  1. 把字段注入改成构造器注入(在复现里):观察流程直接失败 ✓✓(这就是 Spring 的边界);
  2. 打印三个缓存在每一步的 size():画出缓存的生命周期 ✓✓✓;
  3. 给「工厂」加上"返回代理对象"的逻辑:理解三级缓存为什么必须存在(二级不够)✓✓✓;
  4. 对照 《Spring:AOP 与事务》 的代理失效实验:把 AOP 与缓存串起来 ✓✓。

9. 常见错误 ​

#错误说明
1通读源码 ✗✗没有问题的阅读没有终点 ✓✓
2只读不写复现 ✗✗✗「好像懂」在写的时候必崩 ✓✓
3把三级缓存背成口诀 ✗说不清「为什么二级不够」= 没懂 ✓✓
4忽略版本差异 ✗Spring 6 与 5 的实现细节有变,读前先确认版本 ✓
5不画链路图 ✗一周后全忘 ✓

10. 动手实验 ​

  1. 跑 MiniIocLesson 并逐步加日志(缓存 size)✓✓✓;
  2. 读 《Spring:AOP 与事务》 的实验源码,画出「调用 → 代理 → 目标」链路 ✓✓;
  3. 用 logging.level.org.springframework=DEBUG 跑一个最小 Spring 应用,看容器启动过程 ✓✓;
  4. 挑一个新的问题(如「@PostConstruct 什么时候执行」)走一遍三层阅读法 ✓✓✓。

验收方式:任选一个 Spring 机制,能在 30 分钟内给出「链路图 + 最小复现 + 版本差异」三件套。

扩展任务:把五个经典问题做成「Spring 源码阅读卡」(每卡:问题 / 关键类 / 链路图 / 复现要点)✓✓

11. 练习题 ​

  1. 三层阅读法是什么?为什么「最小复现」最有效?
  2. 三个缓存各放什么?为什么二级缓存不能替代三级?
  3. 构造器注入的循环依赖为什么解决不了?
  4. 问题清单驱动的好处?
  5. 版本差异为什么必须先确认?
参考答案(要点)
  1. 跑起来 → 观察 → 最小复现;复现强迫回答每个细节 ✓✓★
  2. 成品 / 已暴露引用 / 工厂;三级支持代理(早期引用可能是代理对象)✓✓★
  3. 实例都还没造出来,无从提前暴露 ✓✓★
  4. 一次一个问题,有终点、可验证 ✓✓★
  5. 实现细节随版本变化,读错版本会得出错误结论 ✓★

12. 掌握标准 ​

  • [ ] 会用三层阅读法 ★★★;
  • [ ] 能画出三级缓存的完整流程 ★★★;
  • [ ] 会写最小复现验证机制 ★★★;
  • [ ] 知道 Spring 的边界(构造器注入)★★;
  • [ ] 有「问题清单」而不是「通读计划」★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《MyBatis 与 Netty 源码阅读》 MyBatis 与 Netty 源码阅读 —— 换两种「内核风格」继续练。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引