版本基线:Spring Framework 7.0.8 (Spring Boot 4.x 底层核心框架)|最低 JDK 17,推荐 JDK 21/25|Jakarta EE 11|全文源码均以
v7.0.8为准
面试被问"三级缓存为什么是三级,两级行不行",十个能背八股的人九个会卡壳。如果你的状态也是"注解一贴、项目一跑、日志一红、开始玄学",这篇就是为你写的。作为 Spring 7.0.8 源码系列的准备篇,今天只聊三件事:为什么读、读什么、怎么读才不半途而废。
一、先自测:你的"会用但不懂"在第几层
先还原一个每天都在发生的面试现场:
面试官 :你项目里天天用
@Autowired,说说 Spring 是什么时候把值注入进去的?你 :呃......容器启动的时候?
面试官 :具体是哪一步?如果我
new出来的对象,@Autowired生效吗?你 :不生效......因为不是容器管的?
面试官 :那 A 依赖 B、B 依赖 A,为什么不会死循环?为什么是三级缓存 而不是两级?
你 :(开始疯狂眨眼)好像是有三个 Map......
面试官 :那
@Transactional在同一个类里this.method()调用,为什么失效?你:😅(笑容逐渐消失,心里开始算这个月房贷)
尴尬的不是"不会用",而是你的知识只有一层"注解 → 效果"的映射表。看看这张对照,中枪的请举手:
| 你会的(咒语层) | 你不知道的(原理层) |
|---|---|
加 @Component 就能被扫描 |
谁扫描的?扫成了什么对象? |
加 @Transactional 能回滚 |
回滚是谁干的?代理什么时候生成? |
@Autowired 能用 |
按类型还是按名字?多个实现怎么选? |
| 循环依赖能启动 | 凭什么能?什么情况下不能? |
@Configuration 能启动 |
配置类是怎么解析成 BeanDefinition 的? |
再来五道快速自检,中了 2 条以上,建议把本文读完:
- 配置报错全靠搜索,复制粘贴完依然不知道根因
- 能背出"IOC 控制反转、AOP 面向切面",但说不出底层实现
- 循环依赖、事务失效、Bean 创建失败,排查全靠瞎试
- 面中高级岗屡屡折戟,薪资卡在瓶颈期
- Spring Boot 4.x 用得很溜,但想做自定义扩展完全无从下手
我把 Spring 使用者分成三层,你可以对号入座:
| 层级 | 典型口头禅 | 遇到问题 | 面试官评价 |
|---|---|---|---|
| L1 会用 API | "百度一下吧" | 试错式修复 | 能用 |
| L2 懂机制 | "应该是代理没生效" | 定位到机制 | 靠谱 |
| L3 能改造 | "我写个 BeanPostProcessor 解决" | 从框架层解决 | 有潜力 |
读源码不是让你背代码,而是把你从 L1 的"试错"抬到 L2 的"定位",再往 L3 走。 这也是 P7+ 和架构岗的分水岭所在。
为什么会"会用但不懂"?别急着怪自己笨,这是三个结构性原因:
- 框架帮你屏蔽了太多 。你在
@RestController里写个方法,一个 HTTP 请求就变成一次方法调用,中间那几十个类你根本没见过。 - 你只学了"结果",没学"过程"。教程都教你"这样写就能跑",但没人告诉你这行代码在容器启动的第几步被执行,知识点在脑子里是一盘散沙。
- 缺乏一条主线索。IoC、AOP、事务、自动装配看起来是五块知识,其实是同一条流水线上的五个工位。
打个比方:不懂源码的人,像背了 100 道菜谱但从没进过厨房;懂源码的人,知道灶台怎么点火、锅怎么热,所以能自己"造菜"。
焦虑的本质一句话:你的知识是"黑盒知识"------知道输入和输出,中间那段是信仰。 而面试、线上故障、架构评审,考的恰恰是中间那段。读源码只做一件事:把信仰变成证据。
二、读源码到底换回什么:几笔实在的账
不喊口号,直接上"读前 vs 读后"对照表:
| 能力维度 | 只会用 Spring | 读懂 7.0.8 源码 |
|---|---|---|
| 问题排查 | 搜索拼关键词、反复试错、耗时几小时 | 看到 BeanCreationException 能顺着 cause chain 定位是哪一步 createBean 失败,分钟级解决 |
| 面试表现 | 停留在 API 用法,一问深就卡壳 | 能画出 refresh() 十二步、讲清三级缓存升级机制,越追问越稳 |
| 架构能力 | 按固定套路写业务代码 | 知道扩展点在哪、调用顺序是什么,敢写自定义 Starter 和基础组件 |
| 技术成长 | 3 年经验重复 3 次,沦为业务熟练工 | 建立完整技术体系,学 Boot 自动配置、Spring AI 都是"套娃" |
拆开说,有四笔账最划算:
1. 面试硬通货:从"背答案"到"能推导"。 初中级岗考用法,高级岗考原理。八股文决定你能不能进门,源码理解决定你能走多远。面试官随便换角度追问,你都能顺着主干往下讲------这种稳,背题背不出来。
2. 排障精准化:从"瞎试"到"秒定位"。 经典的"事务不回滚",懂源码的人一眼知道是同类调用绕过了代理;@Async 不生效、动态代理类型转换异常这些"玄学问题",源码里都有明明白白的答案。一个反常识:读源码最大的收益不是面试加分,而是你以后再也不怕线上故障了。 这种底气,比背一百道题都值钱。
3. 设计思想内化:免费上一门顶级框架设计课。 Spring 的设计模式密度,在整个 Java 生态里是天花板级别:
| 设计模式 | Spring 中的体现 |
|---|---|
| 模板方法 | AbstractApplicationContext#refresh()、JdbcTemplate、RestTemplate 全家桶 |
| 工厂 + 单例注册表 | BeanFactory、DefaultSingletonBeanRegistry 三级缓存 |
| 动态代理 | AOP、事务、@Async |
| 观察者 | ApplicationEvent + @EventListener |
| 责任链 | BeanPostProcessor、HandlerInterceptor、Filter 链 |
| 策略 | InstantiationStrategy(Simple vs CGLIB 实例化)、ResourceLoader |
| 门面 | ApplicationContext |
你写中间件、做基础组件时,脑子里自然有"参照物"。
4. 吃透 Spring Boot 4.x:从"使用者"变"掌控者"。 Boot 的自动配置、条件注解、Starter 机制,全部建立在 Framework 的 SPI、BeanDefinition 注册、容器扩展点之上。不懂源码,你永远只会用 Boot,不会定制 Boot。
不同角色读源码的"性价比"不一样,量力而行:
| 你的角色 | 阅读深度建议 | 核心收益 |
|---|---|---|
| 初级开发(1-3 年) | 读概念 + 跟踪主流程 | 破解"会用不懂"焦虑,面试加分 |
| 中级开发(3-5 年) | 精读 Bean 生命周期 + AOP + 事务 | 独立排查疑难 Bug,主导技术方案 |
| 高级/架构(5 年+) | 横向全读 + 设计模式提炼 | 中间件/基础组件设计,团队影响力 |
| 中间件/基础研发 | 深读扩展点(BeanPostProcessor 等) |
造轮子,框架级抽象能力 |
三、为什么偏偏是现在:7.0.8 是个分水岭版本
"源码什么时候不能读,非要现在?"因为 Spring 7 这一代不是小打小闹的更新,而是朝着现代化、模块化、云原生方向的一次架构级升级------老经验正在批量失效,新秩序刚刚成型,这正是读源码的黄金窗口期。

先看基线要求,"拿着旧地图找不到新大陆":
| 维度 | Spring Framework 7.0.8 的要求/变化 |
|---|---|
| JDK | 最低 17,官方推荐 21/25(LTS),实测兼容到 25 |
| 规范基线 | Jakarta EE 11(Servlet 6.1 / JPA 3.2 / Validation 3.1),Tomcat 11+、Jetty 12.1+ |
| 包名 | 彻底告别 javax.*,全面 jakarta.*,@PostConstruct、@Resource、@Inject 都要改 |
| 空安全 | 全面接入 JSpecify 1.0 ,替代自家 org.springframework.lang 注解,编译期 NPE 预警 |
| 模块移除 | 移除 spring-jcl,改用 Apache Commons Logging 1.3.0 |
| 代理策略 | 默认使用 CGLIB 代理 |
| 其他基线 | Kotlin 2.2、JUnit 6.0、Jackson 3.0(新包名 tools.jackson)、GraalVM 25 |
再看这一代的"大扫除"清单------很多老经验直接作废:
| 变更类型 | 具体内容 | 对你的影响 |
|---|---|---|
| 移除 | spring-jcl 模块 |
日志门面统一,别再找这个包 |
| 移除 | javax.annotation / javax.inject 支持 |
必须迁移 jakarta.* |
| 移除 | ListenableFuture、OkHttp3、Theme |
老代码升级需重构,ListenableFuture 改用 CompletableFuture |
| 变更 | HttpHeaders 不再继承 MultiValueMap |
老教程的 getHeaders().put(...) 写法编译不过,改用 set() / add() |
| 弃用 | RestTemplate(7.1 正式标 @Deprecated) |
新项目转向 RestClient / WebClient / @HttpExchange |
| 弃用 | Jackson 2.x(计划 7.2 完全移除) | 默认拥抱 Jackson 3.x |
| 弃用 | AntPathMatcher(PathPattern 全面接管)、JUnit 4 的 SpringRunner |
路由匹配统一;测试转 JUnit Jupiter |
⚠️ 初学者最容易踩的坑 :网上 80% 的源码博客还是 5.x 时代的。你在 7.0.8 里找不到对应的类或方法名,不是你的问题,是文章过时了 。本系列全程锁定
v7.0.8。
还有一个容易被忽略的亮点:7.0.8 本身是一个安全补丁密集的 patch 版本 ,修了 16 个 CVE,包括 SpEL 任意方法调用 RCE、AntPathMatcher 回溯 DoS、WebFlux Multipart DoS。patch 版本的 changelog 本身就是极好的源码阅读材料------它告诉你框架哪里曾经被攻破过。
与其漫无目的地读,不如从这些新特性切入------代码新、逻辑清晰、官方文档全:
BeanRegistrar:全新的编程式 Bean 注册机制,AOT 友好- JSpecify 空安全:泛型、数组、可变参数的空性都能标注
- 弹性内建 :
RetryTemplate、@Retryable、@ConcurrencyLimit收编进spring-core,不再依赖 Spring Retry - API 版本管理 :MVC / WebFlux 原生支持
@RequestMapping(version = "...") JmsClient:继JdbcClient、RestClient之后的第三块拼图- Java 24+ Class-File API:用 JDK 原生 API 读字节码,替代内置 ASM
- 后台初始化 Bean:耗时 Bean 丢到后台线程并行初始化,启动提速
这些新东西后面会逐个拆,先留个印象。
四、先看地图再出发:Spring 的骨架与核心角色
很多人读源码第一步就错了------打开仓库从第一行开始逐行读,结果读了一周还在 Resource 接口里打转,直接劝退。正确姿势是先看森林,再看树。
4.1 模块全景图
Spring 采用分层架构,核心容器是整个框架的地基。模块依赖和学习顺序一张图说清:

学习顺序别跳 :spring-core → spring-beans → spring-context → spring-aop → spring-tx → spring-webmvc。IoC 容器是最重要的核心技术,AOP 紧随其后,二者是源码学习的两大支柱。
4.2 BeanFactory 接口体系:接口隔离的教科书
正确的读法是先看接口层次,再看实现 。以最核心的 BeanFactory 体系为例:

关键理解 :Spring 把"管理对象"这件事按语义动词拆成了四个正交接口------取(get)、列(list)、层叠(delegate)、注入(wire)。这种接口隔离设计(ISP)是整个框架的地基。
顺带把面试高频题"为什么 ApplicationContext 比 BeanFactory 强"用第一性原理拆了。很多人的回答是"前者多了事件、i18n",这只答了是什么 ,没答为什么 。看 refresh() 就一目了然:BeanFactory 是惰性工厂 ------你不问我就不做;ApplicationContext 是主动容器------启动时把后置处理器、事件广播器、国际化这些基础设施全部就绪,还会预实例化所有非 lazy 单例。
💡 一句话结论 :业务代码 99% 用
ApplicationContext,只有在资源受限的嵌入式场景才退回裸BeanFactory。
4.3 核心角色先认脸
后面源码走读会反复碰到这几个角色,先混个脸熟:
| 角色 | 作用 | 面试关键词 |
|---|---|---|
BeanFactory |
Bean 容器最底层能力 | 获取、创建、管理 Bean |
ApplicationContext |
更高级的 IoC 容器 | 事件、资源、环境、国际化 |
BeanDefinition |
Bean 的"图纸"元数据 | 类名、作用域、懒加载、构造参数 |
BeanFactoryPostProcessor |
修改 BeanDefinition |
配置类解析、属性覆盖 |
BeanPostProcessor |
加工 Bean 实例 | AOP、属性填充、初始化增强 |
Environment |
环境与配置抽象 | Profile、PropertySource |
ApplicationEvent |
事件机制 | 容器刷新事件、启动事件 |
一句话记忆:BeanDefinition 描述 Bean,BeanFactory 管理 Bean,BeanPostProcessor 加工 Bean,ApplicationContext 对外提供完整容器能力。 注意:有了图纸不等于有对象,这是后面很多问题的题眼。
4.4 一个请求的生命周期
再看一个 HTTP 请求在 7.0.8 里走哪些模块,建立"请求视角":

地图有了,接下来正式进入主线------整个 Spring 的心脏。
五、抓住唯一的主线索:refresh() 十二步
初学者最大的误区是打开 spring-context 从上往下读。正确姿势是找到整个容器的"总指挥",顺着它往下追 ------就是 AbstractApplicationContext#refresh()。
为什么是它?因为不管是传统 Spring、Spring MVC 还是 Spring Boot,最终都会走到这一行。Boot 的 run() 方法绕了一大圈,落点还是 refreshContext()。
5.1 入口:三行代码背后的构造方法
先写个最小 Demo,后面所有断点都从它开始:
java
@Configuration
@ComponentScan("com.example")
public class AppConfig {
}
@Component
public class UserService {
public String hello() {
return "Spring 7.0.8 源码准备篇";
}
}
public class Main {
public static void main(String[] args) {
var ctx = new AnnotationConfigApplicationContext(AppConfig.class); // ① 启动
UserService service = ctx.getBean(UserService.class); // ② 取 Bean
System.out.println(service.hello()); // ③ 调用
ctx.close();
}
}
别小看这三行,它已经触发了 Spring 最核心的流程。先看①背后的构造方法,来自 spring-context 模块:
java
// Spring Framework 7.0.8 核心入口:AnnotationConfigApplicationContext
public class AnnotationConfigApplicationContext extends GenericApplicationContext
implements AnnotationConfigRegistry {
// 注解式Bean定义读取器:负责解析 @Bean、@Configuration 等注解
private final AnnotatedBeanDefinitionReader reader;
// 类路径Bean扫描器:负责扫描 @Component、@Service 等注解
private final ClassPathBeanDefinitionScanner scanner;
public AnnotationConfigApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
// 常用构造方法:传入配置类,直接启动IOC容器
public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
this();
register(componentClasses); // 注册成 BeanDefinition(注意:只是"图纸",不是对象!)
refresh(); // 刷新容器 ------ 12步核心流程全在这里
}
}
技术亮点提炼:
- 单一职责:容器本体、注解读取器、路径扫描器分工明确,互不耦合
- 模板方法 :
refresh()是父类定义的固定流程骨架,子类按需扩展- 开闭原则:新增配置方式只需扩展读取器/扫描器,无需修改容器核心代码
5.2 refresh() 全貌:7.0.8 版本
java
// org.springframework.context.support.AbstractApplicationContext (7.0.8)
@Override
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock(); // 7.x 用 ReentrantLock,老版本是 synchronized
try {
this.startupShutdownThread = Thread.currentThread();
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
prepareRefresh(); // 1 记时间、初始化占位符、校验配置
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 2 建工厂、加载 BeanDefinition
prepareBeanFactory(beanFactory); // 3 装 ClassLoader、内置 BPP、忽略依赖
try {
postProcessBeanFactory(beanFactory); // 4 子类扩展点(Web 环境在这改)
invokeBeanFactoryPostProcessors(beanFactory); // 5 ★ 解析 @Configuration/@ComponentScan
registerBeanPostProcessors(beanFactory); // 6 ★ 注册 AOP、@Autowired 的处理器
initMessageSource(); // 7 国际化
initApplicationEventMulticaster(); // 8 事件广播器
onRefresh(); // 9 子类钩子(Boot 在这启动 Tomcat)
registerListeners(); // 10 注册监听器
finishBeanFactoryInitialization(beanFactory); // 11 ★ 实例化所有非懒加载单例
finishRefresh(); // 12 发布 ContextRefreshedEvent
}
catch (RuntimeException | Error ex) {
destroyBeans(); // 出问题销毁已创建的单例,防止脏容器
cancelRefresh(ex);
throw ex;
}
finally { resetCommonCaches(); contextRefresh.end(); }
}
finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}
⚠️ 与旧版文章的一处差异 :启动锁从
synchronized (this.startupShutdownMonitor)换成了ReentrantLock,这是 6.2 以来锁体系重构的一部分。网上大部分文章还是老写法,对照时留意。
十二步记不住?压缩成三个阶段 + 三个关键帧:
text
第一阶段 · 立骨架 (1~3) : 建工厂 → 登记 BeanDefinition(设计图)
第二阶段 · 布机关 (4~6) : 跑 BFPP / 注册 BPP(把扩展点备好)
第三阶段 · 按图开工 (7~12) : 实例化所有单例 Bean → 发布事件

记住这个分界线:前 6 步都在"准备",第 11 步才是真正"造 Bean"。 所有关于 Bean 生命周期、循环依赖、AOP 代理的机制,全都在第 11 步内部上演。理解这一点,你的知识就串起来了。
这三步的顺序感,就是你和普通候选人的分水岭:
| 步骤 | 关键动作 | 面试常考 |
|---|---|---|
⑤ invokeBeanFactoryPostProcessors |
解析配置类、生成 BeanDefinition |
@Import、ImportBeanDefinitionRegistrar、@Conditional 何时生效 |
⑥ registerBeanPostProcessors |
只注册,不执行 | 为什么 BPP 要最早实例化、会触发哪些 Bean 提前初始化 |
⑪ finishBeanFactoryInitialization |
真正 getBean() → 创建 → 注入 → 代理 |
@Transactional 何时生效、循环依赖怎么解 |
🌟 面试金句 :"
refresh()是模板方法模式的教科书实践------父类定死流程骨架,把可变的细节交给onRefresh()、postProcessBeanFactory()这样的钩子。"Spring Boot 之所以能"悄无声息"地启动内嵌 Tomcat,就是因为它重写了onRefresh()。
顺带解答一个高频疑惑:@Configuration 为什么不加 @Component 也能被扫到? 因为它是 @Component 的派生注解 ,由第 5 步的 ConfigurationClassPostProcessor 专门解析。而 @Bean 方法默认单例,是因为配置类被 CGLIB 增强 ,方法调用被拦截后走的是 beanFactory.getBean()------这些细节到启动篇逐行拆。
六、Bean 的一生:从 getBean 到成品
第 11 步内部上演的,就是 Bean 的完整生命周期。这条调用链是核心中的核心:

doGetBean 的"三步走"极度简化版:
java
// org.springframework.beans.factory.support.AbstractBeanFactory(简化)
protected <T> T doGetBean(String name, ...) {
// 第 1 步:先查三级缓存体系,命中直接返回(性能关键)
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null) {
return adaptBeanInstance(name, sharedInstance, requiredType);
}
// 父容器委托、depends-on 校验、scope 分支(singleton / prototype / request...)
// 第 2 步:没命中 → createBean() → doCreateBean()
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args); // 真正的创建
} catch (BeansException ex) { destroySingleton(beanName); throw ex; }
});
}
// 第 3 步:创建完成 → 放入一级缓存 singletonObjects
return adaptBeanInstance(name, sharedInstance, requiredType);
}
createBean 内部记住三个词,面试够用 80%:
text
resolveBeforeInstantiation → 有机会直接返回一个代理(InstantiationAwareBPP 短路)
↓
doCreateBean → createBeanInstance(推断构造器,new 出半成品)
→ addSingletonFactory(提前暴露,解循环依赖)
→ populateBean(属性填充、@Autowired 注入)
→ initializeBean(Aware → before → init → after ← AOP 代理产出)
↓
registerDisposableBeanIfNecessary
6.1 代理短路:Spring 的"上帝接口"
java
// AbstractAutowireCapableBeanFactory#createBean(7.0.8 节选)
try {
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean; // 直接返回代理,连 doCreateBean 都不进!
}
}
...
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
🌟 技术亮点 :看懂这一段,你就知道
BeanPostProcessor为什么被称为 Spring 的"上帝接口"------InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation能在对象出生前把整个 Bean 换掉。
6.2 doCreateBean:先记住一个 JVM 前提
text
new 对象 = ① 分配堆内存(属性全为默认值,即"半成品")
+ ② 执行构造 / 赋值(成为"成品")
这两个阶段可以分开! ← Spring 解决循环依赖的全部秘密就在这
java
// AbstractAutowireCapableBeanFactory#doCreateBean(7.0.8 节选)
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
// ① 实例化:JVM 分配内存,得到一个"半成品"(属性还是默认值)
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
Object bean = instanceWrapper.getWrappedInstance();
// ② 是否提前暴露?单例 + 允许循环依赖 + 正在创建中
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// ③ 把 ObjectFactory 塞进三级缓存
// 关键:放"工厂"而不是"对象本身",才能在真正被依赖时才决定是否生成 AOP 代理
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// ④ 属性填充:@Autowired / @Value 在这里注入,循环依赖就在这一步被打通
populateBean(beanName, mbd, instanceWrapper);
// ⑤ 初始化:Aware 回调 → @PostConstruct → InitializingBean → AOP 代理
Object exposedObject = initializeBean(beanName, bean, mbd);
return exposedObject;
}
@Autowired 到底在哪一步生效? 面试里能这样答,面试官会眼前一亮:
依赖注入发生在
AbstractAutowireCapableBeanFactory#populateBean()里。Bean 实例化之后,会遍历InstantiationAwareBeanPostProcessor,其中AutowiredAnnotationBeanPostProcessor负责处理@Autowired标注的字段和方法。它根据BeanDefinition中缓存的依赖信息,调用beanFactory.resolveDependency()解析依赖,最终通过getBean()拿到目标 Bean 后,用反射field.set()注入进去。如果遇到循环依赖,会走三级缓存提前暴露引用。
6.3 initializeBean:Bean 不是 new 出来就完事
java
// AbstractAutowireCapableBeanFactory#initializeBean(简化)
protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
// 1. 处理 Aware 接口
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
// 2. BeanPostProcessor 前置处理
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
// 3. 执行初始化方法(@PostConstruct / init-method)
invokeInitMethods(beanName, wrappedBean, mbd);
// 4. BeanPostProcessor 后置处理
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
Spring 最厉害的地方不是"创建对象",而是把对象创建过程做成了可扩展流水线 :Aware 回调 → Before 处理 → 初始化方法 → After 处理,最终返回的可能是代理对象。AOP 代理、事务代理,大多发生在 postProcessAfterInitialization。
6.4 AOP 代理的诞生时刻
Spring 在初始化后阶段,通过 AbstractAutoProxyCreator(一个 BeanPostProcessor)决定要不要生成代理:接口类型走 JDK 动态代理,类类型走 CGLIB 字节码增强(7.0 默认 CGLIB)。
java
@Aspect
@Component
public class LogAspect {
// Spring AOP 用动态代理拦截,通知链按 @Order 顺序织入
@Around("execution(* com.demo.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 必须调用,否则目标方法不执行
log.info("{} 耗时 {}ms", pjp.getSignature(), System.currentTimeMillis() - start);
return result;
}
}
同一切面内的通知执行顺序,背下来直接用:
text
@Around 前 → @Before → 目标方法 → @Around 后 → @After → @AfterReturning/@AfterThrowing
🌟 技术亮点 :Spring AOP 通过 Advisor 链式织入,在
ReflectiveMethodInvocation中完成环绕通知的嵌套执行,责任链模式与模板方法模式的混合运用堪称教科书级。
代理对象"看不见"怎么办?打印 AopUtils.isAopProxy(bean) 和 bean.getClass(),看到 EnhancerBySpringCGLIB 字样就说明这是代理------这一步是排查一切"切面没执行"问题的第一现场。
串一下这条链路:refresh() 第 11 步 → getBean() → doCreateBean() → createBeanInstance()。从"总指挥"到"造一个 Bean"一条线走通,你的 Spring 世界观就立住了。下一节,进入面试重灾区。
七、三级缓存与循环依赖:面试重灾区全拆解
这是全网被问最多、也最容易讲错的源码。先看 DefaultSingletonBeanRegistry 的三张 Map------注意,7.0.8 的真实长相和网上 5.x 文章不一样:
java
// DefaultSingletonBeanRegistry(v7.0.8 字段节选)
/** Common lock for singleton creation. */
final Lock singletonLock = new ReentrantLock();
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级:成品
/** Creation-time registry of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16); // 三级:工厂
/** Custom callbacks for singleton creation/registration. */
private final Map<String, Consumer<Object>> singletonCallbacks = new ConcurrentHashMap<>(16); // 7.x 新增
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 二级:半成品
| 缓存 | 字段名 | 存的是什么 |
|---|---|---|
| 一级 | singletonObjects |
完整成品 Bean(走完整个生命周期) |
| 二级 | earlySingletonObjects |
提前暴露的半成品 Bean(可能已织入 AOP 代理) |
| 三级 | singletonFactories |
ObjectFactory 工厂(延迟决定要不要造代理) |
与网上 5.x 文章的两处差异(面试加分点):
- 锁从
synchronized (this.singletonObjects)换成了ReentrantLock singletonLock(6.2 起的单例锁重构); - 早期引用推断前先
tryLock(),拿不到锁直接返回null,避免非创建线程"偷看"半成品:
java
protected @Nullable Object getSingleton(String beanName, boolean allowEarlyReference) {
// 1. 先查一级缓存(成品),不加锁的快速路径
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 2. 查二级缓存(早期引用)
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
if (!this.singletonLock.tryLock()) {
// Avoid early singleton inference outside of original creation thread.
return null;
}
try {
// 双重检查后,3. 查三级缓存,通过 ObjectFactory 创建早期引用
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 4. 升级到二级缓存,删除三级
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
finally { this.singletonLock.unlock(); }
}
}
return singletonObject;
}
缓存升级机制:三级 → 二级 → 一级,引用在不同阶段"搬家",最终二、三级被清理,只留一级。
7.1 高频追问:为什么是三级,两级不够用吗?
如果你只背了"一级存成品、二级存半成品、三级存工厂",面试官一个追问就露馅。答案的关键在 AOP 代理:
🌟 为什么三级缓存存的是
ObjectFactory而不是"早期对象"? 因为 Spring 需要延迟决定 这个早期引用要不要是 AOP 代理。代理对象的创建时机必须推迟到"确定有人需要"时才执行------毕竟可能根本没有循环依赖,过早创建代理是浪费。如果二级缓存直接放对象,就等于绕过了SmartInstantiationAwareBeanPostProcessor,AOP 就废了。这一句话,能直接把你和其他候选人拉开差距。
循环依赖的完整时序:

7.2 构造器注入为什么无解?
addSingletonFactory 发生在 populateBean 之前 ------即"对象刚 new 出来、属性还没填"时就把引用交出去了。而构造器注入 在 createBeanInstance 阶段就要拿到对方,此时连"半成品"都还没有,只能抛 BeanCurrentlyInCreationException。
⚠️ 注意 :三级缓存只能解决 setter / 字段注入的循环依赖。构造器注入的解法是:① 加
@Lazy注入一个代理先顶着;② 改成 setter/字段注入让三级缓存兜底;③ 彻底根治是重构消除循环------抽公共服务、改事件驱动。另外 7.x + AOT 模式下显式循环依赖不被支持,别指望缓存永远救你。
想建立因果确信,可以玩一下反证法:
java
ctx.getBeanFactory().setAllowCircularReferences(false); // 马上循环依赖报错
亲眼看到开关一关程序就崩,比背十遍八股文都管用。
八、@Transactional 失效:一个 this 引发的血案
理解了代理,"事务失效"这类玄学问题就全通了。声明式事务基于 AOP:TransactionInterceptor 拦截 @Transactional 方法,负责开启、提交、回滚。而同类内部方法调用走的是 this,是原始对象,不经过代理------拦截器根本没机会执行:
java
@Service
public class OrderService {
public void pay() {
this.createOrder(); // this 调用,代理失效,事务不生效!
}
@Transactional
public void createOrder(Order order) {
// 数据库操作
}
}
三种修复方案:
java
// 方案一:抽到另一个 Bean(最推荐,顺便解耦)
// 方案二:AopContext.currentProxy().createOrder(order); // 需开启 exposeProxy
// 方案三:自注入 @Lazy private OrderService self; 然后 self.createOrder(order);
"事务失效"四大场景,一个原理通杀:
- 自调用 ------
this绕过代理; - 方法非 public------代理增强有可见性要求;
- 异常被吞------catch 掉了异常,拦截器以为成功;
- 数据库引擎不支持事务------比如 MyISAM。
再深一层:事务与数据库连接的绑定靠 ThreadLocal + ConnectionHolder,嵌套事务用 SAVEPOINT 实现。理解了这个,传播行为那些参数就不再是天书。
🎯 举一反三 :这一条理解透了,
@Async自调用失效、@Cacheable自调用失效全部通杀------它们和事务一样,都是代理增强,都怕this。
九、7.0 新特性预览:读源码的最佳切入口
与其漫无目的地读老代码,不如从新特性切入------代码新、包袱少、官方文档全。下面是 7.0.8 里真实存在、且极具学习价值的几段代码,也是后续精读的"预告片"。
9.1 BeanRegistrar:编程式注册的新范式
传统 @Bean 方法限制很多:一个方法只能注册一个 Bean、返回类型必须具体。Spring 7.0 引入的 BeanRegistrar 直接解放了这些约束:
java
// spring-beans 模块,7.0 新增
@FunctionalInterface
public interface BeanRegistrar {
/**
* Register beans programmatically using the provided BeanRegistry
* and Environment.
*/
void register(BeanRegistry registry, Environment environment) throws Exception;
}
使用姿势(两种):
java
// 方式一:直接实现
public class MyRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean("myService", MyService.class);
}
}
// 方式二:Lambda(函数式接口的最大优势)
@Bean
public BeanRegistrar myRegistrar() {
return (registry, env) -> {
registry.registerBean(MyService.class,
spec -> spec.supplier(MyService::new));
};
}
对比一下传统方式:@Bean 方法受配置类约束、ImportBeanDefinitionRegistrar 接口较重、BeanDefinitionRegistryPostProcessor 更底层。BeanRegistrar 用函数式接口大幅简化了动态注册场景(if/for 批量注册),并且天然支持 AOT 编译------这是 Spring 在 XML/注解之外开辟的"第三条注册路径"。
🌟 面试视角 :读源码不仅要知道"老 API 怎么实现的",还要关注新版本为什么引入新设计。面试官听到这个,会立刻判断你在持续跟进技术演进。
9.2 SpEL 操作数熔断:7.0.8 的安全加固
先看一个真实的线上坑:
java
// 一个真实的坑:SpEL 表达式过复杂,7.0.8 直接熔断
org.springframework.expression.spel.SpelEvaluationException:
The number of operations performed during evaluation
of the SpEL expression exceeds the limit of 10000
从 7.0.8 起,SpEL 求值时的操作数默认上限是 10,000,超过即抛异常中断求值。源码层面长这样:
java
// SpelParserConfiguration 里的新属性
public class SpelParserConfiguration {
private final int maximumAutoGrowSize; // 老的自动增长限制
private final int maxOperations; // 新增:求值操作数上限,默认 10000
// 可通过系统属性 spring.expression.maxOperations 覆盖
public static final String MAX_OPERATIONS_PROPERTY_NAME =
"spring.expression.maxOperations";
}
🌟 学习点:防御性编程的教科书案例------限制单次求值的操作数,防止恶意或失误的复杂表达式拖垮 CPU,DoS 攻击面收窄。你设计自己的 DSL 时完全可以借鉴。
实战经验 :如果你真撞了这个异常,别急着调高上限------表达式复杂到这个程度,说明逻辑不该写在 SpEL 里,搬回 Java 代码。同时排查全局 SpelExpressionParser 和 @Value("#{...}") 有没有掺用户输入,这正是 CVE-2026-41852(SpEL 任意方法调用 RCE)的根因所在。
9.3 @Retryable 弹性内建:从外部依赖到核心能力
以前要用 Spring Retry 这个独立项目,现在 spring-core 直接内置(org.springframework.core.retry 包):
java
// org.springframework.core.retry 包下的核心抽象
public class RetryTemplate {
// 执行带重试逻辑的操作
public <T> T execute(RetryCallback<T> retryCallback,
RecoveryCallback<T> recoveryCallback) throws Throwable {
// 状态机驱动:根据 RetryPolicy 决定是否继续
}
}
// 注解驱动,自动适配响应式方法(Reactor retry)
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public Mono<Order> processOrder(OrderRequest req) { ... }
🌟 学习点 :响应式方法自动用 Reactor 的
retry()装饰管道,普通方法走RetryTemplate------同一套注解、两条执行路径 ,这是"适配器模式"在真实框架里的落地。配套的还有虚拟线程时代的限流注解@ConcurrencyLimit,以及统一开关@EnableResilientMethods。
9.4 其他值得盯住的新东西
- API 版本管理 :
@GetMapping(version = "1.2+")原生支持,内置 4 种解析策略,是学HandlerMapping定位逻辑的绝佳案例; - JSpecify 空安全 :
@NullMarked默认非空、@Nullable显式可空,配合 NullAway 在编译期 拦截 NPE。读源码时看到满屏org.jspecify.annotations.Nullable直接忽略,与业务逻辑无关; - Java 24+ Class-File API:用 JDK 原生 API 读字节码,替代内置 ASM;
JmsClient:继JdbcClient、RestClient之后的第三块拼图。
9.5 两个很多人还不知道的性能大招
后台初始化 Bean:7.0.8 支持把耗时 Bean 丢到后台线程并行初始化:
java
// DefaultListableBeanFactory#preInstantiateSingleton(v7.0.8 节选)
private @Nullable CompletableFuture<?> preInstantiateSingleton(String beanName, RootBeanDefinition mbd) {
if (mbd.isBackgroundInit()) {
Executor executor = getBootstrapExecutor();
if (executor != null) {
// depends-on 的 Bean 强制在主线程初始化
String[] dependsOn = mbd.getDependsOn();
...
// 当前 Bean 丢到后台线程序例化
CompletableFuture<?> future = CompletableFuture.runAsync(
() -> instantiateSingletonInBackgroundThread(beanName), executor);
addSingletonFactory(beanName, () -> {
try { future.join(); }
catch (CompletionException ex) { ReflectionUtils.rethrowRuntimeException(ex.getCause()); }
return future;
});
return (!mbd.isLazyInit() ? future : null);
}
...
}
if (!mbd.isLazyInit()) {
instantiateSingleton(beanName);
}
}
🌟 技术亮点 :后台 Bean 尚未就绪时,往三级缓存里塞的不是对象,而是一个
future.join()的阻塞门闩------谁先用到它,谁就等它跑完。这就是"并行启动 + 按需等待"的优雅实现。
AOT 生成的 Bean 定义 :7.x 的方向是把能提前做的事挪到构建期。AOT 处理后,你会在 target/generated-sources/spring-aot/ 里看到类似这样的代码:
java
// 生成产物示意(真实类名是 xxx__BeanDefinitions)
class OrderService__BeanDefinitions implements BeanFactoryInitializationAotContribution {
@Override
public void applyTo(ConfigurableListableBeanFactory beanFactory) {
RootBeanDefinition mbd = new RootBeanDefinition(OrderService.class);
// 不再反射找构造器:直接绑定到生成的实例化 Supplier
mbd.setInstanceSupplier(() -> new OrderService(
beanFactory.getBean(OrderRepository.class)));
beanFactory.registerBeanDefinition("orderService", mbd);
}
}
这段代码信息量极大:
- 构造器注入之所以在 7.x 是"唯一正解",不是规范洁癖,而是只有构造器信息能在构建期被静态确定;字段注入必须靠运行时反射,AOT 无法优化;
@Conditional/@Profile在 AOT 模式下构建期就被固化,运行时改 profile 无效;- Lambda / 方法引用定义的 Bean、
registerSingleton注册的实例,AOT 无法转换。
这就是"为什么要读 7.0.8 而不是随便找个 5.x 教程"的答案:底层机制已经换了发动机。
十、怎么读才不劝退:方法论与避坑指南
内容讲完,方法同样值钱。新手读 Spring 源码最大的问题不是"看不懂",而是"不知道该看什么,打开 GitHub 就关掉了"。
10.1 先看四个最常见的放弃姿势
| 踩坑 | 后果 |
|---|---|
| 从头到尾逐行读 | 一个月还在 refresh() 里,放弃 |
| 没有目标随便翻 | 读完就忘,不成体系 |
| 只看不画图 | 脑子里一团浆糊,面试还是说不清 |
| 上来就看最新最全的 | 被复杂的类层次劝退 |
10.2 三步阅读法(本系列通用方法论)
text
① 主链路法 先跑通"启动 → 取 Bean"这一条线,所有 if 分支视为不存在
② 断点验证法 先猜("我认为注入发生在 populateBean")→ 打断点 → 被打脸 or 被证实
③ 反证法 改一个开关看现象变化,比如关掉 setAllowCircularReferences(false)
配套心法:
- 先粗读,再精读 :第一遍用
step over而不是step into,把调用链的层次和顺序搞清楚;第二遍再对核心方法精读; - 带着问题读 :"
@Autowired到底在哪里生效?"带着问题追,比从头读效率高十倍; - 先接口后实现 :先读
BeanFactory接口层次,再看DefaultListableBeanFactory; - 只看带生命周期的类 :名字带
Factory、Processor、Context、Resolver的优先,StringUtils、ReflectionUtils这类纯工具类先跳过; - 输出倒逼输入(费曼学习法) :每读完一个核心流程,写一篇笔记、画一张图、讲给同事听。讲不出来 = 没读懂。
10.3 三遍读法
- 第一遍:跑起来 。目标只有一个------让代码跑起来,知道入口在哪。断点打在
AbstractApplicationContext#refresh,不要试图看懂每一行。 - 第二遍:跟一个 Bean 的一生 。只跟一个
UserService:什么时候被扫描?什么时候变成BeanDefinition?什么时候实例化、注入、初始化?最终容器里存的是原始对象还是代理对象? - 第三遍:专题突破。按专题逐个击破,每个专题只解决一个问题:
text
1. IoC 容器启动 2. BeanDefinition 注册 3. Bean 生命周期
4. 依赖注入 5. 循环依赖 6. AOP 代理
7. 事务管理 8. 事件机制 9. Environment 配置抽象
10. Spring Boot 自动装配
10.4 命名规律:省一半力气
| 模式 | 含义 | 例子 |
|---|---|---|
doXxx() |
真正干活的(不重写它,你改不动) | doGetBean / doCreateBean |
xxx() |
外壳:做校验、缓存、异常包装 | getBean / createBean |
postProcessXxx |
钩子回调(扩展点) | postProcessProperties |
口诀:BeanPostProcessor 是钩子,BeanFactoryPostProcessor 是图纸加工 。看到 postProcess 开头就知道是扩展点。
10.5 避坑清单:这些坑我替你踩过了
- 不要从
main分支读 ------今天看的逻辑明天可能被重构,一定git checkout v7.0.8; - 不要一上来就读 Boot 自动配置 ------那是"第二层魔法",先把
refresh()十二步吃透,自动配置只是BeanFactoryPostProcessor的一个应用; - 不要试图一次读懂所有 scope 和所有回调 ------第一轮只看 singleton + 构造器注入 +
@Autowired+@PostConstruct,其余标记为"待办"; - 不要只看不写 ------每读完一节,自己写一个
BeanFactoryPostProcessor/BeanPostProcessor/ImportBeanDefinitionRegistrar验证猜想,动手一次胜过看十遍; - 不要忽视测试代码 ------
spring-beans/src/test里的用例是官方写的"最佳使用文档",比任何博客都准; - 不要忽略 changelog------7.0.8 这种 patch 版本的修复列表,是最好的"框架边界在哪里"的教材。
10.6 路线图:四个阶段,别跳级
把各种"三个月计划""四周计划"合并成一张表,照着走:
| 阶段 | 时间建议 | 目标 | 验收标准 |
|---|---|---|---|
| ① 会用 | 第 1 周 | 熟练注解驱动开发 | 能独立搭建 Spring Boot 项目 |
| ② 懂概念 + 读入口 | 第 2 周 | 跟踪容器启动主流程 | 不看笔记讲清 IoC/AOP/生命周期,画出 refresh() 十二步 |
| ③ 攻核心 | 第 3 周 | 吃透 Bean 创建、三级缓存、代理 | 手写简化版 IoC 容器,能解释"为什么"级别的问题 |
| ④ 横向扩展 + 输出 | 第 4 周 | 事务传播、事件机制、MVC 分发 | 写文章、画图、做分享,面试降维打击 |
每周交付物:一张你自己的手绘流程图 + 一个能跑的验证 demo + 一段能讲给同事听的 3 分钟口述。
🚫 劝退提示 :不要从第 ③ 阶开始。没有第 ② 阶的主干做骨架,机制你记住了也串不起来。一个简单的自测:如果你还不能徒手画出 Bean 从
BeanDefinition到成品的完整链路,那就从阶段 ② 开始。
本系列后续的更新路线也放这里,收藏不迷路:
| 篇章 | 内容 |
|---|---|
| 启动篇 | refresh() 十二步逐行拆解 + 断点地图 |
| IoC 篇 | Bean 的一生:从 BeanDefinition 到 destroy() |
| AOP 篇 | 代理与通知链:CGLIB / JDK 动态代理 |
| 事务篇 | @Transactional 的传播与失效 |
| MVC 篇 | DispatcherServlet 与一次请求的完整链路 |
| Boot 篇 | 自动配置原理:条件注解与 Starter |
| 实战篇 | 面试高频题与简历表达 |
这个系列会持续更新,目标是从"会用"到"懂原理"陪你把 Spring 7.0 的核心模块啃下来。感兴趣的话点个关注,别让它躺在收藏夹里吃灰。
十一、环境准备:30 分钟搞定,别拖
11.1 硬性要求
| 项 | 要求 | 说明 |
|---|---|---|
| JDK | 17(最低)/ 21 或 25(推荐) | 虚拟线程、Record、Sealed Class 要 21+ |
| IDE | IntelliJ IDEA(社区版足够) | Ctrl/Cmd + B、Ctrl + Alt + H 调用层级是最好的朋友 |
| 内存 | IDEA 堆内存建议 -Xmx4g |
源码索引吃内存,不给够会卡到你放弃 |
| Gradle | 用项目自带 wrapper | ./gradlew,别自己装 |
| Git | 需要 | 切 tag 用 |
11.2 拉代码:一定要切 tag
bash
# 方式 A:只读不编译(推荐入门)
mvn dependency:sources # 或 IDE 里点 "Download Sources"
# 方式 B:全量 clone(想自己改代码 + git blame 看演进)
git clone https://github.com/spring-projects/spring-framework.git
cd spring-framework
git checkout v7.0.8 # 必须锁定版本,main 分支天天变
./gradlew :spring-context:compileJava --no-daemon
💡 强烈建议 :不要直接 import 整个大仓库。先在 IDE 里只导入
spring-beans+spring-context两个子模块,跑通第一条线,再逐步加spring-aop、spring-webmvc。贪多是放弃的第一原因。 入门阶段也不要编译整个仓库,用 Maven 依赖 +mvn dependency:sources即可断点调试。
11.3 建一个"最小可调试"工程
别用公司那坨 200 个 starter 的项目。学习时强烈建议脱离 Spring Boot ------Boot 的自动配置会给你加 100+ 个 BeanPostProcessor,断点一打全是 Boot 的栈,根本看不到 Spring 本体。
裸框架最小依赖,最干净的学习环境:
xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>7.0.8</version>
</dependency>
如果你非要带着 Boot 玩,起一个干净的:
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version> <!-- 内置 Spring Framework 7.0.8 -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
</dependencies>
然后在 IDEA 里:File → Project Structure → Modules → 把 spring-beans / spring-context 的源码目录关联进来 (或者更粗暴有效:File → New → Module from Existing Sources 把 spring-framework 作为依赖模块挂进来)。这样你就能在框架代码里直接下断点、单步进去、看变量。
✅ 验证成功的标志 :你能在 AbstractApplicationContext.refresh() 第一行打断点,启动时停住,并且能看到调用栈。到这一步,准备篇就结束了 80%。
开工前的三样"装备"再叮嘱一遍:一份能跑的源码、一个会跳转的 IDE、一张贴在显示器旁边的 refresh() 十二步主干图------读源码时随时对位置,这招极其好用。
十二、热身实验:30 行手写迷你 IOC
强烈建议手敲。不亲手踩过坑,你看 addSingletonFactory 永远没有感觉:
java
public class MiniIoc {
private final Map<Class<?>, Object> beans = new ConcurrentHashMap<>();
public void register(Class<?>... types) throws Exception {
// ① 先把所有对象 new 出来(对应 createBeanInstance)
for (Class<?> t : types) {
beans.put(t, t.getDeclaredConstructor().newInstance());
}
// ② 再统一注入属性(对应 populateBean)
for (Object bean : beans.values()) {
for (Field f : bean.getClass().getDeclaredFields()) {
if (f.isAnnotationPresent(Autowired.class)) {
f.setAccessible(true);
f.set(bean, beans.get(f.getType()));
}
}
}
}
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) { return (T) beans.get(type); }
}
为什么必须分 ① ② 两步? 因为如果边 new 边注入,A 要 B、B 又要 A 时,B 还没进 map,直接死循环。这正是 Spring 三级缓存要解决的问题------你亲手踩过这个坑,再看 addSingletonFactory 就会拍大腿:原来如此。
十三、面试弹药库:高频题与答题模板
13.1 高频 5 问(一句话版,正文前面都已展开)
| 问题 | 一句话答案 |
|---|---|
BeanFactory 和 ApplicationContext 区别? |
BeanFactory 是"工厂",只管造;ApplicationContext 是"容器",多了事件、国际化、AOP、自动注册后置处理器,且默认 eager 初始化单例 |
| Bean 的生命周期? | 实例化 → 属性注入 → Aware → BeanPostProcessor#before → init → after(AOP 在这)→ 使用 → 销毁 |
什么是 BeanDefinition? |
Bean 的"图纸":class、scope、是否懒加载、构造参数......有了图纸不等于有对象 |
| 循环依赖怎么解决? | 三级缓存:成品池 / 半成品池 / 工厂池,关键是在属性注入之前提前暴露引用 |
| 构造器注入为什么不能循环依赖? | 对象还没 new 出来,无法提前暴露引用,只能抛 BeanCurrentlyInCreationException |
13.2 答题模板:30 秒版
我读 Spring 源码,主要是为了解决"会用但不懂"的问题。以前只知道
@Autowired、@Transactional能用,但遇到事务失效、循环依赖、Bean 初始化异常时只能猜。后来我以refresh()为主线,把容器启动、BeanDefinition注册、Bean 生命周期、AOP 代理串起来,再排障就有依据了。
13.3 答题模板:2 分钟版
我觉得读 Spring 源码不是为了背细节,而是为了建立排障和扩展的能力。
比如 Spring 启动的核心是
AbstractApplicationContext#refresh(),它通过模板方法定义了容器刷新流程:准备环境、创建 BeanFactory、执行BeanFactoryPostProcessor、注册BeanPostProcessor、初始化单例 Bean。Bean 的创建也不是简单
new,而是经过实例化、属性注入、Aware 回调、初始化前后处理等流程。AOP 和事务代理很多时候就是在BeanPostProcessor的后置处理阶段完成的。所以读源码之后,我对事务失效、循环依赖、注入失败这些问题不再是背答案,而是能根据生命周期去定位问题。在 Spring Framework 7.0.8 和 Spring Boot 4.x 的场景下,Boot 更多是自动装配和工程化封装,底层仍然是 Framework 的 IoC、AOP、Environment、事件和生命周期机制。
13.4 闭环测试:回到开头那个问题
"
@Transactional怎么生效的?""它在
refresh()的第 11 步,由AbstractAutoProxyCreator(一个BeanPostProcessor)在postProcessAfterInitialization里,用ProxyFactory根据TransactionAttributeSource算出的 Advisor 包装出来的。如果方法是自调用、非 public、或者 Bean 没被代理,就不会生效。"
这时候你不是在背答案,你是在复述你亲眼见过的事实。
13.5 常见焦虑 Q&A
Q1:直接读 Spring Boot 4 的源码行不行? 建议先读 spring-framework,再读 spring-boot。Boot 是"装配层",Framework 是"内核层"。跳过内核直接读装配,你会看到一堆 @ConditionalOnClass 却不知道底层在判断什么。
Q2:要不要用 Debug 断点一步步跟? 要,但别一上来就无脑打断点。先读 refresh() 的整体结构,有全局认知后再针对某个环节 Debug,配合条件断点(如 beanName.equals("userService"))。否则跟到第 20 层调用栈就迷路了。另外方法断点尽量别用,极慢。
Q3:英文文档看不懂怎么办? 7.0 的 Release Notes 和 Reference Documentation 是必读材料,技术英文词汇量其实很有限,配合翻译工具啃两周就能适应。看二手翻译永远慢一步,一手信息才是竞争力。
Q4:读源码会不会过时? 框架会变,但设计思想不会 。三级缓存、模板方法、事件机制这些抽象,从 Spring 2.5 时代活到现在,还会继续活下去。7.0 移除了 ListenableFuture、替换了 Jackson 2,但 Bean 生命周期的骨架几乎没动。
写在最后
把全文压缩成三句话:
- 读源码不是为了背代码,是为了建立"对象 → 流程"的心智模型,把 L1 的试错升级成 L2 的定位;
- 入口就一个 :
AbstractApplicationContext#refresh(),抓住 5 / 6 / 11 三个关键帧; - 方法就三步:主链路 → 断点验证 → 反证法,手敲 30 行迷你 IOC,你会回来感谢我。
落地建议,今天就能做的三件事:
git checkout v7.0.8,在refresh()第一行打上断点,看到调用栈停住的那一刻,你就赢了 80% 的人;- 把十二步主干图手绘一遍,贴在显示器旁边;
- 想一个你最近遇到的"玄学 Bug",带着它去读源码------问题驱动,效率最高。
Spring 源码确实厚,但它不是密林,是有主干的树 :refresh() 是树干,BeanDefinition → 实例化 → 初始化 → 代理 是主枝,其余都是叶子。沿着主干走一遍,剩下的叶子,遇到再摘。读源码最难的从来不是技术,而是开头的那口气------别贪多,先顺着一条链路走通,尝到"原来这么简单"的甜头,剩下的就是水到渠成。
会用 Spring 是工作,读懂 Spring 是底气。
最后聊五毛钱的:你在面试里被问得最惨的一个 Spring 问题是哪个? 是三级缓存、事务失效,还是更刁钻的?评论区告诉我,点赞最高的三个,我会在后面的源码篇里逐行拆解。
觉得这篇帮你把"焦虑"换成了"方向",点个赞 和在看 ,顺手转发给那个也在"会用但不懂"的朋友。关注我,下一篇正式开刷:《refresh() 十二步逐行拆解(附断点地图)》,带你走进容器启动的第一现场,不见不散。
📌 版本基线:Spring Framework 7.0.8(发布于 2026-06-09)|对应 Spring Boot 4.x|最低 JDK 17,推荐 JDK 21/25|Jakarta EE 11|默认 CGLIB 代理|空安全:JSpecify
附:推荐参考资料
- Spring 官方文档:
https://docs.spring.io/spring-framework/reference/ - Spring Framework GitHub Releases(重点关注每个版本的变更说明):
https://github.com/spring-projects/spring-framework - Spring 官方 Blog(Road to GA 系列、版本发布公告):
https://spring.io/blog