你背的Spring答案,全是5.x时代的:7.0.8源码一翻,80%的博客都过时了

版本基线: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 条以上,建议把本文读完:

  1. 配置报错全靠搜索,复制粘贴完依然不知道根因
  2. 能背出"IOC 控制反转、AOP 面向切面",但说不出底层实现
  3. 循环依赖、事务失效、Bean 创建失败,排查全靠瞎试
  4. 面中高级岗屡屡折戟,薪资卡在瓶颈期
  5. 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 本身就是极好的源码阅读材料------它告诉你框架哪里曾经被攻破过。

与其漫无目的地读,不如从这些新特性切入------代码新、逻辑清晰、官方文档全:

  1. BeanRegistrar:全新的编程式 Bean 注册机制,AOT 友好
  2. JSpecify 空安全:泛型、数组、可变参数的空性都能标注
  3. 弹性内建 :RetryTemplate、@Retryable、@ConcurrencyLimit 收编进 spring-core,不再依赖 Spring Retry
  4. API 版本管理 :MVC / WebFlux 原生支持 @RequestMapping(version = "...")
  5. JmsClient :继 JdbcClient、RestClient 之后的第三块拼图
  6. Java 24+ Class-File API:用 JDK 原生 API 读字节码,替代内置 ASM
  7. 后台初始化 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 文章的两处差异(面试加分点):

  1. 锁从 synchronized (this.singletonObjects) 换成了 ReentrantLock singletonLock(6.2 起的单例锁重构);
  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);

"事务失效"四大场景,一个原理通杀:

  1. 自调用 ------this 绕过代理;
  2. 方法非 public------代理增强有可见性要求;
  3. 异常被吞------catch 掉了异常,拦截器以为成功;
  4. 数据库引擎不支持事务------比如 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 三遍读法

  1. 第一遍:跑起来 。目标只有一个------让代码跑起来,知道入口在哪。断点打在 AbstractApplicationContext#refresh,不要试图看懂每一行。
  2. 第二遍:跟一个 Bean 的一生 。只跟一个 UserService:什么时候被扫描?什么时候变成 BeanDefinition?什么时候实例化、注入、初始化?最终容器里存的是原始对象还是代理对象?
  3. 第三遍:专题突破。按专题逐个击破,每个专题只解决一个问题:
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 避坑清单:这些坑我替你踩过了

  1. 不要从 main 分支读 ------今天看的逻辑明天可能被重构,一定 git checkout v7.0.8;
  2. 不要一上来就读 Boot 自动配置 ------那是"第二层魔法",先把 refresh() 十二步吃透,自动配置只是 BeanFactoryPostProcessor 的一个应用;
  3. 不要试图一次读懂所有 scope 和所有回调 ------第一轮只看 singleton + 构造器注入 + @Autowired + @PostConstruct,其余标记为"待办";
  4. 不要只看不写 ------每读完一节,自己写一个 BeanFactoryPostProcessor / BeanPostProcessor / ImportBeanDefinitionRegistrar 验证猜想,动手一次胜过看十遍;
  5. 不要忽视测试代码 ------spring-beans/src/test 里的用例是官方写的"最佳使用文档",比任何博客都准;
  6. 不要忽略 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 生命周期的骨架几乎没动。


写在最后

把全文压缩成三句话:

  1. 读源码不是为了背代码,是为了建立"对象 → 流程"的心智模型,把 L1 的试错升级成 L2 的定位;
  2. 入口就一个 :AbstractApplicationContext#refresh(),抓住 5 / 6 / 11 三个关键帧;
  3. 方法就三步:主链路 → 断点验证 → 反证法,手敲 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

附:推荐参考资料

  1. Spring 官方文档:https://docs.spring.io/spring-framework/reference/
  2. Spring Framework GitHub Releases(重点关注每个版本的变更说明):https://github.com/spring-projects/spring-framework
  3. Spring 官方 Blog(Road to GA 系列、版本发布公告):https://spring.io/blog
相关推荐
阿卡好可爱15 分钟前
Lua 热更新不生效:改完文件线上仍是旧逻辑,package.loaded 与 upvalue 重绑定详解
数据结构·分布式·后端·架构·lua
这个昵称不容易1 小时前
考试报名系统的设计与实现(SpringBoot + MySQL + Vue 三角色平台)
java·springboot·毕设
Adellle1 小时前
设计模式指南 (Java 版) ㊁
java·开发语言·设计模式
特立独行的猫A2 小时前
cj-tauri:用仓颉写桌面与鸿蒙 PC 应用
java·后端·python
Lonely丶墨轩2 小时前
从一个 404 到负数 ID:第三方接口开发中的几次排错
java
聪明的笨猪猪2 小时前
实际工程问题:AI生成MyBatis批量操作的SQL超限与性能雪崩
java·经验分享·面试·ai编程
文丑颜不良啊2 小时前
LeetCode2501~2520题目解析
java·leetcode
mldong2 小时前
同一个节点,同一个人,办两次:jeeflow 的已办撤回
后端·架构