Spring IoC 与依赖注入
一个订单服务需要商品目录,商品目录需要数据访问对象。把这些关系写成构造器参数,就得到一张有方向的对象图:
Orders
└── Catalog
└── Repository依赖注入(DI)由外部为对象提供协作者;控制反转(IoC)把组装、初始化和释放交给容器。业务类仍是普通 Java 类。Spring 根据配置创建对象,把已经解析的依赖交给构造器或属性,再保存可供其他组件使用的引用。
创建第一个 ApplicationContext
容器中的定义与对象
Spring 把受它创建或管理的对象称为 Bean。一个 BeanDefinition 描述怎样取得 Bean:使用哪个类或工厂方法、传入哪些依赖、采用什么作用域、何时初始化和怎样关闭。定义注册后可以暂时没有实例,例如懒加载 Bean 还没有第一次被获取。
ApplicationContext
├── BeanFactory
│ ├── BeanDefinition:对象的创建配置
│ ├── 依赖解析:为注入点选择候选
│ └── 实例管理:单例缓存、scope、创建与销毁
├── Environment:属性与 profile
├── ResourceLoader:资源定位
├── ApplicationEventPublisher:应用事件
└── MessageSource:消息与国际化BeanFactory 提供容器基本契约,ApplicationContext 增加应用级服务并组织启动过程。大多数应用使用后者;只创建一个裸 DefaultListableBeanFactory 时,注解处理器、事件和自动生命周期不会凭空出现。容器概述
配置入口可以是 XML、扫描到的组件、@Configuration 中的 @Bean 方法,或程序注册。它们最终提供定义,业务类不必同时使用所有方式。Java 配置便于显式表达少量组件;扫描适合按包组织的业务组件;编程注册常用于基础设施集成。Java 配置
Linux 与 Docker 中的完整项目
实验使用 Spring Framework 7.0.9,与 Spring Boot 4.1.1 的依赖管理一致;构建镜像为 Maven 3.9.12 / Temurin 25,字节码目标是 Java 17。这里直接启动 Spring Context,没有 Web 服务器,也不依赖 Boot 自动配置。Boot 依赖版本
下载完整实验源码,用 Linux 普通账号解压到新目录。宿主机需要 Docker Engine、unzip,当前账号已有操作 Docker 的权限;Docker daemon 权限很高,不要为实验挂载宿主根目录或 Docker socket。构建容器以当前 UID/GID 运行,Maven 缓存和输出目录均由该账号创建。
mkdir spring-ioc-lab
unzip spring-framework-ioc-container-lab.zip -d spring-ioc-lab
cd spring-ioc-lab
mkdir -p .m2
IMAGE=maven:3.9.12-eclipse-temurin-25
docker pull "$IMAGE"目录中有 pom.xml、FirstContext.java 和扩展实验 IocLab.java。第一个程序的完整源码位于 src/main/java/example/FirstContext.java:
package example;
import org.springframework.context.annotation.*;
public class FirstContext {
record Catalog(String prefix) { String find(int id) { return prefix + id; } }
record Orders(Catalog catalog) { String show(int id) { return catalog.find(id); } }
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean Catalog catalog() { return new Catalog("item-"); }
@Bean Orders orders(Catalog catalog) { return new Orders(catalog); }
}
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(Config.class)) {
var orders = context.getBean(Orders.class);
System.out.println(orders.show(42));
System.out.println("sameCatalog=" + (orders.catalog() == context.getBean(Catalog.class)));
}
}
}@Bean 方法的返回对象交给容器管理。创建 Orders 时,Spring 解析方法参数 Catalog catalog,查找匹配对象再调用工厂方法。proxyBeanMethods=false 表示配置方法之间按普通 Java 方法语义调用;这里依赖通过参数传入,没有依赖方法互调拦截。
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId><artifactId>ioc-container-lab</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>7.0.9</version></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version><configuration><parameters>true</parameters></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><version>3.9.0</version></plugin>
</plugins></build>
</project>编译参数 parameters=true 保留方法参数名,供需要参数名回退的注入场景使用;它不能代替明确限定符。以下容器既构建项目,也运行第一次调用:
docker run --rm --user "$(id -u):$(id -g)" \
-e MAVEN_CONFIG=/tmp/maven \
-v "$PWD:/work" -v "$PWD/.m2:/cache" -w /work "$IMAGE" \
sh -ec 'mvn -B -ntp -Duser.home=/tmp -Dmaven.repo.local=/cache \
clean package dependency:build-classpath -Dmdep.outputFile=target/classpath.txt
java -cp "target/classes:$(cat target/classpath.txt)" example.FirstContext'应用输出:
item-42
sameCatalog=true第一行来自业务调用,第二行比较 Orders 持有的引用和容器返回的引用,两者是同一个单例对象。try-with-resources 在调用后关闭 Context,触发它管理的单例销毁。程序结束不留下监听端口或后台服务。
首次构建需要 Maven 仓库访问。企业网络使用组织批准的 Maven settings.xml,只读挂载并用 mvn -s /config/settings.xml 指定;镜像使用可信私有仓库中的相同版本,离线环境可提前导入镜像和准备完整依赖缓存。不要把账号或仓库令牌放进源码 ZIP。缓存完整后可加 mvn -o 禁止联网。出现 Could not resolve artifact 时先处理仓库、代理或证书;出现 Permission denied 时检查挂载目录对当前 UID 的写权限,两者都发生在容器创建业务对象之前。Maven settings
refresh 怎样组织启动
new AnnotationConfigApplicationContext(Config.class) 包含注册配置与 refresh()。如果需要设置属性、父 Context 或覆盖策略,先使用无参构造器,在 refresh() 之前完成配置。
注册配置类
→ 准备 BeanFactory
→ 执行定义级后处理器
→ 注册实例级 BeanPostProcessor
→ 初始化事件等 Context 服务
→ 创建非懒单例及其依赖
→ 完成刷新,发布 ContextRefreshedEvent预实例化会在启动时触发普通单例创建,提前发现多数装配错误;关键 Bean 还应实际获取和调用,确认可用。懒 Bean 仍会注册定义,但只有被需要时才创建;如果非懒单例直接依赖它,这份需求就在启动期间发生。初始化中的网络连接也会拖住依赖它的对象,因此构造器尽量只完成对象自身状态,外部预热应有独立的时间限制和失败处理。懒加载
注入点怎样选中一个对象
类型、限定符与默认实现
构造器参数、工厂方法参数和字段会形成依赖描述信息,包含声明类型、泛型、注解、是否必需以及可能的名称。只有一个构造器时,通常无需额外写 @Autowired。构造器让必需依赖在对象创建时就可见;字段注入发生得更晚,而且普通 new 得到的对象不会自动填充字段。注入方式
单值依赖的语义是“获得一个匹配者”。以下选择手段承担不同职责:
| 手段 | 作用 |
|---|---|
| 声明类型与泛型 | 限制可赋值候选,例如 Store<Order> 与 Store<User> |
@Qualifier | 对类型候选进一步限定;也可定义业务专用限定注解 |
@Primary | 多个合格候选中指定优先单值候选 |
@Fallback | 让标注的实现作为回退,优先考虑非回退候选 |
| 注入点名称 | 条件满足时可与 Bean 名或别名匹配 |
Optional<T> | 明确允许零个候选 |
ObjectProvider<T> | 把获取时间交给调用者,仍服从容器候选和 scope 规则 |
@Primary 不会强行把已被限定符排除的候选重新纳入。@Fallback 从 Framework 6.2 起可用;它用于实现选择,和网络请求失败后的业务降级没有直接关系。Primary 与 Fallback、限定符
扩展实验定义 email 与 sms 两个 Channel,将 email 标为 Primary,再让 Selected 的参数使用 @Qualifier("sms")。在同一解压目录运行:
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.IocLab candidates'primary=email qualified=sms collection=2
ambiguous=2
optionalMissing=null第一组 Context 的单值按类型查询选中 email,限定注入得到 sms,多值查询仍得到两个实现。第二组去掉优先标记后,按类型查询抛出 NoUniqueBeanDefinitionException;程序捕获指定异常并检查候选数为 2。最后一个 provider 查不到 Ticket,getIfAvailable() 返回 null。它只容忍缺失,多个候选仍可能报歧义。
单值、多值和延迟获取走不同分支
DefaultListableBeanFactory.resolveDependency 先识别 Optional、Provider 以及延迟解析入口。doResolveDependency 可以直接处理已知快捷依赖、@Value 建议值和名称快捷匹配;集合、数组、Map 有多值解析分支。不是所有请求都从完整扫描候选开始。
需要完整候选选择时,工厂利用 DependencyDescriptor 取得类型,查询本容器和祖先中的名称,再由 AutowireCandidateResolver 检查限定条件。多个单值候选进入 determineAutowireCandidate:优先匹配 Primary(并考虑唯一非 Fallback),随后处理依赖名称/建议名称、最高优先级、唯一 default candidate 和可解析依赖等分支。不同入口的快捷路径会省掉部分工作,不能把这段简化为“最后按名字随机挑一个”。7.0.9 候选解析实现
注入点
├── Optional / Provider / @Lazy:创建对应的获取方式
├── 已知值或合法快捷匹配:直接解析
├── 数组 / 集合 / Map:收集多个候选
└── 单值:候选为零 → 缺失处理
候选唯一 → 获取对象
候选多个 → 优先选择;仍不能唯一则报告歧义注入 List<Channel> 表达“使用全部匹配策略”,适用于处理链;Map<String,Channel> 的键是 Bean 名。列表和数组可以结合 Ordered 或 @Order 排序。排序作用在消费次序上,通常不负责控制单例初始化次序。希望只选择一个策略时,别通过“取列表第一项”隐藏歧义。自动注入
泛型同样能参与限定。方法如果只声明返回原始 Store,会损失容器提前预测 Store<Order> 的信息;保留具体泛型返回类型比运行时猜对象更可靠。候选缺失时同时检查类型与泛型,不能只看类是否实现了同名接口。
作用域与引用的存活时间
直接持有和每次获取
| Scope | 创建与复用 | 关闭 |
|---|---|---|
| singleton | 每个容器、每个定义通常一份实例 | Context 跟踪其销毁回调 |
| prototype | 每次向容器请求时创建 | 容器不跟踪完整后续生命周期,使用者负责释放 |
| request / session | 按 Web 请求或会话复用 | Web scope 触发相应销毁 |
| application / websocket | 依赖 Web 应用或会话基础设施 | 随对应上下文生命周期 |
singleton 表达容器内对象复用,不给业务字段加锁。多个请求线程仍可能同时调用同一个 Bean。Web scope 也要求对应上下文存在;在普通命令行 Context 中写 @Scope("request") 并不会建立 HTTP 请求。Bean Scope
一个 singleton 构造器直接注入 prototype 时,只在创建该 singleton 的那一次解析出 prototype,随后持续持有它。需要每次新对象时,保存 ObjectProvider<Ticket>,业务使用时调用 getObject()。
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.IocLab scope'输出 directPrototypeHeld=true providerCreatesNew=true。实验分别比较同一个 Desk 保存的直接引用,以及两次 provider 获取的对象。若 prototype 持有文件或连接,使用者还应在每次业务调用结束时关闭它,不能等待 Context 帮忙收尾。
scoped proxy 则保存一个代理入口,每次方法调用按当前 scope 找目标。request scope 被 singleton 使用时很常见:代理可以长期存在,目标属于当前请求。移到线程池后,请求上下文未必存在;应传递必要的值,而不是把整个请求对象作为后台任务状态。
懒解析把错误移到使用点
@Lazy 放在定义上控制目标初始化;放在注入点上可创建延迟解析代理。Provider 让调用位置更显式。二者适合可选或昂贵依赖,但缺失候选、初始化异常和首次耗时也会移到实际获取时。
关键服务如果必须具备某组件,启动期确认通常更便于部署失败后回滚。辅助指标实现可使用 Optional 或 no-op;审计、鉴权这类必需能力不能因为注入可选而静默失效。选择延迟之前,先明确缺失时业务应该返回什么。
工厂产品、父子容器与依赖故障
一个名字可以取得产品,另一个入口取得工厂
FactoryBean<T> 是容器管理的工厂 Bean,getBean("product") 默认取得其产品,getBean("&product") 才取得工厂自身。工厂的 scope 与 isSingleton() 描述的产品复用是两个问题;产品类型通过 getObjectType() 暴露,类型信息不足会影响提前查找。FactoryBean
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.IocLab factory'输出 product=String factory=ProductFactory。这解释了 Mapper、代理工厂等基础设施中“定义的类和注入的类型不同”的现象。Context、BeanFactory、ResourceLoader 等还可作为可解析依赖注入,它们不一定对应普通业务 BeanDefinition;手工 registerSingleton 注册的现成对象也没有等价的创建配方。
父可被子查找,父不能反向寻找子
父 Context 通常保存共享服务,子 Context 保存局部组件。子容器查找时可以委托父容器;父容器没有从一个子容器向另一个子容器扫描的能力。同名子定义可遮蔽父名称。普通本地定义枚举与包含祖先的查询 API 结果也可能不同。BeanFactory 层级契约
docker run --rm --user "$(id -u):$(id -g)" --network none \
-v "$PWD:/work:ro" -v "$PWD/.m2:/cache:ro" -w /work "$IMAGE" \
sh -ec 'java -cp "target/classes:$(cat target/classpath.txt)" example.IocLab hierarchy'childSeesParent=true parentSeesChild=false shadow=2关闭子容器不会替代父容器的关闭。实验的 try 资源按逆序关闭,先 child 后 parent;父仍有其他使用者时,应由创建父容器的组件管理它。
从失败 Bean 回到注入点
启动失败常被包装成 UnsatisfiedDependencyException 或 BeanCreationException。保留异常 cause 链,找到最内层的缺失、歧义、构造异常或作用域错误,再看外层提供的 Bean 名和参数位置。
| 首个有效线索 | 需要核对的对象 | 修复与复测 |
|---|---|---|
| NoSuchBeanDefinitionException | 注入类型、限定符、扫描包、profile 和定义来源 | 注册所需组件或修正条件;重跑相同 Context |
| NoUniqueBeanDefinitionException | 全部合格候选与选择标记 | 明确 Qualifier/Primary 或改为有意的多值依赖;保留反例测试 |
| BeanCurrentlyInCreationException | 构造器/属性依赖的最小环 | 先调整依赖方向,按循环依赖复现 |
| ScopeNotActiveException | 当前线程与 Web scope | 修正调用位置或传递值;正常请求与后台任务分开测试 |
| BeanNotOfRequiredTypeException | 声明类型、工厂产品和代理接口 | 按公开接口注入,确认最终对象类型 |
| 工厂方法或 init 抛异常 | 原始 cause 和已获取资源 | 修配置或初始化代码,关闭残留资源后新建 Context |
开发期可在刷新后的 Context 上临时打印指定类型候选,不必输出整个对象状态:
for (String name : context.getBeanNamesForType(Channel.class)) {
if (!context.getBeanFactory().containsBeanDefinition(name)) {
System.out.println(name + " source=registered singleton");
continue;
}
var definition = context.getBeanFactory().getBeanDefinition(name);
System.out.println(name + " scope=" + definition.getScope()
+ " source=" + definition.getResourceDescription());
}这段查询枚举当前 Context 的类型候选,可能包括手工注册的现成单例,因此读取定义前先判断定义是否存在;父容器结果需要另用包含祖先的查询。来源为空也可能是程序注册,应在注册处补可识别来源。不要在 BeanFactoryPostProcessor 中为打印对象状态调用 getBean(),那会改变本来要观察的创建顺序。
实验退出后没有常驻容器。target/ 为本轮构建产物,.m2/ 为可复用依赖缓存;需要清理时确认仍位于独立实验目录,再仅删除这两个目录,保留下载的源码。
权威资料与规范地址
可按接口、注解和实现类查阅详细契约。
