前面的 Java 基础章节已经使用 new 创建对象,也说明了类可以通过构造方法依赖具体类或接口。注解与反射让框架能够读取类型元数据,但它们不会自动决定对象怎样创建。控制器、服务和配置对象互相依赖以后,谁负责创建它们、按照什么顺序创建、如何把它们连接起来,就会成为一个独立问题。
Spring 使用 IoC 容器管理这张对象依赖图。本章继续使用心跳接口,从手动组装开始,逐步理解控制反转、依赖注入、容器、配置元数据、启动阶段和依赖解析。
1. 对象依赖图
先创建一个普通 record 保存心跳设置。它目前没有任何 Spring 注解:
1package cn.usehook.spring.config;23public record HealthProperties(String status, String service) {}
心跳接口由三个主要对象协作:
| 对象 | 职责 | 直接依赖 |
|---|---|---|
HealthProperties | 保存心跳设置 | 无 |
HealthService | 生成心跳结果 | HealthProperties |
HealthController | 处理 HTTP 请求 | HealthService |
不使用容器时,可以在程序入口手动创建并组装它们:
01package cn.usehook.spring.basics;0203import cn.usehook.spring.config.HealthProperties;04import cn.usehook.spring.controller.HealthController;05import cn.usehook.spring.health.HealthService;0607public class ManualApplication {0809public static void main(String[] args) {10HealthProperties properties = new HealthProperties("UP", "spring");11HealthService healthService = new HealthService(properties);12HealthController healthController = new HealthController(healthService);13}14}
创建顺序由构造器依赖决定:先创建没有依赖的配置对象,再创建服务,最后创建控制器。
1HealthController2└── 依赖 HealthService3└── 依赖 HealthProperties
负责创建整张依赖图的位置称为组合根。小型程序可以在 main() 中直接完成,项目扩大后则会遇到实例复用、条件创建、初始化顺序和资源关闭等问题。IoC 容器把对象组装抽离成统一机制,但没有改变这些依赖本身。
2. IoC 与依赖注入
IoC 是 Inversion of Control 的缩写,通常译为「控制反转」。这里反转的是应用组件的创建、查找和组装控制权。
控制器主动创建依赖时,具体实现和创建参数都被写死在控制器中:
1private final HealthService healthService = new HealthService(2new HealthProperties("UP", "spring")3);
改为构造器声明依赖后,控制器只表达自己需要什么:
1private final HealthService healthService;23public HealthController(HealthService healthService) {4this.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() 会创建适合当前应用类型的上下文,并返回可配置接口:
01package cn.usehook.spring;0203import org.springframework.boot.SpringApplication;04import org.springframework.boot.autoconfigure.SpringBootApplication;05import org.springframework.context.ConfigurableApplicationContext;0607@SpringBootApplication08public class Application {0910public static void main(String[] args) {11ConfigurableApplicationContext context = SpringApplication.run(12Application.class,13args14);15System.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:
01package cn.usehook.spring.health;0203import cn.usehook.spring.config.HealthProperties;04import org.springframework.stereotype.Service;0506@Service07public class HealthService {0809private final HealthProperties properties;1011public HealthService(HealthProperties properties) {12this.properties = properties;13}1415public HealthStatus check() {16return new HealthStatus(properties.status(), properties.service());17}18}
普通 HealthProperties 没有组件注解,可以由配置类中的 @Bean 方法创建:
01package cn.usehook.spring.config;0203import org.springframework.context.annotation.Bean;04import org.springframework.context.annotation.Configuration;0506@Configuration07public class HealthConfiguration {0809@Bean10public HealthProperties healthProperties() {11return 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。可以把主要阶段简化为:
01准备 Environment02创建 ApplicationContext03读取配置元数据04注册 Bean 定义05处理 Bean 定义06注册 Bean 实例后置处理器07按依赖关系创建非懒加载单例 Bean08执行初始化并完成必要的代理包装09启动 Web 服务器10发布应用已就绪事件
这个过程可以概括为「先收集定义,再创建对象」。Bean 定义描述类型、名称、创建方式、依赖、作用域和生命周期,它不是最终实例。
BeanFactoryPostProcessor 在普通 Bean 创建前处理定义;BeanPostProcessor 在实例初始化前后参与处理。依赖注入、生命周期注解和许多 AOP 代理能力都建立在这些扩展点上。
因此,最终注入的对象有时可能是容器生成的代理,而不只是构造器直接产生的原始对象。后面的事务章节会看到代理如何在 Service 方法前后开启和结束事务。
默认的非懒加载单例通常在上下文刷新时创建。必需 Bean 无法创建时,整个上下文启动失败,不会留下一个依赖不完整却继续接收请求的应用。
6. 依赖解析
容器准备创建 HealthController 时,要沿着构造器参数解析完整依赖:
1创建 HealthController2-> 选择构造方法3-> 查找 HealthService Bean4-> 创建 HealthService5-> 查找 HealthProperties Bean6-> 调用 @Bean 方法创建配置对象7-> 调用 HealthController 构造方法8-> 完成初始化与后置处理
只有一个构造方法时,不需要添加 @Autowired。Spring 可以直接选择它,并根据参数类型查找候选对象。
| 候选结果 | 容器行为 |
|---|---|
| 只有一个匹配 Bean | 直接注入 |
| 没有匹配 Bean | 必需依赖无法满足,启动失败 |
| 存在多个匹配 Bean | 需要进一步确定候选对象 |
| 参数是集合 | 可以注入全部匹配对象 |
同类型候选对象的名称、@Primary 和 @Qualifier 会在下一章展开。本章只需要确认:容器主要依据依赖类型组装对象,构造参数名不能代替清晰的候选规则。
构造器循环依赖无法完成。例如服务依赖发布器,发布器又依赖服务,两个对象都在等待另一个先创建。通常应重新划分职责、提取共同依赖或使用事件打断双向关系,而不是用懒加载掩盖设计问题。
7. 心跳依赖图
当前完整关系由一个 @Bean 和两个扫描组件组成:
1HealthConfiguration2└── 创建 HealthProperties Bean3└── 注入 HealthService Bean4└── 注入 HealthController Bean
请求到达时,核心依赖图已经组装完成。Spring MVC 找到控制器,控制器调用服务,服务读取配置对象并创建本次响应数据:
1GET /api/v1/health2-> 已组装的 HealthController Bean3-> 已注入的 HealthService Bean4-> 已注入的 HealthProperties Bean5-> 新建 HealthStatus 普通对象6-> 新建 HealthResponse 普通对象7-> JSON 响应
| 对象 | 由谁创建 | 是否需要容器管理 |
|---|---|---|
HealthProperties | @Bean 方法与容器 | 需要,作为共享配置依赖 |
HealthService | IoC 容器 | 需要,被控制器依赖 |
HealthController | IoC 容器 | 需要,被 Spring MVC 注册 |
HealthStatus | HealthService.check() | 不需要,只保存一次调用结果 |
HealthResponse | Controller 方法 | 不需要,只保存一次响应数据 |
IoC 的重点不是让所有对象都成为 Bean,而是让核心协作对象的创建和依赖关系集中受控。下一章继续观察 Bean 定义、Bean 实例、注册来源和候选选择;之后再把当前写死的 HealthProperties 替换为真正的配置绑定。