跳到主内容
创建时间: 2026-09-16最后更新: 2026-09-16

前面的 Java 基础章节已经使用 new 创建对象,也说明了类可以通过构造方法依赖具体类或接口。注解与反射让框架能够读取类型元数据,但它们不会自动决定对象怎样创建。控制器、服务和配置对象互相依赖以后,谁负责创建它们、按照什么顺序创建、如何把它们连接起来,就会成为一个独立问题。

Spring 使用 IoC 容器管理这张对象依赖图。本章继续使用心跳接口,从手动组装开始,逐步理解控制反转、依赖注入、容器、配置元数据、启动阶段和依赖解析。

1. 对象依赖图

先创建一个普通 record 保存心跳设置。它目前没有任何 Spring 注解:

src/main/java/cn/usehook/spring/config/HealthProperties.java
1
package cn.usehook.spring.config;
2
3
public record HealthProperties(String status, String service) {}

心跳接口由三个主要对象协作:

对象职责直接依赖
HealthProperties保存心跳设置无
HealthService生成心跳结果HealthProperties
HealthController处理 HTTP 请求HealthService

不使用容器时,可以在程序入口手动创建并组装它们:

src/main/java/cn/usehook/spring/basics/ManualApplication.java
01
package cn.usehook.spring.basics;
02
03
import cn.usehook.spring.config.HealthProperties;
04
import cn.usehook.spring.controller.HealthController;
05
import cn.usehook.spring.health.HealthService;
06
07
public class ManualApplication {
08
09
public static void main(String[] args) {
10
HealthProperties properties = new HealthProperties("UP", "spring");
11
HealthService healthService = new HealthService(properties);
12
HealthController healthController = new HealthController(healthService);
13
}
14
}

创建顺序由构造器依赖决定:先创建没有依赖的配置对象,再创建服务,最后创建控制器。

dependency-graph.txt
1
HealthController
2
└── 依赖 HealthService
3
└── 依赖 HealthProperties

负责创建整张依赖图的位置称为组合根。小型程序可以在 main() 中直接完成,项目扩大后则会遇到实例复用、条件创建、初始化顺序和资源关闭等问题。IoC 容器把对象组装抽离成统一机制,但没有改变这些依赖本身。

2. IoC 与依赖注入

IoC 是 Inversion of Control 的缩写,通常译为「控制反转」。这里反转的是应用组件的创建、查找和组装控制权。

控制器主动创建依赖时,具体实现和创建参数都被写死在控制器中:

HealthController.java
1
private final HealthService healthService = new HealthService(
2
new HealthProperties("UP", "spring")
3
);

改为构造器声明依赖后,控制器只表达自己需要什么:

HealthController.java
1
private final HealthService healthService;
2
3
public HealthController(HealthService healthService) {
4
this.healthService = healthService;
5
}

两种方式的区别是:

问题主动创建IoC 容器
谁创建服务控制器或启动代码容器
谁选择对象应用代码容器根据配置元数据解析
谁把服务交给控制器应用代码容器
控制器是否了解创建过程了解全部细节只声明依赖类型

依赖注入(Dependency Injection,DI)是实现 IoC 的一种方式。IoC 描述「组件不再主动控制依赖来源」这一结果,DI 描述「外部把依赖传给组件」这一过程。

获得依赖以后,控制器仍然会主动调用 healthService.check()。被反转的是依赖获取,不是正常的业务方法调用。IoC 也不要求消除所有 new,HealthStatus、请求 DTO 和响应 DTO 仍然可以在一次调用中直接创建。

构造器注入把必需依赖写进类型结构,也让普通测试可以直接创建对象。让业务类注入 ApplicationContext 后到处调用 getBean(),会重新把依赖查找隐藏进方法,不适合作为常规业务写法。

3. Spring 容器

Spring 容器的核心接口是 BeanFactory 和 ApplicationContext:

接口主要能力
BeanFactory保存 Bean 定义、创建 Bean、解析依赖并提供 Bean
ApplicationContext在前者基础上增加环境、资源、事件、国际化和生命周期等能力

Spring Boot 应用通常使用 ApplicationContext。SpringApplication.run() 会创建适合当前应用类型的上下文,并返回可配置接口:

src/main/java/cn/usehook/spring/Application.java
01
package cn.usehook.spring;
02
03
import org.springframework.boot.SpringApplication;
04
import org.springframework.boot.autoconfigure.SpringBootApplication;
05
import org.springframework.context.ConfigurableApplicationContext;
06
07
@SpringBootApplication
08
public class Application {
09
10
public static void main(String[] args) {
11
ConfigurableApplicationContext context = SpringApplication.run(
12
Application.class,
13
args
14
);
15
System.out.println(context.getClass().getName());
16
}
17
}

具体上下文实现由 Spring Boot 根据应用是 Servlet Web、响应式 Web 还是非 Web 应用决定。业务代码不应该依赖某个具体实现类。

一个 JVM 可以拥有多个上下文,每个上下文都有自己的对象边界。虽然可以执行 context.getBean(HealthService.class) 主动取得 Bean,但普通 Controller 和 Service 应继续通过构造器声明依赖。主动查找主要用于框架集成、诊断或教学观察。

4. 配置元数据

容器不会管理 classpath 中的每一个类。它先读取配置元数据,确定哪些对象应该成为 Bean、怎样创建以及需要哪些依赖。

当前项目先使用三种来源:

来源示例作用
组件扫描@Service、@RestController发现项目维护的组件类
Java 配置@Configuration、@Bean显式声明对象创建方法
自动配置@EnableAutoConfiguration根据依赖、配置和现有 Bean 提供框架组件

HealthService 是项目自己的业务组件,使用 @Service:

src/main/java/cn/usehook/spring/health/HealthService.java
01
package cn.usehook.spring.health;
02
03
import cn.usehook.spring.config.HealthProperties;
04
import org.springframework.stereotype.Service;
05
06
@Service
07
public class HealthService {
08
09
private final HealthProperties properties;
10
11
public HealthService(HealthProperties properties) {
12
this.properties = properties;
13
}
14
15
public HealthStatus check() {
16
return new HealthStatus(properties.status(), properties.service());
17
}
18
}

普通 HealthProperties 没有组件注解,可以由配置类中的 @Bean 方法创建:

src/main/java/cn/usehook/spring/config/HealthConfiguration.java
01
package cn.usehook.spring.config;
02
03
import org.springframework.context.annotation.Bean;
04
import org.springframework.context.annotation.Configuration;
05
06
@Configuration
07
public class HealthConfiguration {
08
09
@Bean
10
public HealthProperties healthProperties() {
11
return new HealthProperties("UP", "spring");
12
}
13
}

容器调用 healthProperties() 并管理它返回的对象。这里暂时使用固定值,只为观察对象组装;后面的「配置绑定」会把它替换为 application.yaml 中的值。

入口类的 @SpringBootApplication 组合了配置类、组件扫描和自动配置。因为入口位于 cn.usehook.spring,它的子包中的 HealthConfiguration、HealthService 和 HealthController 都在扫描范围内。

自动配置还会检查 classpath、配置和容器中已有的 Bean。例如引入 Web MVC Starter 后,MVC 自动配置才会参与;应用已经提供某类自定义 Bean 时,对应默认配置通常会退让。

5. 容器启动

SpringApplication.run() 会准备 Environment、创建并刷新 ApplicationContext。可以把主要阶段简化为:

context-startup.txt
01
准备 Environment
02
创建 ApplicationContext
03
读取配置元数据
04
注册 Bean 定义
05
处理 Bean 定义
06
注册 Bean 实例后置处理器
07
按依赖关系创建非懒加载单例 Bean
08
执行初始化并完成必要的代理包装
09
启动 Web 服务器
10
发布应用已就绪事件

这个过程可以概括为「先收集定义,再创建对象」。Bean 定义描述类型、名称、创建方式、依赖、作用域和生命周期,它不是最终实例。

BeanFactoryPostProcessor 在普通 Bean 创建前处理定义;BeanPostProcessor 在实例初始化前后参与处理。依赖注入、生命周期注解和许多 AOP 代理能力都建立在这些扩展点上。

因此,最终注入的对象有时可能是容器生成的代理,而不只是构造器直接产生的原始对象。后面的事务章节会看到代理如何在 Service 方法前后开启和结束事务。

默认的非懒加载单例通常在上下文刷新时创建。必需 Bean 无法创建时,整个上下文启动失败,不会留下一个依赖不完整却继续接收请求的应用。

6. 依赖解析

容器准备创建 HealthController 时,要沿着构造器参数解析完整依赖:

dependency-resolution.txt
1
创建 HealthController
2
-> 选择构造方法
3
-> 查找 HealthService Bean
4
-> 创建 HealthService
5
-> 查找 HealthProperties Bean
6
-> 调用 @Bean 方法创建配置对象
7
-> 调用 HealthController 构造方法
8
-> 完成初始化与后置处理

只有一个构造方法时,不需要添加 @Autowired。Spring 可以直接选择它,并根据参数类型查找候选对象。

候选结果容器行为
只有一个匹配 Bean直接注入
没有匹配 Bean必需依赖无法满足,启动失败
存在多个匹配 Bean需要进一步确定候选对象
参数是集合可以注入全部匹配对象

同类型候选对象的名称、@Primary 和 @Qualifier 会在下一章展开。本章只需要确认:容器主要依据依赖类型组装对象,构造参数名不能代替清晰的候选规则。

构造器循环依赖无法完成。例如服务依赖发布器,发布器又依赖服务,两个对象都在等待另一个先创建。通常应重新划分职责、提取共同依赖或使用事件打断双向关系,而不是用懒加载掩盖设计问题。

7. 心跳依赖图

当前完整关系由一个 @Bean 和两个扫描组件组成:

health-context.txt
1
HealthConfiguration
2
└── 创建 HealthProperties Bean
3
└── 注入 HealthService Bean
4
└── 注入 HealthController Bean

请求到达时,核心依赖图已经组装完成。Spring MVC 找到控制器,控制器调用服务,服务读取配置对象并创建本次响应数据:

health-request.txt
1
GET /api/v1/health
2
-> 已组装的 HealthController Bean
3
-> 已注入的 HealthService Bean
4
-> 已注入的 HealthProperties Bean
5
-> 新建 HealthStatus 普通对象
6
-> 新建 HealthResponse 普通对象
7
-> JSON 响应
对象由谁创建是否需要容器管理
HealthProperties@Bean 方法与容器需要,作为共享配置依赖
HealthServiceIoC 容器需要,被控制器依赖
HealthControllerIoC 容器需要,被 Spring MVC 注册
HealthStatusHealthService.check()不需要,只保存一次调用结果
HealthResponseController 方法不需要,只保存一次响应数据

IoC 的重点不是让所有对象都成为 Bean,而是让核心协作对象的创建和依赖关系集中受控。下一章继续观察 Bean 定义、Bean 实例、注册来源和候选选择;之后再把当前写死的 HealthProperties 替换为真正的配置绑定。