@ConfigurationProperties 和 @Value 有什么区别?
简化版
@ConfigurationProperties 适合把同一前缀下的一组层级配置类型安全地绑定到对象,支持宽松绑定、类型转换、构造器绑定和校验;@Value 适合少量独立值或需要 SpEL 的场景。复杂配置优先集中建模,不要把大量 @Value 分散到业务类中。
详细版
ConfigurationProperties 可绑定 Duration、DataSize、集合、Map 和嵌套对象,并把 app.remote-timeout、APP_REMOTE_TIMEOUT 等形式按规则映射到 Java 属性。配置类需要通过 @ConfigurationPropertiesScan、@EnableConfigurationProperties 等方式注册。
@Value("${app.name:default}") 适合读取单个属性,支持占位符和 SpEL,但缺少面向一组配置的元数据、聚合校验和清晰模型。两者都从 Environment 获取值,区别主要在绑定模型和使用方式,不是配置来源优先级不同。
完整版教学
一、把配置当成一个领域对象
@ConfigurationProperties(prefix = "app.client")
@Validated
public record ClientProperties(
@NotBlank String baseUrl,
@Positive int maxConnections,
Duration timeout) {}
配置文件可以写成:
app:
client:
base-url: https://api.example.com
max-connections: 20
timeout: 3s
调用方依赖 ClientProperties,可以一次看到完整配置契约,也能在启动时发现格式和约束错误。
二、宽松绑定是什么
Spring Boot Binder 能将配置源中的不同命名风格映射到同一属性,例如 kebab-case、camelCase、下划线和环境变量常用的大写形式。配置文件中推荐使用规范的 kebab-case,环境变量则遵循大写和下划线转换规则。
宽松绑定不意味着任意拼写都会成功。列表索引、Map key、环境变量中的数字和特殊字符都有转换规则,部署前应验证最终绑定结果。
三、构造器绑定与可变绑定
不可变配置对象通过构造器一次完成绑定,Record 很适合这种模型。单一参数化构造器通常可被识别为绑定构造器;存在多个构造器时,需要按当前 Boot 版本的构造器绑定规则明确选择。
构造器绑定类型应由 ConfigurationProperties 扫描或 EnableConfigurationProperties 注册,不要同时依赖普通 @Component 创建流程。需要给第三方可变 JavaBean 绑定属性时,也可以在 @Bean 方法上使用 @ConfigurationProperties。
四、类型转换与校验
Binder 支持常见标量以及 Duration、DataSize 等 Boot 类型转换,例如 500ms、10MB 比裸数字更能表达单位。绑定成功只代表格式可转换,不代表业务值合理。
添加 @Validated 与 Jakarta Validation 约束后,缺失 URL、负连接数等问题可在启动阶段失败。嵌套对象的级联约束要正确声明,容器元素和可选默认值也要纳入验证设计。
五、什么时候使用 @Value
@Value("${app.region:cn}")
private String region;
少量、独立且不值得建立配置类的值可以使用 @Value,它还支持 SpEL。ConfigurationProperties 不会像 @Value 那样对属性值执行 SpEL,这反而让外部配置的数据边界更简单、更可预测。
当一个业务类出现许多同前缀 @Value,或者配置需要嵌套、校验和 IDE 元数据时,应提取配置对象。配置对象也不应该混入网络调用等业务行为。
六、默认值与敏感信息
默认值应只用于确实安全、通用的选项。数据库密码、令牌等必需秘密不能用看似可用的弱默认掩盖缺失,应由部署环境或秘密管理系统提供,并避免通过日志、Actuator 或 toString() 泄露。
配置绑定对象可以被多个 Bean 共享,优先设计为不可变。若运行期需要动态刷新,那是另一套配置中心与刷新一致性问题,普通 ConfigurationProperties 本身不等于自动热更新。
七、用能力矩阵选择而不是凭习惯
假设客户端有 base-url、timeout、max-connections、重试次数和 3 个 TLS 子项,共 7 个配置值。写 7 个 @Value 会把契约散落在消费类中;一个 ClientProperties 可以集中完成单位转换、嵌套建模和启动校验。相反,只有一个构建版本字符串且确实需要占位符时,单独创建配置类未必更清晰。
| 能力 | ConfigurationProperties | @Value |
|---|---|---|
| 结构化批量绑定 | 强 | 弱,逐项声明 |
| 宽松绑定 | 完整支持 | 有限,建议使用规范 kebab-case 键 |
| 类型转换 | 支持 Duration、DataSize 等 | 依赖转换服务 |
| 聚合校验 | 可结合 Validation | 需自行组织 |
| IDE 配置元数据 | 支持生成 | 不支持 |
| SpEL | 不执行 | 支持 |
| 不可变构造器模型 | 支持 | 不适合作为聚合模型 |
Environment PropertySources
│
├─ Binder ──> ClientProperties(结构、转换、校验)
└─ Placeholder/SpEL ──> @Value 注入点
两者都读取 Environment,所以“换成 ConfigurationProperties 就改变配置优先级”是错误推论。真正变化的是消费模型:一个以属性对象为契约,一个以单个表达式为注入点。
选择钩子:同一前缀出现第 3 个相关配置时,就应认真考虑建模;需要层级、校验或单位时直接选 ConfigurationProperties。
八、常见误区与追问
- 误区:@ConfigurationProperties 和 @Value 使用不同配置来源。 二者都从 Environment 取值,来源覆盖顺序不因注解改变。
- 误区:宽松绑定能容忍任意错误拼写。 它只支持规定的命名变体,环境变量、集合索引和 Map 特殊键都有明确转换规则。
- 误区:ConfigurationProperties 会自动热更新。 普通绑定反映启动或 Bean 创建时的值,动态刷新需要配置中心及额外生命周期机制。
- 追问:Record 为什么适合配置绑定? 它天然表达不可变数据载体,构造完成后配置契约固定;多构造器时仍要遵守目标 Boot 版本的选择规则。
- 追问:第三方类怎样绑定配置? 可在创建该对象的
@Bean方法上标注 ConfigurationProperties,按 JavaBean 属性完成绑定。 - 追问:为什么不建议秘密设置弱默认值? 缺失秘密本应让启动失败,弱默认会把配置错误拖到生产运行阶段并扩大安全风险。
九、加强记忆
一组配置用 ConfigurationProperties 建模:宽松绑定、类型转换、构造器不可变、启动校验;零散单值或 SpEL 才考虑 @Value。二者读取的是同一套 Environment,差别在于配置是否形成清晰、可验证的类型契约。