最近,我瞅见我们团队一个新人提交了个 PR (代码合并请求)。他用 @Value
注解把 application.properties
里的参数注入到一个重试服务(retry service)里,就像下面这样:
(此处通常会有一个使用 @Value
的代码示例,但原文未提供)
是的,这么写确实能跑起来,而且代码写起来也简单。但作为一个经验丰富的开发者,我脑子里第一个念头就是——“这玩意儿以后扩展起来要命啊。”
这种方式(大量使用 @Value
)的问题在于:
-
• 配置分散 (Scattered configuration) — 属性散落在各个类中,难以集中管理、分组或重构。
-
• 缺乏校验 (No validation) —
@Value
本身不提供便捷的校验机制,无效的配置值可能会悄无声息地让逻辑在运行时出错。 -
• 缺乏不可变性 (Lack of immutability) — 如果注入的字段不是
final
的,它们理论上可以在运行时被修改(尽管这通常不是好实践)。 -
• 手动设置默认值 (Manual defaulting) — 像
@Value("${retry.attempts:3}")
这样的默认值写法,虽然方便,但当这样的用法变多时,会开始污染业务逻辑代码的可读性。
所以,我建议他改用 @ConfigurationProperties
。
@ConfigurationProperties
注解能把咱们的 application.properties
或 application.yaml
文件里的外部属性,以类型安全的方式绑定到我们应用程序中的特定 Java 类上。
Spring Boot 的 @ConfigurationProperties
允许我们将一组相关的配置属性映射到一个单独的类中。
从 Spring Boot 3 开始,它还完美支持 Java Records,这给我们带来了不可变性 (immutability) 和更强的类型安全 (type-safety)。
使用 @ConfigurationProperties
(尤其是配合 Records) 的好处包括:
-
• 🔐 使用 Java Records 实现不可变的配置对象。
-
• 🧠 类型安全且对 IDE 友好(编辑器能提供更好的自动补全支持)。
-
• 🧪 支持 JSR-303 校验 (例如
@NotNull
,@Min
等注解可以直接用在配置属性类上)。 -
• 🧼 关注点分离 (Separation of concerns) — 配置逻辑与业务逻辑清晰分开。
-
• ✅ 测试更简单 — 在单元测试中可以轻松注入 Mock 的配置 Bean。
🛠️ 如何在 Java Records 中使用 @ConfigurationProperties
下面是一个如何操作的例子:
- 1. 创建一个配置属性类 (Config Properties Class - 使用 Record)我们先创建一个
record
类来承载相关的配置属性:import org.springframework.boot.context.properties.ConfigurationProperties; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; // @ConstructorBinding // Spring Boot 3.0 之后,对于 record 和构造器绑定的类,此注解不再是必须的 @ConfigurationProperties(prefix = "app.retry")// 指定配置文件中的前缀 publicrecordRetryProperties( @Min(1)int maxAttempts, // 最小尝试次数为1 @Min(100)long initialIntervalMs, // 初始间隔至少100毫秒 @Max(5)double multiplier // 退避乘数最大为5 // 可以根据需要添加更多属性 ) { // 对于 record,构造器、getter、equals、hashCode、toString 都是自动生成的 // 可以在这里添加默认构造器或者静态工厂方法(如果需要更复杂的默认逻辑) public RetryProperties { // 可以在紧凑构造函数中进行额外的校验或规范化(如果JSR-303不够用) } }
- 2. 在主应用类中启用配置属性类的扫描你需要在你的 Spring Boot 主启动类或任何一个
@Configuration
类上,让 Spring Boot 知道去哪里扫描并激活你的@ConfigurationProperties
类。通常可以通过@EnableConfigurationProperties
或确保该类能被组件扫描到。示例如下:
(或者,如果import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(RetryProperties.class)// 明确启用这个配置属性类 publicclassMyApplication { publicstaticvoidmain(String[] args) { SpringApplication.run(MyApplication.class, args); } }
RetryProperties
本身被标记为@Component
,并且在组件扫描路径下,有时也能工作,但@EnableConfigurationProperties
是更推荐的方式) - 3. 在服务类中使用构造器注入来注入配置类现在,你可以在你的服务类(比如
RetryService
)中,通过构造器注入这个RetryProperties
对象了。示例如下:
当import org.springframework.stereotype.Service; @Service publicclassMyRetryService { privatefinal RetryProperties retryConfig; // 使用 final 保证不可变 // 通过构造器注入 RetryProperties publicMyRetryService(RetryProperties retryConfig) { this.retryConfig = retryConfig; } publicvoidattemptOperation() { intattempts= retryConfig.maxAttempts(); // 从配置对象获取参数 longinterval= retryConfig.initialIntervalMs(); doublemultiplier= retryConfig.multiplier(); System.out.println("重试配置 - 最大尝试次数: " + attempts); System.out.println("重试配置 - 初始间隔: " + interval + "ms"); System.out.println("重试配置 - 退避乘数: " + multiplier); // ... 执行重试逻辑 ... } }
RetryProperties
作为一个final
属性通过构造器注入时,其不可变性得到了保证。如果你的服务类使用了像 Lombok 这样的库,并且有@RequiredArgsConstructor
或@AllArgsConstructor
注解,Lombok 会自动为你生成包含此参数的构造函数。然后,你就可以从这个配置实例 (retryConfig
) 中轻松获取所需的参数了。
虽然 @Value
可能适用于小规模应用或快速原型开发,但一旦遇到以下情况,它很快就会变得难以维护:
-
• 你需要对配置值进行校验。
-
• 你使用了多个相互关联的配置值。
-
• 你希望代码整洁、服务可测试。
通过使用 @ConfigurationProperties
配合 Java Records,你就采用了现代化的 Spring Boot 开发方式,它能带来整洁的代码、类型安全以及更好的关注点分离。
我在应用程序开发中遵循以下规则:
-
• 对于结构化的、可复用的、分组的配置,我使用
@ConfigurationProperties
。 -
• 而对于快速的、简单的或一次性的单个属性,我才考虑使用
@Value
。