Chapter 04

自测与判别

前三章把"容器 = 对象图装配机、魔法 = 生命周期上的 BPP"这条主线讲完了。这一章用三层题把它从"读过"压成"能调用"——尤其是判别层,模拟面试官顺着一个现象往机制深处追问的样子。

怎么用这一章

  • 合上前三章作答。写下来,别只在脑子里"想一下"——想得通和写得出差着一整层。
  • 三层梯度:概念层(回忆术语)→ 原理层(讲机制)→ 判别层(按场景选方案)。
  • 判别层是重点,每题都要求你在两章及以上的机制之间做选择。
  • 所有答案集中在页面最底部一个折叠块里——做完一整层再去对。

题量约 16 道,难度自下而上递增。先看这三层各考什么、对应哪一章:

概念层 · 回忆术语 对应 01 章 原理层 · 讲机制 对应 02 章 应用判别层 · 迁移 综合 01 + 02 + 03 迁移难度 ↑ ~6 题 ~5 题 ~5 题
图 4.0三层题的难度梯度与章节映射。注意:朱红的顶层(判别)才是面试拉开差距的地方——它考的不是"知不知道",而是"在两个机制之间选得对不对"。

A概念层(对应 01 章)

  1. 一句话说清 IoC 与 DI 的关系——它们是两个机制,还是一件事的两个视角?
  2. BeanDefinition 和 bean 实例分别处在容器启动的哪个阶段?为什么要分开?
  3. 裸用 BeanFactory 与用 ApplicationContext,在"单例何时被创建"上有什么区别?
  4. 构造器注入、setter 注入、字段注入,分别在 bean 生命周期的哪一步把依赖放进去?
  5. Spring AOP 的"连接点"只有哪一种?这一条约束直接导致了什么后果?
  6. singleton 和 prototype,容器分别负责到哪一步?谁的销毁容器不管?

B原理层(对应 02 章 + 03 章机制)

  1. refresh() 里,BFPP 在哪一步"运行"?BPP 在哪一步"注册"、又在哪一步"运行"?注册和运行是不是同一步?
  2. Bean 生命周期三步是什么?@Autowired 注入与 AOP 代理分别落在哪一步、由什么组件完成?
  3. 三级缓存为什么需要"第三级(工厂)"?去掉它、只留两级,在什么情况下会出错?
  4. 为什么构造器注入的循环依赖无法靠三级缓存解开?根因在哪个时机?
  5. @PostConstruct 方法里的 this,是代理还是原始对象?为什么?这对在其中调用本类事务方法意味着什么?

C应用判别层(综合判别 · 本章重点)

每题先给场景,要求你在不同章节的机制之间做选择,并讲清依据——不是选对就行,是要说出"为什么不是另一个"。

  1. 注入方式之争:一个 ReportService 有 3 个必填依赖,团队规范要求可单元测试、且希望配置错误在启动期就暴露。该用构造器注入还是字段注入?如果其中两个依赖恰好互相引用(循环),你的选择会被迫改变吗?(牵涉 01 注入方式 + 02 循环依赖)
  2. 事务不回滚排查:同事的 @Transactional 方法不回滚,代码"看着没问题"。你要依次排查哪三件事?每件事对应教程哪一节的机制?(牵涉 03 自调用 / final + 02 初始化时机)
  3. 配置类写法:code review 看到一个类用 @Component 而非 @Configuration,里面有多个 @Bean 方法且彼此调用。要不要打回?最坏会静默发生什么?(牵涉 03 full/lite + 01 单例语义)
  4. 给领域对象加审计:需求是给一批 new 出来的领域对象(不是 bean)的字段写入加审计日志。用 Spring AOP 能实现吗?要不要上 AspectJ?为什么?(牵涉 01 连接点 + 03 Spring AOP vs AspectJ)
  5. prototype 没刷新:单例 OrderService 里注入了一个 @Scope("prototype") 的 PricingContext,发现每次请求拿到的都是同一个。根因是什么?给出两种修法。(牵涉 01 作用域 + 02 注入时机)
亲手画一张图

合上教程,在纸上或 Excalidraw 里画出 Bean 生命周期三步——只画三个框就行(实例化 / 属性填充 / 初始化),然后在图上标出两件事:@Autowired 注入发生在哪一步、AOP 代理在哪一步生成。

画完翻回 §2.2 对照——你标的 AOP 代理位置,是在初始化之前还是之后?标对了,说明本教程最核心的那条脊梁你已经握住了;标错了,正好回去重读那一节,这就是它最该被你记住的地方。

全部答案(三层都做完再展开)

A · 概念层

  1. 一件事的两个视角:IoC 是"获得依赖的控制权从对象反转到容器"这个结果,DI(注入)是实现它的手段。不是两个机制。
  2. 启动先收齐所有 BeanDefinition(配方阶段,无实例),之后才按配方造 bean 实例。分开是为了在实例化前留出改写配方的间隙(占位符替换、@Configuration 解析)。
  3. BeanFactory 惰性,getBean 时才造;ApplicationContext 在 refresh 末尾急切造好所有非 lazy 单例,配置错误启动期就暴露。
  4. 构造器注入在"实例化"那一刻(依赖是构造参数);setter 和字段注入都在"属性填充"阶段、实例已存在之后。
  5. 只有"方法执行"。后果:增强靠代理、只在外部经过代理的调用上触发,于是同类内部 this.method() 自调用绕过代理、通知不触发。
  6. singleton 由容器创建并管理整个生命周期(含销毁回调);prototype 容器造完即撒手,不缓存、不负责调用其销毁方法。

B · 原理层

  1. BFPP 在第 4 步 invokeBeanFactoryPostProcessors 运行;BPP 在第 5 步 registerBeanPostProcessors 仅注册(登记+排序),到第 7 步造每个 bean 时才运行。注册≠运行。
  2. 实例化 → 属性填充 → 初始化。@Autowired 在属性填充,由 AutowiredAnnotationBeanPostProcessor 完成;AOP 代理在初始化最后一步 postProcessAfterInitialization,由自动代理创建器(也是一个 BPP)完成。
  3. 第三级存的是"工厂",它把"提前暴露的引用要不要做成 AOP 代理"延迟到真正被取用那一刻才决定。只留两级会提前暴露原始对象,而被增强的 bean 最终应是代理,依赖方就拿到了和容器最终版本不一致的引用。
  4. 三级缓存起作用的前提是"对象已实例化、只差填属性"。构造器注入的依赖是构造参数,对象在拿到依赖前根本造不出壳,无法被提前暴露,所以循环时谁都到不了可暴露状态,抛 BeanCurrentlyInCreationException。
  5. 是原始对象。代理在初始化最后一步才生成,而 @PostConstruct 在它之前执行。后果:在 @PostConstruct 里调本类的 @Transactional 方法,既是自调用、对象又尚未被代理,事务必然不生效。

C · 应用判别层

  1. 用构造器注入:依赖必填、可 final 不可变、不靠容器就能 new 出来测、且循环依赖会在启动期直接报错(符合"错误早暴露"诉求)。若两个依赖确实互相引用形成循环,构造器注入会启动失败——此时被迫改为 setter/字段注入之一来打破(靠三级缓存解开),或更应回头质疑这个循环设计本身。
  2. 依次查:① 是不是自调用(同类内 this.method() 调用,绕过代理 —— 03.3);② 方法或类是不是 final(CGLIB 无法覆盖 —— 03.2);③ 是不是在 @PostConstruct / 构造器里调用(代理尚未生成 —— 02.2)。三者都是"调用没经过代理"的不同形态。
  3. 要打回。@Component 是 lite 模式、类不被 CGLIB 增强,@Bean 方法互调变成普通 Java 调用,会静默造出重复对象、破坏单例,且启动不报错,要到运行时状态不一致才暴露。改回 @Configuration(full)。
  4. Spring AOP 做不到:它的连接点只有方法执行(01.6),既拦不了字段写入,作用对象也只限容器里的 bean,而领域对象是 new 出来的。必须上 AspectJ(编译期/加载期字节码织入,连接点含字段、作用于任意对象 —— 03.5)。
  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 与原生镜像:把本教程的运行期反射装配搬到构建期固化——理解这套机制后再看,会顺很多。