《Spring Boot 入门》6.2 @ConfigurationProperties 类型安全配置

本节把 6.1 里的一整块配置绑成类型安全的 Java 对象:先对比 @ConfigurationProperties 与 @Value 的差别,再给出 record 与构造器绑定的写法(4.x 已无需 @ConstructorBinding),覆盖嵌套对象、List/Map 与松散绑定规则,最后用 @Validated 做校验并生成 IDE 补全元数据。

本节目标:把图书管理服务的一整块配置绑定成类型安全的 Java 对象,并掌握松散绑定、嵌套集合、配置校验与 IDE 补全这四件事。
适用版本:Spring Boot 4.1.x(Java 21)

6.2 @ConfigurationProperties 类型安全配置

6.1 节我们为图书服务建好了三套配置文件,但取值的写法还很原始。最直接的方式是用 @Value 一个一个注入:

@Component
class BorrowService {
    @Value("${book.name}")
    private String name;
    @Value("${book.max-borrow-days}")
    private int maxBorrowDays;
    @Value("${book.page-size}")
    private int pageSize;
    @Value("${book.contact.email}")
    private String contactEmail;
}

配置只有五项时还能忍,一旦涨到二十项,这个类就会被注入语句淹没。本节用 @ConfigurationProperties 把「前缀下的一整块配置」一次性绑成一个对象。

@Value 的五个痛点

上面那段代码暴露了 @Value 的五个问题。

第一,字段注入难以测试。 单元测试里没法直接 new 出这个对象,只能靠反射或起一个 Spring 上下文。

第二,没有类型安全。 键名写错、类型不匹配都要等到启动时才知道;@Value("${book.maxBorrowDays}") 里的 camelCase 在 yml 里根本不存在,却不会在编译期报错。

第三,不支持松散绑定。 @Value 要求键名逐字符匹配,max-borrow-days 和 maxBorrowDays 在它眼里是两个不同的键。

第四,无法校验。 想把 max-borrow-days 限制在 1 到 365 之间,@Value 做不到。

第五,集合与嵌套对象很别扭。 绑一个 List<String> 得写 SpEL,绑一个嵌套对象更是噩梦。

@ConfigurationProperties 正是为这五点设计的。

两者的对比

维度@Value@ConfigurationProperties
批量绑定一个键一个注解一个前缀下全部键
类型安全弱,靠 SpEL 与转换器强,绑定到 POJO 或 record
松散绑定不支持支持 kebab、camel、下划线、大写
配置校验不支持@Validated + JSR-380
嵌套与集合需 SpEL,写法繁琐天然支持
IDE 提示无靠生成的元数据
默认值${x:default}字段初始化或构造器参数
注入方式字段或参数构造器,便于测试
适用场景一两个零散值成块的、同前缀的配置

判断标准很简单:同一个前缀下有三个以上键,就用 @ConfigurationProperties;只有一两个零散值,@Value 反而更轻。

第一个 @ConfigurationProperties

先看配置。application.yml:

book:
  name: 图书管理服务
  max-borrow-days: 30
  page-size: 20
  categories:
    - 小说
    - 技术
  contact:
    email: ops@example.com
    phone: 010-00000000

再用一个 record 接住它:

package com.example.book.config;

import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "book")
public record BookProperties(
        String name,
        int maxBorrowDays,
        int pageSize,
        List<String> categories,
        Contact contact) {

    public record Contact(String email, String phone) {
    }
}

三处细节值得注意:prefix = "book" 表示绑定 book.* 下的所有键;max-borrow-days 通过松散绑定落到 maxBorrowDays,不需要额外配置;record 的访问器没有 get 前缀,读值写 props.name() 而不是 props.getName()。

注册它,最省事的是在主类上开扫描:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class BookApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookApplication.class, args);
    }
}

然后在业务类里构造器注入:

@Service
public class NotificationService {
    private final BookProperties props;

    public NotificationService(BookProperties props) {
        this.props = props;
    }

    public void notifyOverdue(String isbn) {
        System.out.printf("发送逾期提醒:%s -> %s%n", isbn, props.contact().email());
    }
}

注意这里没有任何 @Value,也没有字段注入;props 是构造器参数,单测里可以直接 new NotificationService(new BookProperties(...)) 构造出来。

构造器绑定与 record

@ConfigurationProperties 有两种绑定方式。

绑定方式触发条件特点
构造器绑定只有一个带参构造器,或类型是 record不可变、字段可 final、易测试
JavaBean 绑定有默认构造器与 setter可变,适合第三方类

关键变化:Spring Boot 3.0 起,只有一个带参构造器的类会自动走构造器绑定,@ConstructorBinding 不再需要显式标注。到 4.x 依然如此。只有当你提供了多个构造器、需要指明用哪一个时,才在目标构造器上补 @ConstructorBinding 消歧。

record 天生只有一个全参构造器,所以上面那段代码直接可用,不需要任何额外注解。反过来,如果你用 class 加 getter/setter,就走 JavaBean 绑定,要求每个字段都有 setter——这也是为什么本书示例统一用 record。

嵌套对象、List 与 Map

嵌套对象不需要注解,Spring 会递归绑定。application.yml:

book:
  name: 图书管理服务
  limits:
    max-books-per-user: 5
    max-reservations: 3
  categories:
    - 小说
    - 技术
  metadata:
    region: cn-north-1
    owner: ops

对应的类型:

import java.util.List;
import java.util.Map;

@ConfigurationProperties(prefix = "book")
public record BookProperties(
        String name,
        Limits limits,
        List<String> categories,
        Map<String, String> metadata) {

    public record Limits(int maxBooksPerUser, int maxReservations) {
    }
}

几点经验:

  • List 可以用 YAML 列表,也可以在 .properties 里写成逗号分隔的 book.categories=小说,技术。
  • Map<String, String> 的 key 不做松散绑定,region 就是 region。如果 key 含点号或大写等特殊字符,要用 metadata.[some.key]=v 这种方括号语法。
  • 从环境变量来的 Map key 会被转成小写,写跨环境配置时留意。
  • 嵌套层级再深也会递归绑定,但校验不会自动递归(见下一节)。

松散绑定规则

松散绑定(relaxed binding)是 @ConfigurationProperties 最有用的特性之一:同一个属性可以有多种写法,Spring 都能对上。

配置里的写法形式说明
book.max-borrow-dayskebab-case规范形式,YAML 与 properties 推荐
book.maxBorrowDayscamelCase代码风格,也能绑上
book.max_borrow_days下划线从老系统迁移时常见
BOOK_MAXBORROWDAYS全大写环境变量形式:点变下划线、删连字符

最后一行是重点:max-borrow-days 转环境变量时连字符被直接删除,正确写法是 BOOK_MAXBORROWDAYS,而不是 BOOK_MAX_BORROW_DAYS(后者会被解析成 book.max.borrow.days,是另一个键)。环境变量的完整规则见 6.3。

反过来要记住:@Value 不支持松散绑定。yml 里写 max-borrow-days,@Value("${book.maxBorrowDays}") 会直接抛 Could not resolve placeholder 'book.maxBorrowDays',而 @ConfigurationProperties 的 maxBorrowDays 字段照样绑得上。

用 @Validated 做配置校验

配置错了要在启动时就炸,而不是等第一个请求进来才暴露。加一个 starter:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

然后在属性类上标 @Validated 并加 JSR-380 注解:

import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "book")
public record BookProperties(
        @NotBlank String name,
        @Min(1) @Max(365) int maxBorrowDays,
        @Valid @NotNull Contact contact) {

    public record Contact(@NotBlank String email, String phone) {
    }
}

把 book.max-borrow-days 改成 0,启动会失败并给出可读的提示:

Description:
Binding to target org.springframework.boot.context.properties.bind.BindException:
Failed to bind properties under 'book' to com.example.book.config.BookProperties
Reason: maxBorrowDays must be greater than or equal to 1

三个易错点:@Validated 要用 org.springframework.validation.annotation.Validated(不是 Jakarta 的那个);嵌套对象必须加 @Valid,否则内层约束不生效;注解包是 jakarta.validation.constraints(Jakarta EE 11,4.x 用 Hibernate Validator 9.0)。

注册方式:Scan 还是 Enable

维度@ConfigurationPropertiesScan@EnableConfigurationProperties
作用扫描包及子包下所有 @ConfigurationProperties 类并注册显式注册列出的类
粒度粗,整个包细,逐个指定
位置主类或任意配置类任意 @Configuration 类
参数basePackages 可指定范围直接列出 Class
适用自有配置类、包结构规整第三方类、需要精确控制

两条硬性提醒:被扫描或被启用的类不要再标 @Component,否则会与扫描结果重复注册并报 bean 名冲突;只标 @Component 也能注册(走组件扫描),但那样配置类就混在普通 Bean 里,失去了集中管理的意义,不推荐。

给第三方类绑定

有些类来自第三方库,源码不能改,没法加 @ConfigurationProperties。这时用 @Bean 方法把它接进来:

@Configuration
class ClientConfig {

    @Bean
    @ConfigurationProperties(prefix = "book.client")
    ClientSettings clientSettings() {
        return new ClientSettings();
    }
}

这种方式走的是 JavaBean 绑定,ClientSettings 需要 setter。

生成元数据获得 IDE 补全

上面每个属性类都可以配一个编译期处理器,生成 IDE 用的元数据:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

它会在编译期生成 META-INF/spring-configuration-metadata.json。有了它,在 application.yml 里敲 book. 就能得到自动补全、类型提示,以及字段上 Javadoc 的内容。

两个注意点:标 optional 是为了不把这个处理器传递给依赖你的项目;如果还想补充手写的描述(例如某个属性的取值枚举),放到 src/main/resources/META-INF/additional-spring-configuration-metadata.json。

本节常见坑速查

现象原因处理
启动报找不到 BookProperties bean既没扫描也没显式启用加 @ConfigurationPropertiesScan
字段全是默认值prefix 拼错,或类没注册核对前缀与注册方式
校验完全不生效忘了 @Validated补注解与 validation starter
嵌套对象里的约束不生效忘了在内层字段加 @Valid加 @Valid
环境变量不生效写成了带下划线的 BOOK_MAX_BORROW_DAYS改成 BOOK_MAXBORROWDAYS
值读成 "750" 或 falseYAML 隐式类型转换给字符串加引号,见 6.1

小结

  • @ConfigurationProperties 把「一个前缀下的一整块配置」绑成类型安全的对象;@Value 只适合一两个零散值。
  • record 与单个带参构造器走构造器绑定,4.x 不需要 @ConstructorBinding,只有多构造器消歧时才写。
  • 嵌套对象、List、Map 都能直接绑;Map 的 key 不做松散绑定。
  • 松散绑定支持 kebab、camel、下划线、大写四种写法;环境变量形式是「点变下划线、删连字符、全大写」。
  • @Validated 加 JSR-380 注解让配置在启动时校验,嵌套对象记得加 @Valid。
  • 用 @ConfigurationPropertiesScan 或 @EnableConfigurationProperties 注册,二选一;spring-boot-configuration-processor 提供 IDE 补全。

配置对象有了,但「同一个键在五个地方都写了值,到底哪个生效」还没讲清。下一节我们把配置源按官方顺序排一遍,并用一个实测把这些规则验证出来。

阅读导航:上一节:6.1 application.yml 与 Profile · 下一节:6.3 外部化配置与优先级 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计