2.14 Spring 的本质

从对象自行寻找依赖走到容器装配与代理承接横切边界,沿 Bean 定义、创建、依赖解析、代理和调用诊断 Spring。

学习目标

  • 能沿 Bean 定义、实例创建、依赖解析、代理生成和业务调用追踪一次容器装配
  • 能解释模板方法、装饰者、IoC/DI 与 AOP 如何分别承担流程复用、行为叠加、依赖装配和横切通知
  • 能在正常、边界和故障场景中回答:为什么类内自调用可能绕过事务/日志代理,以及怎样验证真正经过了代理入口

2.14 Spring 的本质

本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 2.14 Spring 的本质。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。

先想象一间工作坊:工匠只描述自己需要什么工具,仓库管理员负责准备工具,检查员在出入口加上计量或安全步骤。若工匠每次都自己去找工具,代码会和仓库位置绑死;若检查员只站在入口外而工匠在屋内互相调用,检查也可能被绕过。

这一章解决的是“对象创建、依赖装配和横切动作由谁负责”。没有清晰的容器边界,业务对象会自己构造依赖、难以替换和测试;没有代理边界,事务、日志和权限看似配置成功,实际调用路径却可能没有经过通知。验收要跟踪对象身份和真实入口,而不是只看注解或配置文件。

Spring 容器链:对象与横切行为都要有入口目标对象由容器创建,调用者拿到的可能是承接通知的代理1Bean 定义类型 + 作用域容器证据2创建实例构造/初始化容器证据3解析依赖有向图容器证据4生成代理切点 + 通知横切边界5调用业务入口可追踪调用证据代理能拦截入口调用,类内 this 调用可能直接绕过代理
专属图示:把 IoC/DI 的装配链和 AOP 的调用入口放到同一张图中。

三个会让 Spring 机制失真的陷阱

九个目录节点到装配证据

2.14 Spring 的本质

总合同是:容器读取 Bean 定义,创建对象,解析依赖,按需要生成代理,再把代理或目标交给调用者。诊断时必须区分目标对象和代理对象,并记录 Bean 名、类型、依赖身份、通知匹配和实际调用入口。

问题来源

不是“代码写得不优雅”,而是创建权、装配权和业务权混在一个类里。先画依赖图,找出谁拥有创建权,再决定哪些边界交给容器,才能知道 IoC 解决的具体摩擦。

设计模式:模板方法

解释了为什么框架可以规定初始化、执行和清理的先后,而业务只补充变化步骤。它解决流程复用,不自动解决依赖装配或代理绕过;这几个问题必须分别留下证据。

设计模式:装饰者

是代理和横切行为的直觉模型:外层可以加入计时、事务或权限,再把调用转给目标。装饰者必须明确顺序、异常和返回值,否则多层包装会让真正执行的行为难以追踪。

Spring 容器链:对象与横切行为都要有入口目标对象由容器创建,调用者拿到的可能是承接通知的代理1Bean 定义类型 + 作用域容器证据2创建实例构造/初始化容器证据3解析依赖有向图容器证据4生成代理切点 + 通知横切边界5调用业务入口可追踪调用证据代理能拦截入口调用,类内 this 调用可能直接绕过代理
专属图示:把 IoC/DI 的装配链和 AOP 的调用入口放到同一张图中。

AOP

让横切规则从业务方法中抽离,但“匹配到了”不等于“运行时经过了”。验收要写出切点、通知顺序、代理类型、异常策略和自调用行为,并用实际调用轨迹验证。

实现AOP

至少要回答代理何时生成、代理暴露什么接口、目标怎样取得、通知失败怎样处理。接口代理和子类代理的限制不同,最终应检查调用者拿到的对象和方法入口,而不是只检查生成配置。

对象的创建

受作用域、构造器参数、工厂方法和初始化回调影响。把创建过程记录成事件,才能诊断重复实例、提前使用或关闭时未释放资源;对象由谁创建也决定它能否获得容器能力。

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 / 5

1. 读取 Bean 定义并固定图谱

记录 Bean 名、实现类型、作用域、构造器参数和配置来源。基线只有一条清晰依赖边;边界场景加入缺失实现,预期是启动阶段给出可定位的装配错误。

Spring 容器链:对象与横切行为都要有入口目标对象由容器创建,调用者拿到的可能是承接通知的代理1Bean 定义类型 + 作用域容器证据2创建实例构造/初始化容器证据3解析依赖有向图容器证据4生成代理切点 + 通知横切边界5调用业务入口可追踪调用证据代理能拦截入口调用,类内 this 调用可能直接绕过代理
专属图示:把 IoC/DI 的装配链和 AOP 的调用入口放到同一张图中。

Lab

Spring Bean 与代理实验

只改变依赖图或调用入口,观察对象身份、通知计数和容器错误如何变化。

容器创建 Checkout 并注入 PaymentPort

bean definition → constructor injection → proxy → external submit

判定

accept:依赖身份稳定,外部入口经过容器边界

当前样本:容器装配;保存 Bean 定义、实例身份、依赖边、代理入口和通知计数。

正常、边界与故障证据矩阵

Spring 证据矩阵:对象身份与调用入口要对得上正常样本看容器闭环,边界样本看作用域,故障样本看绕过路径观察项正常边界故障定义扫描命中多实现缺 Bean实例身份稳定作用域变更手动 new依赖图可排序可选边循环边通知外部入代理代理限制this 绕过先记录定义、实例和入口,再判断通知是否真的执行
专属图示:把容器装配、作用域边界、依赖故障和代理入口逐项对照。
样本只改变的变量预期判定必存证据
正常容器创建、依赖图可解析、外部代理入口依赖注入成功,通知按顺序执行Bean 身份、代理类型、通知轨迹
边界作用域变化、可选依赖或代理限制生命周期/缺省行为明确,不静默补洞定义快照、实例身份、匹配结果
故障缺 Bean、循环依赖或类内自调用启动失败或通知明确绕过并可诊断首个失败边、调用入口、恢复结果

故障诊断:先定位对象和入口

  1. 定义侧:查 Bean 名、扫描范围、配置来源、实现候选和作用域;注解存在不等于定义生效。
  2. 创建侧:查构造器/工厂、初始化回调和实例身份;空依赖往往是手动构造或生命周期过早使用。
  3. 代理侧:查代理类型、接口、切点匹配、通知顺序和异常传播;目标对象与代理对象必须分开记录。
  4. 调用侧:查调用是从容器拿到的代理进入,还是通过 this/手动实例绕过;用通知计数或事务状态复核。

如果依赖为空,先问实例是否由容器创建;如果通知没有执行,先问调用是否经过代理入口;如果启动失败,沿依赖图寻找第一个未解析节点;如果对象被创建多次,比较作用域、工厂和缓存身份。每次只改一个边界并重放基线。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

问题来源

创建权、装配权和业务逻辑混在对象内部造成的替换、测试和一致性难题。

设计模式:模板方法

基类固定流程顺序,子类只提供变化步骤的复用方式。

设计模式:装饰者

包裹原对象并在调用前后追加行为,同时保持原接口可用的方式。

AOP

按连接点把日志、事务、权限等横切规则应用到多个调用的方式。

实现AOP

用代理、拦截器或织入把通知放入目标调用边界的具体过程。

对象的创建

容器依据定义选择构造方式、实例化并管理生命周期的阶段。

IoC与DI

容器接管对象控制权,并按依赖图把协作者注入对象的装配方式。

练习

练习

问题 1: 类上有事务注解,但类内自调用没有事务效果。请解释原因并给出一种可验证的改法。

问题 2: 为什么构造器注入有助于发现循环依赖?你会保存哪些诊断证据?

问题 3: 修改本页“Spring Bean 与代理实验”的故障场景,使它同时显示缺失 Bean 和类内自调用绕过通知,并说明重置后应恢复哪些状态。

资料与写作方式声明

本章以码农翻身权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

本页小结

  • 容器先读定义,再创建、装配和管理 Bean 生命周期。
  • IoC/DI 解决创建与依赖边界,模板方法/装饰者解释复用与行为叠加。
  • AOP 的关键是实际代理入口、通知匹配和自调用边界。
  • 目标对象、代理对象、依赖图和生命周期都要可验证。

读完后的自测问题是:当注解看起来已经生效但类内自调用没有通知时,你能否指出对象身份、调用入口和最小改动,并用实验轨迹证明修复成立?

讨论

评论区加载中…