读完这篇,你能做什么
能用构造器表达协作者,并区分单例作用域与线程安全保证。
学习理由、准备要求与知识点
为什么学它
显式依赖注入让业务对象关系可替换和可测试,是 Spring 其他机制的基础。
先读这些内容
知识点清单
IoC与依赖注入- Bean 定义和注册
- 构造器依赖
- 单构造器注入
- 组件扫描范围
- 接口与实现注册
- singleton 的容器范围
- 无请求状态服务设计
运行前先确认Java 17;Spring Framework 6.2 系列。源码定位固定为 v6.2.11,教学模型只保留必要候选选择与构造依赖,不实现 Spring 容器。
01先弄懂它是什么
先想怎样创建一个能工作的 Service:菜品服务离不开查询菜品的协作者,就在构造器里接收它;这次要查的菜品编号,则放在业务方法参数里。前者是长期依赖,后者是本次请求数据。若都塞进单例字段,不同请求可能互相覆盖;换一种注入注解也不能解决这个问题。
IoC 表示把对象创建和组合的责任交给外部,依赖注入是其中一种做法。普通 Java 也可以先创建 Mapper,再通过构造器传给 Service,不一定先学注解。Spring 在此基础上统一管理注册、作用域、配置和生命周期。扫描组件、@Bean 方法等能产生 Bean 定义,但代码写了一个接口,并不会自动生成它的实现。
默认的 singleton 表示:同一个容器中,同一份 Bean 定义通常共用一个对象。它不等于“一个类在整个 JVM 里只能有一个对象”;同一个类可以注册成多份定义,父子容器也另有规则。
final 字段能阻止你换掉协作者引用,却不能阻止协作者修改内部数据。构造器参数太多时,应先检查这个类是否承担了太多工作,再按业务用法拆分。只有一个构造器的组件通常不用额外写 @Autowired;同一类型有多个实现时,则要说清选哪一个。
文中这几个词,先认一认
- Bean 定义
- 告诉 Spring 对象怎样创建、是什么类型、采用什么作用域以及需要哪些依赖的说明,不是对象本身。
- 构造器注入
- 创建对象时就把必需的协作者传进去,使缺少依赖的问题尽早暴露。
- 候选歧义
- 一个位置可以匹配多个 Bean,而当前规则无法决定选哪一个,需要进一步指定。
02看看它怎么工作
以 Controller → Service → Mapper 为例,容器创建 Controller 前必须解析 Service,而 Service 又触发 Mapper 的解析。实际过程还有代理、懒加载与工厂对象等分支,但直接构造依赖可画成有向图:边从使用者指向被依赖者。图中依赖先准备,使用者后构造;共享依赖不应因为走到两条路径就被重复创建为单例。
同一个类型找到多个 Bean 时,要先想清自己需要一个还是全部。限定符用来缩小可选范围,Primary 用来指定优先使用的候选,集合注入则可以一次拿到全部实现;不能拿扫描顺序代替这些选择。参数名有时也会参与匹配,但需要编译时保留参数名等条件。像支付渠道这样的明确业务选择,直接写出限定条件,通常比依赖某个参数名更容易看懂和维护。
如果 A 的构造器要求 B,而 B 的构造器又要求 A,两者都等对方先完成,直接构造无法得到第一个实例。教学图用访问中状态检测这个环。生产设计应检查职责方向,常见修正是提取共同协作者或由更高层用例编排。懒代理可能改变创建时机,却不自动修复互相调用造成的递归或事务边界混乱。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["读取 Bean 定义"] --> B["解析构造器依赖"]
B --> C{"依赖是否已完成"}
C -->|"是"| D["复用实例"]
C -->|"否"| E{"依赖是否正在创建"}
E -->|"是"| F["报告循环路径"]
E -->|"否"| G["先创建所有依赖"]
G --> H["构造并发布实例"]
D --> H按顺序看,每一步做了什么
- 从启动扫描范围或显式配置定位每个 Bean 定义。
- 为必需构造器参数找出符合类型与限定条件的候选。
- 无候选和多候选分别诊断,避免给所有实现都标 Primary。
- 先创建依赖,再创建使用者,保留失败路径而非只看最后一行异常。
- 检查实例作用域与可变字段,确认请求数据通过参数流动。
03对照实现代码
Spring Framework v6.2.11 · DefaultListableBeanFactory#doResolveDependency、#determineAutowireCandidate
依据真实符号编写的独立教学实现,非原源码节选。省略限定符解析、类型转换、别名、Fallback、父容器、优先级及懒代理,仅展示零个、一个和多个候选的基本分支。
import java.util.*;
class SourceModel {
record Candidate(String name, boolean primary, Object value) {}
static Object choose(List<Candidate> candidates) {
// 必需依赖没有候选,必须报告装配错误。
if (candidates.isEmpty()) throw new IllegalStateException("missing");
if (candidates.size() == 1) return candidates.get(0).value();
Candidate chosen = null;
for (Candidate c : candidates) {
if (!c.primary()) continue;
// 两个首选不能靠集合顺序消除歧义。
if (chosen != null) throw new IllegalStateException("multiple primary");
chosen = c;
}
// 真实容器还会继续检查名称、优先级等规则;此模型明确不支持。
if (chosen == null) throw new IllegalStateException("ambiguous");
return chosen.value();
}
}doResolveDependency 不只是一个 Map 查询;源码先处理快捷解析及集合等情况,再进入普通候选集合分支。读代码应先确定自己的注入点落在哪条路径。
必需注入点没有候选会报告缺失,多个候选则调用 determineAutowireCandidate。错误分支本身解释了为何扫描范围与限定符需要分开排查。
该版本候选选择包含首选、依赖名、限定建议名、优先级等规则,教学模型保留最小子集。不能据模型断言 Spring 仅支持 Primary。
挑中 Bean 定义后,还要拿到实际对象并检查类型。这个对象可能此时才创建,所以“发现了 Bean 的定义”和“已经交付可用实例”是两步,排查时要看失败发生在哪一步。
代码先调用谁,再调用谁
- 构造参数解析器请求依赖
resolveDependency/doResolveDependencyfindAutowireCandidatesdetermineAutowireCandidate- 解析并校验所选实例
04动手写一个例子
三色访问:构造依赖排序并报告循环
给出 Bean 名称到直接依赖列表的映射,生成依赖先于使用者的确定顺序;共享依赖只出现一次。缺失定义或循环必须报错。模型只处理静态直接构造图,不实例化业务对象。
- 输入 1
controller→service,service→mapper- 输出 1
[mapper, service, controller]
- 输入 2
a→b,b→a- 输出 2
抛出 cycle 异常
白、灰、黑只是三种标记:白色表示还没看过,灰色表示正在处理但还没完成,黑色表示已经完成。先把当前对象标成灰色,再逐个检查它需要的依赖。若沿这条调用路径又遇到灰色对象,就说明绕了一圈回到仍在等待的对象,存在循环依赖。
所有依赖成功完成后,才将当前节点加入结果并标记完成。因此结果中每条边的被依赖端必定在前,菱形图中第二次到达已完成节点会直接返回。
节点遍历按名称排序保证多个合法答案中选择固定结果;依赖顺序保留输入次序。排序只提供可复现输出,不能把它误解为 Spring 按名称创建所有 Bean 的承诺。
为什么这样写是对的灰色节点恰好位于当前递归路径;黑色节点的全部依赖已在结果中,且每个黑色节点只输出一次。
顶点排序需要 V log V;DFS 对每个顶点和依赖边至多处理一次。输入图占 V+E,状态、路径和结果各最多 V。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
static List<String> order(Map<String,List<String>> graph) {
List<String> result = new ArrayList<>();
Map<String,Integer> color = new HashMap<>();
Deque<String> path = new ArrayDeque<>();
// 排序只保证示例输出稳定,不模拟真实容器创建顺序。
for (String name : new TreeSet<>(graph.keySet()))
visit(name, graph, color, path, result);
return List.copyOf(result);
}
static void visit(String name, Map<String,List<String>> graph,
Map<String,Integer> color, Deque<String> path, List<String> result) {
if (!graph.containsKey(name)) throw new IllegalArgumentException("missing: " + name);
int state = color.getOrDefault(name, 0);
if (state == 2) return; // 已完成的共享依赖只输出一次。
if (state == 1) throw new IllegalArgumentException("cycle: " + path + " -> " + name);
color.put(name, 1);
path.addLast(name);
for (String dependency : graph.get(name))
visit(dependency, graph, color, path, result);
path.removeLast();
// 依赖全部完成后才允许发布当前节点。
color.put(name, 2);
result.add(name);
}
static void eq(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) throw new AssertionError(actual);
}
static void fails(String text, Runnable test) {
try { test.run(); throw new AssertionError("expected failure"); }
catch (IllegalArgumentException e) {
if (!e.getMessage().contains(text)) throw new AssertionError(e);
}
}
public static void main(String[] args) {
eq(List.of(), order(Map.of()));
eq(List.of("mapper","service","controller"), order(Map.of(
"controller",List.of("service"),"service",List.of("mapper"),"mapper",List.of())));
eq(List.of("d","b","c","a"), order(Map.of(
"a",List.of("b","c"),"b",List.of("d"),"c",List.of("d"),"d",List.of())));
fails("missing", () -> order(Map.of("a",List.of("b"))));
fails("cycle", () -> order(Map.of("a",List.of("b"),"b",List.of("a"))));
fails("cycle", () -> order(Map.of("a",List.of("a"))));
System.out.println("PASS 6 dependency graph tests");
}
}这些特殊情况也要试一试
- 空图返回空列表;单节点无依赖能够独立完成。
- 缺失依赖立即失败,不能默默创建一个占位对象继续运行。
- 自环和多节点环都报告错误;重复依赖只输出一次。
- 极深依赖链可能耗尽 Java 调用栈,生产级图工具应改用显式栈。
进阶选读 · 把循环报告升级为强连通分量诊断
场景大型模块中有多个相互独立的环,只报第一个环不足以规划重构。
做法使用 Tarjan 或 Kosaraju 分解强连通分量;大于一个节点的分量及含自环的单节点都是循环簇,再把分量压缩成 DAG 分析模块边界。
复杂度强连通分量计算 O(V+E),若为了展示排序则额外 O(V log V)。
好处与代价完整图诊断更有帮助,但静态图无法直接覆盖 ObjectProvider、运行时查找或工厂方法内部隐含依赖,应明确采集范围。
05项目里怎么用
排查装配错误时,先用异常中的注入点确定所需类型,再核对定义是否注册、是否受条件配置限制,以及候选是否被限定符过滤。手动 new 一个 Service 可能解决编译,却跳过代理和生命周期,后续事务或校验行为会不一致。修复应落在配置或依赖设计中。
服务实例尽量保持请求无状态。把 userId、查询条件和分页结果放在局部变量中,把数据库访问器等协作者保存在 final 字段中。若必须共享缓存或计数器,要单独选择并发结构、过期策略与资源上限;singleton 本身不提供同步。
单元测试直接向构造器传入手写替身即可验证业务分支。容器测试用于验证扫描、限定符与代理是否按配置工作。两种测试回答不同问题:前者通过不能证明 Bean 已注册,后者启动成功也不能证明所有业务输入符合规则。
容易踩的坑
- 把接口被引用误当成接口已有 Bean 实现。
- 通过修改字段名偶然选中候选,却没有记录依赖选择规则。
- 将请求用户存进共享 Service 字段,引发跨请求串数据。
- 用允许循环引用的开关掩盖职责环,未验证代理和创建时机。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 构造器注入 | 必需依赖显式、易于离线测试。 | 循环依赖及职责过宽会立即暴露。 | 业务服务和稳定必需协作者。 |
| Setter 注入 | 便于表达可选项和可变配置。 | 需要约束未设置时的合法状态。 | 有合理默认值的可选协作者。 |
ObjectProvider 按需获取 | 可以延迟查找并获取不同作用域实例。 | 运行时失败与容器耦合更明显。 | 确实需要延迟或多实例获取的边界。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01IoC 必须使用注解吗?
展开参考答案与推导
不必须。手动构造注入也是外部装配,Spring 还支持显式配置和工厂方法。
02单例 Bean 就是一个类全 JVM 一个对象吗?
展开参考答案与推导
不是。通常按容器和 Bean 定义划分,同类可有多个实例。
03构造器 final 字段能保证服务线程安全吗?
展开参考答案与推导
只能固定引用,不能保证其所指对象的可变状态安全。
换个情况,还会处理吗
01Service 依赖两个同类型支付实现,启动歧义怎么办?
展开参考答案与推导
确认需要一个还是全部;前者用明确限定或唯一首选,后者注入集合并建立业务路由。
02Bean 在单元测试中正常,应用却找不到它?
展开参考答案与推导
构造器测试不涉及注册;检查扫描包、配置导入、条件和 Mapper 注册方式。
03A 与 B 构造器互相依赖应怎样改?
展开参考答案与推导
找出双向职责,提取共享协作者或上移编排;延迟注入只能作为有解释的时机调整。
写代码试一试
01加入独立节点 x,输出必须满足什么?
- 输入 / 条件
a→b,b 无依赖,x 无依赖- 输出 / 预期
每个节点一次,且 b 在 a 前
提示 合法排序未必唯一。
展开参考答案与推导
保留依赖先完成的不变量;独立节点顺序可按名称固定。
02如何测试共享依赖不会重复输出?
- 输入 / 条件
a→c,b→c,c 无依赖- 输出 / 预期
结果只包含一次 c
提示 检查完成状态。
展开参考答案与推导
断言结果长度为三且 c 的频次为一,再断言 c 在 a、b 前。
03如何避免超长链递归溢出?
- 输入 / 条件
十万个节点构成单链- 输出 / 预期
正确顺序或受控规模拒绝
提示 显式维护访问帧。
展开参考答案与推导
用包含节点和下一个依赖下标的栈替代递归,保留三色与退出时输出规则。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能用构造器表达协作者,并区分单例作用域与线程安全保证。
本篇重点加练
重点加练依赖图和容器作用域,检查对象创建与共享边界是否符合真实需求。
- 构造器依赖
- singleton 的容器范围
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
一个订单服务手工创建邮件发送器,现希望替换为测试发送器。分别画出手工组装与容器组装的对象图,列出 Bean 定义应表达的信息。
- 提交内容
- 两份组装图与注册职责说明。
- 怎样算完成
- 创建职责与业务调用分开
- 不把
IoC限定为某种注解
练到的知识点IoC 与依赖注入 · Bean 定义和注册
- 作业 02编程实践
用单构造器实现
OrderService,依赖ReceiptSender接口;不用启动 Spring 先以两个自写实现验证成功发送和失败传播,再给出容器注册版本。- 提交内容
- Java 类、测试、自写输入边界案例和两种启动证据。
- 怎样算完成
- 依赖显式且可替换
- 两种组装遵守同一业务合同
练到的知识点构造器依赖 · 单构造器注入
综合应用
- 作业 03排错实验
建立 app 和 outside 两个包,在练习项目把一个实现放在扫描范围外,并另外构造接口有两个候选实现的情形;分别记录启动失败与修复证据。
- 提交内容
- 包结构、最小配置、两组错误记录及恢复测试。
- 怎样算完成
- 两个故障独立复现
- 修复理由能对应注册证据
练到的知识点组件扫描范围 · 接口与实现注册
- 作业 04面试场景
同 JVM 中创建两个独立
ApplicationContext,均注册计费服务;服务还保存“当前用户”字段。说明应该如何验证实例范围以及并发请求的数据隔离。- 提交内容
- 实例关系图和并发验收设计。
- 怎样算完成
- 容器范围与线程安全分别论证
- 请求状态的所有权明确
练到的知识点singleton 的容器范围 · 无请求状态服务设计
- 作业 05设计评审
支付服务依赖短信服务,短信服务又依赖支付服务查询状态。根据最小业务场景拆分职责,给出依赖图和测试接口,不仅修改注入方式消除启动错误。
- 提交内容
- 重构前后依赖图与行为验收案例。
- 怎样算完成
- 重构保留业务行为
- 循环原因得到解释
- 不引入隐藏服务定位依赖
练到的知识点构造器依赖 · 组件扫描范围 · 接口与实现注册 · 无请求状态服务设计
- 作业 06编程实践
建立两个小容器,各注册同一服务与不同消息前缀依赖;验证容器内重复获取、容器间获取和业务行为的关系,再关闭其中一个。
- 提交内容
- 本地工程、自写实例及行为测试和运行证据。
- 怎样算完成
- 对象身份与输出行为分别验证
- 容器生命周期可区分
练到的知识点构造器依赖 · singleton 的容器范围
- 作业 07设计评审
生成报表需要 Clock、编号生成器和存储接口,要求纯业务测试不启动 Spring。设计构造依赖与组装入口,列出是否应由服务自行创建的对象。
- 提交内容
- 对象图、构造签名和测试组装代码。
- 怎样算完成
- 关键外部依赖可控制
- 依赖生命周期有说明
练到的知识点构造器依赖
- 作业 08面试场景
后台管理和公开 API 在同 JVM 内各有应用上下文,团队依赖单例 Bean 维护一份“全站开关”。列出验证实例数量与更新传播所需证据。
- 提交内容
- 容器关系图和开关更新验收。
- 怎样算完成
- 全站范围有明确部署前提
- 共享需求不只由类名判断
练到的知识点singleton 的容器范围
进阶挑战 · 掌握前置后选做
- 作业 09编程实践
完成图算法后:实现小型构造器依赖规划器,输入 A→B、A→C、B→D、C→D,并支持孤立节点与循环输入;只输出构建计划,不调用反射实例化。
- 提交内容
- Java 程序、随机小图参考测试、边界测试与运行证据。
- 怎样算完成
- 共享依赖处理一致
- 环与缺失节点政策明确
- 复杂度与图表示相符
练到的知识点Bean 定义和注册 · 构造器依赖 · singleton 的容器范围
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
报表服务原来只运行一个容器,现在允许每位用户创建独立工作空间。重新评审哪些依赖可共享、哪些应隔离,并比较资源预算与测试复杂度。
- 提交内容
- 按依赖划分的共享政策和两工作空间样本。
- 怎样算完成
- 选择依据来自状态与成本
- 隔离目标有具体观察指标
练到的知识点构造器依赖 · singleton 的容器范围
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Spring Framework:IoC 容器 (新标签页)↗
按对象定义、依赖注入和容器扩展章节阅读,建立 Bean 管理的完整视角。
- 实践参考文档英文
Spring Framework:Bean 作用域 (新标签页)↗
比较 singleton、prototype 与请求作用域,检查对象状态和销毁责任。
- 深入规范英文
Java 17 语言规范:类、方法与记录类 (新标签页)↗
按需阅读构造器、方法重载、static
/final、枚举和 record 的定义与约束。 - 深入参考文档英文
Spring 6.2:依赖注入 (新标签页)↗
阅读构造器、依赖解析和循环依赖,区分装配职责与业务职责。
- 深入参考文档英文
Spring 6.2:Bean 作用域 (新标签页)↗
核对 singleton 的容器边界及不同作用域对象的交互。
- 深入参考文档英文
Spring v6.2.11 候选解析源码 (新标签页)↗
从
doResolveDependency追踪候选收集、唯一选择与结果类型校验。