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

0 阅读37分钟

版本基线: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 这一代不是小打小闹的更新,而是朝着现代化、模块化、云原生方向的一次架构级升级——老经验正在批量失效,新秩序刚刚成型,这正是读源码的黄金窗口期。

image.png

先看基线要求,"拿着旧地图找不到新大陆":

维度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 采用分层架构,核心容器是整个框架的地基。模块依赖和学习顺序一张图说清:

image.png

学习顺序别跳:spring-core → spring-beans → spring-context → spring-aop → spring-tx → spring-webmvc。IoC 容器是最重要的核心技术,AOP 紧随其后,二者是源码学习的两大支柱。

4.2 BeanFactory 接口体系:接口隔离的教科书

正确的读法是先看接口层次,再看实现。以最核心的 BeanFactory 体系为例:

image.png

关键理解:Spring 把"管理对象"这件事按语义动词拆成了四个正交接口——取(get)、列(list)、层叠(delegate)、注入(wire)。这种接口隔离设计(ISP)是整个框架的地基。

顺带把面试高频题"为什么 ApplicationContext 比 BeanFactory 强"用第一性原理拆了。很多人的回答是"前者多了事件、i18n",这只答了是什么,没答为什么。看 refresh() 就一目了然:BeanFactory 是惰性工厂——你不问我就不做;ApplicationContext 是主动容器——启动时把后置处理器、事件广播器、国际化这些基础设施全部就绪,还会预实例化所有非 lazy 单例。

💡 一句话结论:业务代码 99% 用 ApplicationContext,只有在资源受限的嵌入式场景才退回裸 BeanFactory。

4.3 核心角色先认脸

后面源码走读会反复碰到这几个角色,先混个脸熟:

角色作用面试关键词
BeanFactoryBean 容器最底层能力获取、创建、管理 Bean
ApplicationContext更高级的 IoC 容器事件、资源、环境、国际化
BeanDefinitionBean 的"图纸"元数据类名、作用域、懒加载、构造参数
BeanFactoryPostProcessor修改 BeanDefinition配置类解析、属性覆盖
BeanPostProcessor加工 Bean 实例AOP、属性填充、初始化增强
Environment环境与配置抽象Profile、PropertySource
ApplicationEvent事件机制容器刷新事件、启动事件

一句话记忆:BeanDefinition 描述 Bean,BeanFactory 管理 Bean,BeanPostProcessor 加工 Bean,ApplicationContext 对外提供完整容器能力。 注意:有了图纸不等于有对象,这是后面很多问题的题眼。

4.4 一个请求的生命周期

再看一个 HTTP 请求在 7.0.8 里走哪些模块,建立"请求视角":

image.png

地图有了,接下来正式进入主线——整个 Spring 的心脏。


五、抓住唯一的主线索:refresh() 十二步

初学者最大的误区是打开 spring-context 从上往下读。正确姿势是找到整个容器的"总指挥",顺着它往下追——就是 AbstractApplicationContext#refresh()。

为什么是它?因为不管是传统 Spring、Spring MVC 还是 Spring Boot,最终都会走到这一行。Boot 的 run() 方法绕了一大圈,落点还是 refreshContext()。

5.1 入口:三行代码背后的构造方法

先写个最小 Demo,后面所有断点都从它开始:

@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 模块:

// 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 版本

// 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 以来锁体系重构的一部分。网上大部分文章还是老写法,对照时留意。

十二步记不住?压缩成三个阶段 + 三个关键帧:

第一阶段 · 立骨架   (1~3)  : 建工厂 → 登记 BeanDefinition(设计图)
第二阶段 · 布机关   (4~6)  : 跑 BFPP / 注册 BPP(把扩展点备好)
第三阶段 · 按图开工 (7~12) : 实例化所有单例 Bean → 发布事件

image.png

记住这个分界线:前 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 的完整生命周期。这条调用链是核心中的核心:

image.png

doGetBean 的"三步走"极度简化版:

// 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%:

resolveBeforeInstantiation  →  有机会直接返回一个代理(InstantiationAwareBPP 短路)
      ↓
doCreateBean                →  createBeanInstance(推断构造器,new 出半成品)
                              → addSingletonFactory(提前暴露,解循环依赖)
                              → populateBean(属性填充、@Autowired 注入)
                              → initializeBean(Aware → before → init → after ← AOP 代理产出)
      ↓
registerDisposableBeanIfNecessary

6.1 代理短路:Spring 的"上帝接口"

// 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 前提

new 对象 = ① 分配堆内存(属性全为默认值,即"半成品")
          + ② 执行构造 / 赋值(成为"成品")

这两个阶段可以分开!  ← Spring 解决循环依赖的全部秘密就在这
// 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 出来就完事

// 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)。

@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;
    }
}

同一切面内的通知执行顺序,背下来直接用:

@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 文章不一样:

// 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 代理)
三级singletonFactoriesObjectFactory 工厂(延迟决定要不要造代理)

与网上 5.x 文章的两处差异(面试加分点):

  1. 锁从 synchronized (this.singletonObjects) 换成了 ReentrantLock singletonLock(6.2 起的单例锁重构);
  2. 早期引用推断前先 tryLock(),拿不到锁直接返回 null,避免非创建线程"偷看"半成品:
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 就废了。这一句话,能直接把你和其他候选人拉开差距。

循环依赖的完整时序:

image.png

7.2 构造器注入为什么无解?

addSingletonFactory 发生在 populateBean 之前——即"对象刚 new 出来、属性还没填"时就把引用交出去了。而构造器注入在 createBeanInstance 阶段就要拿到对方,此时连"半成品"都还没有,只能抛 BeanCurrentlyInCreationException。

⚠️ 注意:三级缓存只能解决 setter / 字段注入的循环依赖。构造器注入的解法是:① 加 @Lazy 注入一个代理先顶着;② 改成 setter/字段注入让三级缓存兜底;③ 彻底根治是重构消除循环——抽公共服务、改事件驱动。另外 7.x + AOT 模式下显式循环依赖不被支持,别指望缓存永远救你。

想建立因果确信,可以玩一下反证法:

ctx.getBeanFactory().setAllowCircularReferences(false);  // 马上循环依赖报错

亲眼看到开关一关程序就崩,比背十遍八股文都管用。


八、@Transactional 失效:一个 this 引发的血案

理解了代理,"事务失效"这类玄学问题就全通了。声明式事务基于 AOP:TransactionInterceptor 拦截 @Transactional 方法,负责开启、提交、回滚。而同类内部方法调用走的是 this,是原始对象,不经过代理——拦截器根本没机会执行:

@Service
public class OrderService {

    public void pay() {
        this.createOrder();   // this 调用,代理失效,事务不生效!
    }

    @Transactional
    public void createOrder(Order order) {
        // 数据库操作
    }
}

三种修复方案:

// 方案一:抽到另一个 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 直接解放了这些约束:

// 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;
}

使用姿势(两种):

// 方式一:直接实现
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 的安全加固

先看一个真实的线上坑:

// 一个真实的坑: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,超过即抛异常中断求值。源码层面长这样:

// 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 包):

// 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 丢到后台线程并行初始化:

// 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/ 里看到类似这样的代码:

// 生成产物示意(真实类名是 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 三步阅读法(本系列通用方法论)

① 主链路法   先跑通"启动 → 取 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. 第三遍:专题突破。按专题逐个击破,每个专题只解决一个问题:
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 硬性要求

项要求说明
JDK17(最低)/ 21 或 25(推荐)虚拟线程、Record、Sealed Class 要 21+
IDEIntelliJ IDEA(社区版足够)Ctrl/Cmd + B、Ctrl + Alt + H 调用层级是最好的朋友
内存IDEA 堆内存建议 -Xmx4g源码索引吃内存,不给够会卡到你放弃
Gradle用项目自带 wrapper./gradlew,别自己装
Git需要切 tag 用

11.2 拉代码:一定要切 tag

# 方式 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 本体。

裸框架最小依赖,最干净的学习环境:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>7.0.8</version>
</dependency>

如果你非要带着 Boot 玩,起一个干净的:

<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 永远没有感觉:

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