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

上一章介绍了 IoC 容器如何读取配置元数据、创建对象并解析依赖。本章把视角移到容器中的对象:普通 Java 类怎样成为 Spring Bean,Bean 定义与实例有什么区别,以及容器存在多个同类型对象时怎样选择。

当前 HealthController、HealthService 和 HealthProperties 仍然共同完成心跳接口。本章只讨论 Bean 的注册与查找,作用域和生命周期放到后面的独立文章。

1. Spring Bean

Spring Bean 是由 Spring IoC 容器创建、装配和管理的对象。它在 Java 语法层面仍然是普通对象,仍然通过类描述结构、通过构造方法创建、通过实例方法完成工作。

下面的类本身只是普通 Java 类型:

src/main/java/cn/usehook/spring/health/HealthService.java
1
package cn.usehook.spring.health;
2
3
public class HealthService {
4
5
public HealthStatus check() {
6
return new HealthStatus("UP", "spring");
7
}
8
}

应用代码主动调用 new HealthService() 时,得到普通对象。给类添加 @Service 并让它处于组件扫描范围内后,容器会为它注册 Bean 定义,再根据定义创建实例:

src/main/java/cn/usehook/spring/health/HealthService.java
01
package cn.usehook.spring.health;
02
03
import org.springframework.stereotype.Service;
04
05
@Service
06
public class HealthService {
07
08
public HealthStatus check() {
09
return new HealthStatus("UP", "spring");
10
}
11
}
管理内容普通对象Spring Bean
创建者应用代码主动 new容器根据 Bean 定义创建
依赖来源调用方手动提供容器解析并注入
实例数量由业务代码决定由 Bean 作用域决定
初始化和销毁业务代码安排可以参与容器生命周期
框架增强原始对象必要时可由代理包装

Spring Bean 不等于传统 JavaBean。JavaBean 通常强调无参构造、属性 getter 和 setter 等约定;Spring Bean 强调对象由 Spring 容器管理,它可以只有一个带参数的构造方法,也不要求为所有字段提供 setter。

2. 定义与实例

类、Bean 定义和 Bean 实例属于三个层次:

概念心跳项目中的示例作用
Java 类HealthService.class描述字段、构造方法和行为
Bean 定义名为 healthService 的容器元数据描述怎样创建和管理对象
Bean 实例注入 Controller 的具体对象真正执行 check()

Bean 定义通常包含类型、名称、创建方式、构造依赖、作用域、懒加载和生命周期回调。一个 .java 文件只提供类型声明,不会自动产生 Bean;容器必须先获得定义,才能决定何时创建对象。

同一个 Java 类可以对应多个 Bean。下面两个方法都返回 Clock,却注册两个名称不同的定义:

src/main/java/cn/usehook/spring/config/TimeConfiguration.java
01
package cn.usehook.spring.config;
02
03
import java.time.Clock;
04
import java.time.ZoneId;
05
import org.springframework.context.annotation.Bean;
06
import org.springframework.context.annotation.Configuration;
07
08
@Configuration
09
public class TimeConfiguration {
10
11
@Bean
12
public Clock utcClock() {
13
return Clock.systemUTC();
14
}
15
16
@Bean
17
public Clock shanghaiClock() {
18
return Clock.system(ZoneId.of("Asia/Shanghai"));
19
}
20
}

容器现在拥有 utcClock 和 shanghaiClock 两个 Bean。它们类型相同但定义不同,也会产生不同实例。反过来,一个 Bean 定义能产生几个实例由作用域决定,而不是由 Java 类决定。

3. Bean 的来源

Spring Boot 项目中的 Bean 主要来自组件扫描、@Bean 方法和自动配置。

组件扫描

入口类的 @SpringBootApplication 会从 cn.usehook.spring 根包开始扫描。项目维护的组件使用与职责匹配的注解:

注解常见职责当前案例
@Component通用组件没有更具体角色的协作对象
@Service业务逻辑HealthService
@Repository数据访问后面的 Repository
@ControllerMVC 页面控制器返回服务器页面的控制器
@RestControllerREST 接口控制器HealthController

这些角色注解都建立在 @Component 之上。使用具体注解不会改变普通 Java 语法,但能让类的职责和框架处理意图更清楚。

@Bean 工厂方法

不能修改源码的第三方类型,或者对象创建过程需要明确参数时,可以使用 @Bean。上一章的普通 HealthProperties 就通过这种方式注册:

src/main/java/cn/usehook/spring/config/HealthConfiguration.java
1
@Configuration
2
public class HealthConfiguration {
3
4
@Bean
5
public HealthProperties healthProperties() {
6
return new HealthProperties("UP", "spring");
7
}
8
}

默认 Bean 名称是方法名 healthProperties,声明类型来自返回类型,实例来自方法返回值。@Bean 方法也可以通过参数声明依赖,容器会在调用方法前先解析这些参数。

一个类通常选择组件扫描或 @Bean 中的一种注册方式。给 HealthService 添加 @Service 后又声明一个返回 HealthService 的 @Bean,会产生两个同类型候选对象;除非业务确实需要两个实例,否则这是重复注册。

自动配置

Spring Boot 根据 classpath、配置和容器中已有对象注册框架 Bean。引入 Web MVC Starter 后,处理请求所需的 MVC 基础设施才会出现。应用主动提供某些自定义 Bean 时,对应默认配置通常会退让。

自动配置产生的对象仍然是 Bean,也遵守相同的名称、依赖解析、作用域和生命周期规则。

4. 名称与候选对象

未显式指定时,组件类通常以首字母小写的类名作为 Bean 名称,@Bean 方法则使用方法名:

注册代码默认名称类型
@Service class HealthServicehealthServiceHealthService
@RestController class HealthControllerhealthControllerHealthController
@Bean Clock utcClock()utcClockClock

构造器注入先根据参数类型寻找候选对象。容器中只有一个 HealthService 时,参数名不必和 Bean 名称一致:

src/main/java/cn/usehook/spring/controller/HealthController.java
1
public HealthController(HealthService service) {
2
this.healthService = service;
3
}

存在两个 Clock 时,仅凭类型无法确定唯一值。可以用 @Primary 指定没有额外条件时的默认候选:

src/main/java/cn/usehook/spring/config/TimeConfiguration.java
01
@Bean
02
@Primary
03
public Clock utcClock() {
04
return Clock.systemUTC();
05
}
06
07
@Bean
08
public Clock shanghaiClock() {
09
return Clock.system(ZoneId.of("Asia/Shanghai"));
10
}

某个使用方明确需要上海时区时,使用 @Qualifier 继续筛选同类型对象:

src/main/java/cn/usehook/spring/health/HealthService.java
1
public HealthService(@Qualifier("shanghaiClock") Clock clock) {
2
this.clock = clock;
3
}

@Qualifier 不是绕过类型检查的字符串查找。容器先确定 Clock 类型候选,再用限定值缩小范围。

需求方式
为大多数注入点提供默认候选@Primary
某个注入点选择特定候选@Qualifier
把公共实现降为备用候选@Fallback

@Fallback 适合框架或公共模块提供默认实现:只要存在普通候选,备用对象就不会成为优先选择。心跳项目只有自己的实现时不需要使用它。

5. 单值与集合注入

只有一个业务实现时,构造器直接声明具体类型通常最清楚,不需要机械地创建同名接口。真正存在多个实现且调用方需要全部对象时,可以注入集合或映射:

src/main/java/cn/usehook/spring/health/ClockRegistry.java
01
package cn.usehook.spring.health;
02
03
import java.time.Clock;
04
import java.util.Map;
05
import org.springframework.stereotype.Component;
06
07
@Component
08
public class ClockRegistry {
09
10
private final Map<String, Clock> clocks;
11
12
public ClockRegistry(Map<String, Clock> clocks) {
13
this.clocks = Map.copyOf(clocks);
14
}
15
}

映射的键是 Bean 名称,值是对应的 Clock。@Primary 只影响单值依赖,不会从集合中移除其他候选。

并非运行期间出现的每个对象都应成为 Bean:

对象是否适合原因
HealthController是由 MVC 调用并依赖服务
HealthService是被控制器依赖的共享业务组件
HealthProperties是被服务共享的配置依赖
Clock是需要统一配置并方便替换测试
HealthStatus通常不是一次心跳调用产生的结果
请求 DTO通常不是属于一次请求,由 MVC 参数解析创建

IoC 的目的不是消灭 new,而是管理组件之间长期存在的依赖。短生命周期值对象、返回值和局部数据继续由业务代码创建。

6. 观察容器

为了验证名称和候选结果,可以临时从启动完成的上下文中读取 Bean:

src/main/java/cn/usehook/spring/Application.java
01
ConfigurableApplicationContext context = SpringApplication.run(
02
Application.class,
03
args
04
);
05
06
HealthService first = context.getBean(HealthService.class);
07
HealthService second = context.getBean("healthService", HealthService.class);
08
Map<String, Clock> clocks = context.getBeansOfType(Clock.class);
09
10
System.out.println(first == second);
11
System.out.println(clocks.keySet());

按类型和按名称取得的是同一个 healthService 引用,因为它当前使用默认单例作用域。getBeansOfType() 返回所有匹配对象,可用于观察多个 Clock 是否都已注册。

这段代码只适合教学和诊断,不应让普通业务类保存 ApplicationContext 再主动查找依赖。业务依赖仍然通过构造器声明,关系才会直接体现在类结构中。

现在已经知道 Bean 怎样注册和选择。下一篇将上一章写死的 HealthProperties 工厂方法替换为外部配置绑定;随后再学习作用域如何决定实例数量,以及容器怎样处理初始化和销毁。