「项目配置」章节已经把 app.health.status 和 app.health.service 放进 Spring Environment。经过 IoC 与 Spring Bean 两章后,现在可以继续回答当时留下的问题:Java 对象怎样取得这些值,配置对象又为什么能够被 Service 注入。
本章先用 @Value 读取单个配置,再用 @ConfigurationProperties 把同一前缀下的配置绑定成一个带校验的 Bean。完成后,HealthService 不再依赖写死的状态和服务名称。
1. Environment
Spring Boot 启动时会把配置文件、环境变量、系统属性和命令行参数等属性来源合并到 Environment。覆盖顺序已经在前文确定,因此业务组件读取到的是合并后的最终值。
1application.yaml2环境变量与命令行参数3↓ 合并与覆盖4Spring Environment5↓ 读取或绑定6Spring Bean7↓ 构造器注入8HealthService
组件可以注入 Environment 并调用 getProperty(),但业务代码会因此反复书写字符串键名和类型转换。更常用的方式是让 Spring 在创建 Bean 时完成注入或结构化绑定。
2. @Value
只有一两个互不相关的值时,可以在构造参数上使用 @Value:
01package cn.usehook.spring.health;0203import org.springframework.beans.factory.annotation.Value;04import org.springframework.stereotype.Service;0506@Service07public class HealthService {0809private final String status;10private final String service;1112public HealthService(13@Value("${app.health.status}") String status,14@Value("${app.health.service}") String service15) {16this.status = status;17this.service = service;18}1920public HealthStatus check() {21return new HealthStatus(status, service);22}23}
Spring 创建 HealthService 时解析两个占位符,再把最终字符串传入构造方法。字段可以保持 final,对象创建完成时配置也已经齐全。
@Value 适合少量独立值。随着同一前缀下的配置增多,键名会散落在多个构造参数中,也难以集中表达配置结构和校验规则,这时应该使用配置属性绑定。
3. 配置属性类
心跳配置天然属于同一组。创建 record 描述 app.health 的结构:
01package cn.usehook.spring.config;0203import jakarta.validation.constraints.NotBlank;04import org.springframework.boot.context.properties.ConfigurationProperties;05import org.springframework.validation.annotation.Validated;0607@Validated08@ConfigurationProperties(prefix = "app.health")09public record HealthProperties(10@NotBlank String status,11@NotBlank String service12) {}
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 中加入:
1<dependency>2<groupId>org.springframework.boot</groupId>3<artifactId>spring-boot-starter-validation</artifactId>4</dependency>
4. 注册配置 Bean
@ConfigurationProperties 描述怎样绑定,但配置类还要注册进容器。IoC 章节曾用 HealthConfiguration.healthProperties() 创建固定值;现在删除这个 @Bean 方法,避免容器中同时出现手动对象和配置绑定对象。
当前项目改为在入口类启用配置属性扫描:
01package cn.usehook.spring;0203import org.springframework.boot.SpringApplication;04import org.springframework.boot.autoconfigure.SpringBootApplication;05import org.springframework.boot.context.properties.ConfigurationPropertiesScan;0607@SpringBootApplication08@ConfigurationPropertiesScan09public class Application {1011public static void main(String[] args) {12SpringApplication.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 参数改为依赖一个配置对象:
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}
构造器只声明 Java 类型,不再包含字符串形式的配置键。IoC 容器先创建 HealthProperties,再把它注入 HealthService,最后把服务注入 Controller。
1Environment 中的 app.health.*2-> HealthProperties Bean3-> HealthService Bean4-> HealthController Bean
Controller 不需要同时注入配置对象。使用什么默认状态和服务名称属于心跳业务实现,HTTP 边界只调用 HealthService。
6. 验证绑定结果
配置类可以通过 Spring Boot 测试验证实际绑定和容器注册:
01package cn.usehook.spring.config;0203import org.junit.jupiter.api.Test;04import org.springframework.beans.factory.annotation.Autowired;05import org.springframework.boot.test.context.SpringBootTest;0607import static org.assertj.core.api.Assertions.assertThat;0809@SpringBootTest(properties = {10"app.health.status=UP",11"app.health.service=configuration-test"12})13class HealthPropertiesTests {1415private final HealthProperties properties;1617@Autowired18HealthPropertiesTests(HealthProperties properties) {19this.properties = properties;20}2122@Test23void bindsHealthConfiguration() {24assertThat(properties.status()).isEqualTo("UP");25assertThat(properties.service()).isEqualTo("configuration-test");26}27}
测试属性的优先级高于应用文件,所以该测试不会依赖开发机器上的实际配置。它验证的是属性合并、绑定、校验和 Bean 注册这一整条路径。
把 app.health.service 临时改为空白并启动测试,配置校验应让 ApplicationContext 创建失败。正常测试不应故意保留失败配置;针对失败绑定的精确断言可以以后使用 ApplicationContextRunner 编写。
配置绑定完成后,项目中的对象关系已经闭合:配置来源由 Environment 管理,配置结构由 HealthProperties 表达,业务服务只依赖类型明确的 Bean。下一篇继续讨论这些 Bean 的作用域与生命周期,之后再学习自动配置、AOP 与 Spring MVC 请求处理流程。