2.14 Spring 的本质
从对象自行寻找依赖走到容器装配与代理承接横切边界,沿 Bean 定义、创建、依赖解析、代理和调用诊断 Spring。
学习目标
- 能沿 Bean 定义、实例创建、依赖解析、代理生成和业务调用追踪一次容器装配
- 能解释模板方法、装饰者、IoC/DI 与 AOP 如何分别承担流程复用、行为叠加、依赖装配和横切通知
- 能在正常、边界和故障场景中回答:为什么类内自调用可能绕过事务/日志代理,以及怎样验证真正经过了代理入口
2.14 Spring 的本质
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.14 Spring 的本质。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
先想象一间工作坊:工匠只描述自己需要什么工具,仓库管理员负责准备工具,检查员在出入口加上计量或安全步骤。若工匠每次都自己去找工具,代码会和仓库位置绑死;若检查员只站在入口外而工匠在屋内互相调用,检查也可能被绕过。
这一章解决的是“对象创建、依赖装配和横切动作由谁负责”。没有清晰的容器边界,业务对象会自己构造依赖、难以替换和测试;没有代理边界,事务、日志和权限看似配置成功,实际调用路径却可能没有经过通知。验收要跟踪对象身份和真实入口,而不是只看注解或配置文件。
三个会让 Spring 机制失真的陷阱
九个目录节点到装配证据
2.14 Spring 的本质
总合同是:容器读取 Bean 定义,创建对象,解析依赖,按需要生成代理,再把代理或目标交给调用者。诊断时必须区分目标对象和代理对象,并记录 Bean 名、类型、依赖身份、通知匹配和实际调用入口。
问题来源
↡业务对象自行创建依赖、复制初始化流程并承担横切处理,导致替换、测试和一致性变得困难的问题起点。不是“代码写得不优雅”,而是创建权、装配权和业务权混在一个类里。先画依赖图,找出谁拥有创建权,再决定哪些边界交给容器,才能知道 IoC 解决的具体摩擦。
设计模式:模板方法
↡由基类固定算法骨架,把可变步骤留给子类实现,从而复用流程顺序的设计模式。解释了为什么框架可以规定初始化、执行和清理的先后,而业务只补充变化步骤。它解决流程复用,不自动解决依赖装配或代理绕过;这几个问题必须分别留下证据。
设计模式:装饰者
↡通过包裹一个对象并在调用前后增加行为,同时保持原接口可用的设计模式。是代理和横切行为的直觉模型:外层可以加入计时、事务或权限,再把调用转给目标。装饰者必须明确顺序、异常和返回值,否则多层包装会让真正执行的行为难以追踪。
AOP
↡把日志、事务、权限等横切关注点按连接点匹配并应用到多个业务调用的编程方式。让横切规则从业务方法中抽离,但“匹配到了”不等于“运行时经过了”。验收要写出切点、通知顺序、代理类型、异常策略和自调用行为,并用实际调用轨迹验证。
实现AOP
↡在运行时或构建时用代理、拦截器或织入机制,把通知插入目标调用边界的具体过程。至少要回答代理何时生成、代理暴露什么接口、目标怎样取得、通知失败怎样处理。接口代理和子类代理的限制不同,最终应检查调用者拿到的对象和方法入口,而不是只检查生成配置。
对象的创建
↡依据 Bean 定义选择构造器、创建实例、执行初始化并登记生命周期的容器阶段。受作用域、构造器参数、工厂方法和初始化回调影响。把创建过程记录成事件,才能诊断重复实例、提前使用或关闭时未释放资源;对象由谁创建也决定它能否获得容器能力。
IoC与DI
↡把对象控制权交给容器,并由容器按照依赖关系把协作者注入对象的装配方式。不是一个神秘总开关:IoC 说明控制权转移,DI 说明依赖怎样进入对象。构造器注入把必需依赖变成可检查的图,接口和配置则让实现可以替换,测试也能显式提供替身。
最小容器与代理合同
interface PaymentPort { Receipt pay(Command command); }
final class Checkout {
private final PaymentPort payments;
Checkout(PaymentPort payments) {
this.payments = payments;
}
Receipt submit(Command command) {
return payments.pay(command);
}
}合同把必需依赖放进构造器,让创建失败尽早发生;容器可以选择真实 PaymentPort、测试替身和包裹它们的代理。若 Checkout 通过 new 自己创建 PaymentPort,装配和通知证据就不再属于这条容器链,测试必须明确这一差异。
五步复核一次 Spring 调用
1. 读取 Bean 定义并固定图谱
记录 Bean 名、实现类型、作用域、构造器参数和配置来源。基线只有一条清晰依赖边;边界场景加入缺失实现,预期是启动阶段给出可定位的装配错误。
Lab
Spring Bean 与代理实验
只改变依赖图或调用入口,观察对象身份、通知计数和容器错误如何变化。
容器创建 Checkout 并注入 PaymentPort
bean definition → constructor injection → proxy → external submit
判定
accept:依赖身份稳定,外部入口经过容器边界
当前样本:容器装配;保存 Bean 定义、实例身份、依赖边、代理入口和通知计数。
正常、边界与故障证据矩阵
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 容器创建、依赖图可解析、外部代理入口 | 依赖注入成功,通知按顺序执行 | Bean 身份、代理类型、通知轨迹 |
| 边界 | 作用域变化、可选依赖或代理限制 | 生命周期/缺省行为明确,不静默补洞 | 定义快照、实例身份、匹配结果 |
| 故障 | 缺 Bean、循环依赖或类内自调用 | 启动失败或通知明确绕过并可诊断 | 首个失败边、调用入口、恢复结果 |
故障诊断:先定位对象和入口
- 定义侧:查 Bean 名、扫描范围、配置来源、实现候选和作用域;注解存在不等于定义生效。
- 创建侧:查构造器/工厂、初始化回调和实例身份;空依赖往往是手动构造或生命周期过早使用。
- 代理侧:查代理类型、接口、切点匹配、通知顺序和异常传播;目标对象与代理对象必须分开记录。
- 调用侧:查调用是从容器拿到的代理进入,还是通过
this/手动实例绕过;用通知计数或事务状态复核。
如果依赖为空,先问实例是否由容器创建;如果通知没有执行,先问调用是否经过代理入口;如果启动失败,沿依赖图寻找第一个未解析节点;如果对象被创建多次,比较作用域、工厂和缓存身份。每次只改一个边界并重放基线。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 问题来源
创建权、装配权和业务逻辑混在对象内部造成的替换、测试和一致性难题。
- 设计模式:模板方法
基类固定流程顺序,子类只提供变化步骤的复用方式。
- 设计模式:装饰者
包裹原对象并在调用前后追加行为,同时保持原接口可用的方式。
- AOP
按连接点把日志、事务、权限等横切规则应用到多个调用的方式。
- 实现AOP
用代理、拦截器或织入把通知放入目标调用边界的具体过程。
- 对象的创建
容器依据定义选择构造方式、实例化并管理生命周期的阶段。
- IoC与DI
容器接管对象控制权,并按依赖图把协作者注入对象的装配方式。
练习
练习
问题 1: 类上有事务注解,但类内自调用没有事务效果。请解释原因并给出一种可验证的改法。
问题 2: 为什么构造器注入有助于发现循环依赖?你会保存哪些诊断证据?
问题 3: 修改本页“Spring Bean 与代理实验”的故障场景,使它同时显示缺失 Bean 和类内自调用绕过通知,并说明重置后应恢复哪些状态。
本页小结
- 容器先读定义,再创建、装配和管理 Bean 生命周期。
- IoC/DI 解决创建与依赖边界,模板方法/装饰者解释复用与行为叠加。
- AOP 的关键是实际代理入口、通知匹配和自调用边界。
- 目标对象、代理对象、依赖图和生命周期都要可验证。
读完后的自测问题是:当注解看起来已经生效但类内自调用没有通知时,你能否指出对象身份、调用入口和最小改动,并用实验轨迹证明修复成立?