Chapter 04
自测与判别
前三章把"容器 = 对象图装配机、魔法 = 生命周期上的 BPP"这条主线讲完了。这一章用三层题把它从"读过"压成"能调用"——尤其是判别层,模拟面试官顺着一个现象往机制深处追问的样子。
怎么用这一章
- 合上前三章作答。写下来,别只在脑子里"想一下"——想得通和写得出差着一整层。
- 三层梯度:概念层(回忆术语)→ 原理层(讲机制)→ 判别层(按场景选方案)。
- 判别层是重点,每题都要求你在两章及以上的机制之间做选择。
- 所有答案集中在页面最底部一个折叠块里——做完一整层再去对。
题量约 16 道,难度自下而上递增。先看这三层各考什么、对应哪一章:
A概念层(对应 01 章)
- 一句话说清 IoC 与 DI 的关系——它们是两个机制,还是一件事的两个视角?
- BeanDefinition 和 bean 实例分别处在容器启动的哪个阶段?为什么要分开?
- 裸用 BeanFactory 与用 ApplicationContext,在"单例何时被创建"上有什么区别?
- 构造器注入、setter 注入、字段注入,分别在 bean 生命周期的哪一步把依赖放进去?
- Spring AOP 的"连接点"只有哪一种?这一条约束直接导致了什么后果?
- singleton 和 prototype,容器分别负责到哪一步?谁的销毁容器不管?
B原理层(对应 02 章 + 03 章机制)
- refresh() 里,BFPP 在哪一步"运行"?BPP 在哪一步"注册"、又在哪一步"运行"?注册和运行是不是同一步?
- Bean 生命周期三步是什么?
@Autowired注入与 AOP 代理分别落在哪一步、由什么组件完成? - 三级缓存为什么需要"第三级(工厂)"?去掉它、只留两级,在什么情况下会出错?
- 为什么构造器注入的循环依赖无法靠三级缓存解开?根因在哪个时机?
@PostConstruct方法里的this,是代理还是原始对象?为什么?这对在其中调用本类事务方法意味着什么?
C应用判别层(综合判别 · 本章重点)
每题先给场景,要求你在不同章节的机制之间做选择,并讲清依据——不是选对就行,是要说出"为什么不是另一个"。
- 注入方式之争:一个
ReportService有 3 个必填依赖,团队规范要求可单元测试、且希望配置错误在启动期就暴露。该用构造器注入还是字段注入?如果其中两个依赖恰好互相引用(循环),你的选择会被迫改变吗?(牵涉 01 注入方式 + 02 循环依赖) - 事务不回滚排查:同事的
@Transactional方法不回滚,代码"看着没问题"。你要依次排查哪三件事?每件事对应教程哪一节的机制?(牵涉 03 自调用 / final + 02 初始化时机) - 配置类写法:code review 看到一个类用
@Component而非@Configuration,里面有多个@Bean方法且彼此调用。要不要打回?最坏会静默发生什么?(牵涉 03 full/lite + 01 单例语义) - 给领域对象加审计:需求是给一批
new出来的领域对象(不是 bean)的字段写入加审计日志。用 Spring AOP 能实现吗?要不要上 AspectJ?为什么?(牵涉 01 连接点 + 03 Spring AOP vs AspectJ) - prototype 没刷新:单例
OrderService里注入了一个@Scope("prototype")的PricingContext,发现每次请求拿到的都是同一个。根因是什么?给出两种修法。(牵涉 01 作用域 + 02 注入时机)
亲手画一张图
合上教程,在纸上或 Excalidraw 里画出 Bean 生命周期三步——只画三个框就行(实例化 / 属性填充 / 初始化),然后在图上标出两件事:@Autowired 注入发生在哪一步、AOP 代理在哪一步生成。
画完翻回 §2.2 对照——你标的 AOP 代理位置,是在初始化之前还是之后?标对了,说明本教程最核心的那条脊梁你已经握住了;标错了,正好回去重读那一节,这就是它最该被你记住的地方。
全部答案(三层都做完再展开)
A · 概念层
- 一件事的两个视角:IoC 是"获得依赖的控制权从对象反转到容器"这个结果,DI(注入)是实现它的手段。不是两个机制。
- 启动先收齐所有 BeanDefinition(配方阶段,无实例),之后才按配方造 bean 实例。分开是为了在实例化前留出改写配方的间隙(占位符替换、
@Configuration解析)。 - BeanFactory 惰性,getBean 时才造;ApplicationContext 在 refresh 末尾急切造好所有非 lazy 单例,配置错误启动期就暴露。
- 构造器注入在"实例化"那一刻(依赖是构造参数);setter 和字段注入都在"属性填充"阶段、实例已存在之后。
- 只有"方法执行"。后果:增强靠代理、只在外部经过代理的调用上触发,于是同类内部
this.method()自调用绕过代理、通知不触发。 - singleton 由容器创建并管理整个生命周期(含销毁回调);prototype 容器造完即撒手,不缓存、不负责调用其销毁方法。
B · 原理层
- BFPP 在第 4 步 invokeBeanFactoryPostProcessors 运行;BPP 在第 5 步 registerBeanPostProcessors 仅注册(登记+排序),到第 7 步造每个 bean 时才运行。注册≠运行。
- 实例化 → 属性填充 → 初始化。
@Autowired在属性填充,由AutowiredAnnotationBeanPostProcessor完成;AOP 代理在初始化最后一步postProcessAfterInitialization,由自动代理创建器(也是一个 BPP)完成。 - 第三级存的是"工厂",它把"提前暴露的引用要不要做成 AOP 代理"延迟到真正被取用那一刻才决定。只留两级会提前暴露原始对象,而被增强的 bean 最终应是代理,依赖方就拿到了和容器最终版本不一致的引用。
- 三级缓存起作用的前提是"对象已实例化、只差填属性"。构造器注入的依赖是构造参数,对象在拿到依赖前根本造不出壳,无法被提前暴露,所以循环时谁都到不了可暴露状态,抛
BeanCurrentlyInCreationException。 - 是原始对象。代理在初始化最后一步才生成,而
@PostConstruct在它之前执行。后果:在@PostConstruct里调本类的@Transactional方法,既是自调用、对象又尚未被代理,事务必然不生效。
C · 应用判别层
- 用构造器注入:依赖必填、可
final不可变、不靠容器就能new出来测、且循环依赖会在启动期直接报错(符合"错误早暴露"诉求)。若两个依赖确实互相引用形成循环,构造器注入会启动失败——此时被迫改为 setter/字段注入之一来打破(靠三级缓存解开),或更应回头质疑这个循环设计本身。 - 依次查:① 是不是自调用(同类内
this.method()调用,绕过代理 —— 03.3);② 方法或类是不是final(CGLIB 无法覆盖 —— 03.2);③ 是不是在@PostConstruct/ 构造器里调用(代理尚未生成 —— 02.2)。三者都是"调用没经过代理"的不同形态。 - 要打回。
@Component是 lite 模式、类不被 CGLIB 增强,@Bean方法互调变成普通 Java 调用,会静默造出重复对象、破坏单例,且启动不报错,要到运行时状态不一致才暴露。改回@Configuration(full)。 - Spring AOP 做不到:它的连接点只有方法执行(01.6),既拦不了字段写入,作用对象也只限容器里的 bean,而领域对象是
new出来的。必须上 AspectJ(编译期/加载期字节码织入,连接点含字段、作用于任意对象 —— 03.5)。 - 根因:字段注入只在
OrderService(单例)创建时发生一次,那一次拿到的PricingContext被长期持有,prototype 的"每次新建"在这种注入下失效(01.5 + 02.2 注入时机)。修法:注入ObjectProvider<PricingContext>每次getObject()取新的;或用@Lookup方法注入;或给 prototype 加作用域代理@Scope(proxyMode=TARGET_CLASS)。
·都答对了?那你已经握住主线了
如果判别层五道都能讲出"为什么不是另一个",那么开篇那句话本质——容器是对象图装配机、魔法都是生命周期上的 BeanPostProcessor——就不再是一句口号,而是你能拿它去推导任何陌生 Spring 行为的工具。下一步可以往这些方向走:
- Spring 事务传播:在"代理 = AOP"的基础上,深入
@Transactional的 7 种传播行为与回滚规则。 - Spring Boot 自动配置:BFPP/BPP + 条件装配,是 starter 和
@Conditional的底座。 - AOT 与原生镜像:把本教程的运行期反射装配搬到构建期固化——理解这套机制后再看,会顺很多。