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

「项目配置」章节已经把 app.health.status 和 app.health.service 放进 Spring Environment。经过 IoC 与 Spring Bean 两章后,现在可以继续回答当时留下的问题:Java 对象怎样取得这些值,配置对象又为什么能够被 Service 注入。

本章先用 @Value 读取单个配置,再用 @ConfigurationProperties 把同一前缀下的配置绑定成一个带校验的 Bean。完成后,HealthService 不再依赖写死的状态和服务名称。

1. Environment

Spring Boot 启动时会把配置文件、环境变量、系统属性和命令行参数等属性来源合并到 Environment。覆盖顺序已经在前文确定,因此业务组件读取到的是合并后的最终值。

configuration-flow.txt
1
application.yaml
2
环境变量与命令行参数
3
↓ 合并与覆盖
4
Spring Environment
5
↓ 读取或绑定
6
Spring Bean
7
↓ 构造器注入
8
HealthService

组件可以注入 Environment 并调用 getProperty(),但业务代码会因此反复书写字符串键名和类型转换。更常用的方式是让 Spring 在创建 Bean 时完成注入或结构化绑定。

2. @Value

只有一两个互不相关的值时,可以在构造参数上使用 @Value:

src/main/java/cn/usehook/spring/health/HealthService.java
01
package cn.usehook.spring.health;
02
03
import org.springframework.beans.factory.annotation.Value;
04
import org.springframework.stereotype.Service;
05
06
@Service
07
public class HealthService {
08
09
private final String status;
10
private final String service;
11
12
public HealthService(
13
@Value("${app.health.status}") String status,
14
@Value("${app.health.service}") String service
15
) {
16
this.status = status;
17
this.service = service;
18
}
19
20
public HealthStatus check() {
21
return new HealthStatus(status, service);
22
}
23
}

Spring 创建 HealthService 时解析两个占位符,再把最终字符串传入构造方法。字段可以保持 final,对象创建完成时配置也已经齐全。

@Value 适合少量独立值。随着同一前缀下的配置增多,键名会散落在多个构造参数中,也难以集中表达配置结构和校验规则,这时应该使用配置属性绑定。

3. 配置属性类

心跳配置天然属于同一组。创建 record 描述 app.health 的结构:

src/main/java/cn/usehook/spring/config/HealthProperties.java
01
package cn.usehook.spring.config;
02
03
import jakarta.validation.constraints.NotBlank;
04
import org.springframework.boot.context.properties.ConfigurationProperties;
05
import org.springframework.validation.annotation.Validated;
06
07
@Validated
08
@ConfigurationProperties(prefix = "app.health")
09
public record HealthProperties(
10
@NotBlank String status,
11
@NotBlank String service
12
) {}

prefix 只写公共前缀。Spring 将 app.health.status 绑定到 status,将 app.health.service 绑定到 service,并根据 record 主构造方法创建对象。

属性名使用规范的 kebab-case,例如 request-timeout;Java 组件使用驼峰命名,例如 requestTimeout。Spring Boot 的宽松绑定可以在这些形式之间转换。复杂配置还可以使用 Duration、DataSize、枚举、列表和嵌套 record,让类型本身表达单位与结构。

@Validated 让绑定完成后执行 Bean Validation。这里要求两个值都不能为 null、空字符串或纯空白,配置错误会在应用启动阶段暴露,而不是等到接口收到请求才失败。

配置校验依赖 Jakarta Validation,在 pom.xml 中加入:

pom.xml
1
<dependency>
2
<groupId>org.springframework.boot</groupId>
3
<artifactId>spring-boot-starter-validation</artifactId>
4
</dependency>

4. 注册配置 Bean

@ConfigurationProperties 描述怎样绑定,但配置类还要注册进容器。IoC 章节曾用 HealthConfiguration.healthProperties() 创建固定值;现在删除这个 @Bean 方法,避免容器中同时出现手动对象和配置绑定对象。

当前项目改为在入口类启用配置属性扫描:

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.boot.context.properties.ConfigurationPropertiesScan;
06
07
@SpringBootApplication
08
@ConfigurationPropertiesScan
09
public class Application {
10
11
public static void main(String[] args) {
12
SpringApplication.run(Application.class, args);
13
}
14
}

扫描从 cn.usehook.spring 开始,因此位于 cn.usehook.spring.config 的 HealthProperties 会成为 Bean。它的值来自 Environment,而不是 record 中写死的常量。

只有少数明确的配置类时,也可以在配置类上使用 @EnableConfigurationProperties(HealthProperties.class) 逐个注册。两种方式解决的是同一问题,当前项目采用扫描,不要再通过 @Bean 或其他方式重复注册同一个配置类。

5. 注入 Service

把 HealthService 从两个 @Value 参数改为依赖一个配置对象:

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
}

构造器只声明 Java 类型,不再包含字符串形式的配置键。IoC 容器先创建 HealthProperties,再把它注入 HealthService,最后把服务注入 Controller。

health-configuration-graph.txt
1
Environment 中的 app.health.*
2
-> HealthProperties Bean
3
-> HealthService Bean
4
-> HealthController Bean

Controller 不需要同时注入配置对象。使用什么默认状态和服务名称属于心跳业务实现,HTTP 边界只调用 HealthService。

6. 验证绑定结果

配置类可以通过 Spring Boot 测试验证实际绑定和容器注册:

src/test/java/cn/usehook/spring/config/HealthPropertiesTests.java
01
package cn.usehook.spring.config;
02
03
import org.junit.jupiter.api.Test;
04
import org.springframework.beans.factory.annotation.Autowired;
05
import org.springframework.boot.test.context.SpringBootTest;
06
07
import static org.assertj.core.api.Assertions.assertThat;
08
09
@SpringBootTest(properties = {
10
"app.health.status=UP",
11
"app.health.service=configuration-test"
12
})
13
class HealthPropertiesTests {
14
15
private final HealthProperties properties;
16
17
@Autowired
18
HealthPropertiesTests(HealthProperties properties) {
19
this.properties = properties;
20
}
21
22
@Test
23
void bindsHealthConfiguration() {
24
assertThat(properties.status()).isEqualTo("UP");
25
assertThat(properties.service()).isEqualTo("configuration-test");
26
}
27
}

测试属性的优先级高于应用文件,所以该测试不会依赖开发机器上的实际配置。它验证的是属性合并、绑定、校验和 Bean 注册这一整条路径。

把 app.health.service 临时改为空白并启动测试,配置校验应让 ApplicationContext 创建失败。正常测试不应故意保留失败配置;针对失败绑定的精确断言可以以后使用 ApplicationContextRunner 编写。

配置绑定完成后,项目中的对象关系已经闭合:配置来源由 Environment 管理,配置结构由 HealthProperties 表达,业务服务只依赖类型明确的 Bean。下一篇继续讨论这些 Bean 的作用域与生命周期,之后再学习自动配置、AOP 与 Spring MVC 请求处理流程。