主题
界面
主题
读源码不是「从头读文件」,而是「带着问题做实验」 —— 本节的实证:用约 80 行复现 Spring 的三级缓存,把抽象概念变成可运行的代码 ✓✓:
场景: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 ✓
★ 两种读源码的方式,效率差十倍 ✗✓:
| 通读式(低效)✗ | 问题驱动式(高效)✓✓ |
|---|---|
| 从入口类一路读下去,越读越晕 | 一次只回答一个问题 |
| 追求「读懂全部」 | 追求「能画出这条链路」 |
| 读完就忘 | 用最小复现验证,忘不掉 ✓✓✓ |
★★ 一句话 ✓✓:读源码的产物不是「读过的页数」,而是「一张链路图 + 一个可运行的复现」 ✓。
① 跑起来:跑既有实验([S3-007](../../03-enterprise/spring/S3-007) 的 IocLesson/AopLesson),观察现象
② 观察:开 debug 日志 / 打断点 / 用 Arthas 看调用栈([S6-011](../../06-java-deep/jvm/S6-011))
③ 最小复现:把机制用 50-100 行写出来 —— **能复现,才算真懂** ✓✓✓★ 为什么「最小复现」最有效 ✓✓✓:它强迫你回答每一个细节(缓存放什么、什么时候放、什么时候清)—— 读的时候「好像懂了」,写的时候才知道哪里不懂 ✓。
| 问题 | 关键类(Spring 源码) | 本知识库的复现 |
|---|---|---|
| 循环依赖怎么解决? | DefaultSingletonBeanRegistry 的三级缓存 | 本节 MiniIocLesson ✓✓ |
| Bean 的生命周期? | AbstractAutowireCapableBeanFactory.doCreateBean | 见 5.3 步骤清单 ✓ |
| AOP 代理什么时候生成? | AbstractAutoProxyCreator | 《Spring:AOP 与事务》 的失效场景实验 ✓ |
@Transactional 为什么失效? | 代理 + 自调用 | 《Spring:AOP 与事务》 实测 ✓✓ |
| 事务传播行为? | AbstractPlatformTransactionManager | 同上 ✓ |
★ 用法 ✓✓:一次挑一个问题,用三层阅读法走完 → 记一张链路图 → 下一个 ✓。
★ 三个缓存的职责(对应 Spring 的字段名) ✓✓✓:
| 缓存 | Spring 字段 | 放什么 | 为什么需要 |
|---|---|---|---|
| 一级 | singletonObjects | 成品 bean | 常规命中 |
| 二级 | earlySingletonObjects | 已提前暴露的引用 | 避免重复调用工厂(保证单例)✓ |
| 三级 | singletonFactories | 工厂(可能是代理) | 支持 AOP:早期引用要能是代理 ✓✓ |
★ 实测流程(节选) ✓✓✓:
实例化 a:A@7852e922(属性还没填)
addSingletonFactory(a) → 三级缓存注册工厂(提前暴露)
a.populateBean:需要 b → 递归 getBean
实例化 b:B@232204a1(属性还没填)
b.populateBean:需要 a → 递归 getBean
getSingleton(a):三级缓存命中 → 提前暴露的引用升入二级缓存
同一个 A 实例吗? true ✓✓(循环依赖被解开,且没有产生两个对象)★★ 关键结论 ✓✓✓:三级缓存的作用是「提前暴露未完成品」 —— 一级放成品、二级放已暴露引用、三级放工厂(支持代理) ✓; 而构造器注入的循环依赖无法解决(实例都造不出来,无从暴露)✗✓ —— 复现代码里已注释该分支。
| 工具 | 用途 |
|---|---|
| IDE 断点 + 调用栈 | 看「谁调用了谁」(F7/F8 有策略地走)✓ |
Spring 的 logging.level.org.springframework=DEBUG | 看容器启动的关键步骤 ✓ |
Arthas jad / trace | 线上/无源码时反编译与追踪(《Arthas 与线上问题诊断》)✓ |
| 写复现 | 最高效的「调试器」✓✓✓ |
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;
}一天一个问题(不贪多)→ 写链路图 → 写复现 → 讲给别人听(讲不清 = 没懂)✓✓把 doCreateBean 的注释抄到笔记里 ✗(一周后全忘)
正解:**自己写一个 80 行的版本**,再回去对照真实源码找差异 ✓✓✓代码:labs/java-deep/src/main/java/cn/kb/learn/deep/reading/MiniIocLesson.java
运行:
cd labs
java -cp java-deep/target/classes cn.kb.learn.deep.reading.MiniIocLesson实测输出(完整):
=== 场景: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 ✓✓(循环依赖被解开,且没有产生两个对象)size():画出缓存的生命周期 ✓✓✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 通读源码 ✗✗ | 没有问题的阅读没有终点 ✓✓ |
| 2 | 只读不写复现 ✗✗✗ | 「好像懂」在写的时候必崩 ✓✓ |
| 3 | 把三级缓存背成口诀 ✗ | 说不清「为什么二级不够」= 没懂 ✓✓ |
| 4 | 忽略版本差异 ✗ | Spring 6 与 5 的实现细节有变,读前先确认版本 ✓ |
| 5 | 不画链路图 ✗ | 一周后全忘 ✓ |
MiniIocLesson 并逐步加日志(缓存 size)✓✓✓;logging.level.org.springframework=DEBUG 跑一个最小 Spring 应用,看容器启动过程 ✓✓;@PostConstruct 什么时候执行」)走一遍三层阅读法 ✓✓✓。验收方式:任选一个 Spring 机制,能在 30 分钟内给出「链路图 + 最小复现 + 版本差异」三件套。
扩展任务:把五个经典问题做成「Spring 源码阅读卡」(每卡:问题 / 关键类 / 链路图 / 复现要点)✓✓
《MyBatis 与 Netty 源码阅读》 MyBatis 与 Netty 源码阅读 —— 换两种「内核风格」继续练。
暂无