bigywater.tech
Spring · 71

@ConfigurationProperties:松散绑定、校验与构造器绑定

@ConfigurationProperties 到底接受哪些写法?列表和 Map 在多个来源里怎么合并?嵌套对象会不会被校验?只有一个带参构造器时 setter 还起不起作用?这篇在 Spring Boot 4.0.8 上逐个场景实测。

列表替换,Map 合并
文件里 hosts=a,b,c 加系统属性 hosts[0]=z,结果 [z];文件里 labels.a=1 加系统属性 labels.b=2,结果 {b=2, a=1}
没有校验器就没有校验
类上有 @Validated,但 classpath 里没有 hibernate-validator:name 空白、pool=11 也正常启动
绑定一次
启动后往 Environment 加新值:重新 bind 能读到 changed,已绑定的 bean 和 @Value 字段还是 initial

宽松绑定

实测 · 本机执行输出
  application.properties 里写 app.first-name=v:
    firstName=v, maxConnections=0, hosts=[], labels={}
  application.properties 里写 app.firstName=v:
    firstName=v, maxConnections=0, hosts=[], labels={}
  application.properties 里写 app.first_name=v:
    firstName=v, maxConnections=0, hosts=[], labels={}
  application.properties 里写 app.firstname=v:
    firstName=v, maxConnections=0, hosts=[], labels={}
  application.properties 里写 app.FIRST-NAME=v:
    firstName=v, maxConnections=0, hosts=[], labels={}
  操作系统环境变量 APP_FIRSTNAME=v:
    firstName=v, maxConnections=0, hosts=[], labels={}
  操作系统环境变量 APP_FIRST_NAME=v:
    firstName=v, maxConnections=0, hosts=[], labels={}
  操作系统环境变量 APP_FIRST__NAME=v:
    firstName=null, maxConnections=0, hosts=[], labels={}

properties 里 5 种写法、环境变量里 2 种写法都能绑定;环境变量里多一个下划线(APP_FIRST__NAME)就绑定不上。

列表与 Map

实测 · 本机执行输出
  app.hosts=a,b,c:
    firstName=null, maxConnections=0, hosts=[a, b, c], labels={}
  app.hosts[0]=x 与 app.hosts[1]=y:
    firstName=null, maxConnections=0, hosts=[x, y], labels={}
  app.hosts[1]=y(没有 [0]):
    启动失败:Description: Binding to target [Bindable@25d3cfc8 type = java.util.List<java.lang.String>, value = 'provided', annotations = array<Annotation>[[empty]], bindMethod = [null]] failed: Property: app.hosts[1] Value: "y" Origin: … Reason: The elements [app.hosts[1]] were left unbound. Action: Update your application's configuration
  环境变量 APP_HOSTS_0=e0、APP_HOSTS_1=e1:
    firstName=null, maxConnections=0, hosts=[e0, e1], labels={}
  环境变量 APP_HOSTS=a,b:
    firstName=null, maxConnections=0, hosts=[a, b], labels={}
  文件里 app.hosts=a,b,c,系统属性 -Dapp.hosts[0]=z(不同来源的列表合并还是替换):
    firstName=null, maxConnections=0, hosts=[z], labels={}
  app.labels.env=prod:
    firstName=null, maxConnections=0, hosts=[], labels={env=prod}
  app.labels.a.b=x(key 里有点号,不加方括号):
    firstName=null, maxConnections=0, hosts=[], labels={a.b=x}
  app.labels[a.b]=x(加方括号):
    firstName=null, maxConnections=0, hosts=[], labels={a.b=x}
  环境变量 APP_LABELS_ENV=prod:
    firstName=null, maxConnections=0, hosts=[], labels={env=prod}
  文件里 app.labels.a=1,系统属性 -Dapp.labels.b=2(不同来源的 Map 合并还是替换):
    firstName=null, maxConnections=0, hosts=[], labels={b=2, a=1}

不同来源的列表整体替换,Map 按 key 合并;下标必须从 [0] 连续;key 里的点号可以用方括号括起来。

类型转换

实测 · 本机执行输出
  Duration(没有单位的数字默认毫秒)
  "30s" → PT30S
  "PT30S" → PT30S
  "2m" → PT2M
  "500" → PT0.5S
  "1.5s" → 失败:'1.5s' is not a valid duration
  "30 s" → 失败:'30 s' is not a valid duration
  "1d" → PT24H
  "-5s" → PT-5S
  "PT-5S" → PT-5S
  "30seconds" → 失败:'30seconds' is not a valid duration
  Duration(@DurationUnit(SECONDS))
  "500" → PT8M20S
  "500ms" → PT0.5S
  "2m" → PT2M
  DataSize
  "10MB" → 10485760B
  "10mb" → 失败:Unknown data unit suffix 'mb'
  "1024" → 1024B
  "1KB" → 1024B
  "1.5MB" → 失败:'1.5MB' does not match data size pattern
  "10 MB" → 10485760B
  "2GB" → 2147483648B
  boolean
  "true" → true
  "TRUE" → true
  "yes" → true
  "on" → true
  "1" → true
  "y" → 失败:Invalid boolean value 'y'
  "no" → false
  "off" → false
  "0" → false
  "t" → 失败:Invalid boolean value 't'
  "" → null
  枚举 Mode{FAST_MODE,SLOW_MODE}
  "FAST_MODE" → FAST_MODE
  "fast-mode" → FAST_MODE
  "fast_mode" → FAST_MODE
  "fastMode" → FAST_MODE
  "FAST-MODE" → FAST_MODE
  "fast mode" → FAST_MODE
  "fastmode" → FAST_MODE
  "Fast_Mode" → FAST_MODE
  int
  "42" → 42
  "042" → 42
  "0x1F" → 31
  "1,000" → 失败:For input string: "1,000"
  "3.0" → 失败:For input string: "3.0"
  "1000L" → 失败:For input string: "1000L"
  "" → null
  List<String>(逗号分隔)
  "a,b,c" → [a, b, c]
  "a, b ,c" → [a, b, c]
  "a,,c" → [a, , c]
  "a" → [a]
  "" → []
  "a;b" → [a;b]
  -- 绑定失败时的启动报错
  app.max-connections=abc:
    启动失败:Description: Failed to bind properties under 'app.max-connections' to int: Property: app.max-connections Value: "abc" Origin: … Reason: failed to convert java.lang.String to int (caused by java.lang.NumberFormatException: For input string: "abc") Action: Update your application's configuration

Duration 无单位按毫秒、不接受 1.5s;DataSize 单位要大写、不接受小数;boolean 接受 yes/on/1,不接受 y/t;枚举写法宽松。

校验

实测 · 本机执行输出
  name 空白、pool=11,inner.url 与 inner2.url 都填了:
    启动失败:Description: Binding to target demo.V failed: Property: v.name Value: "" Origin: … Reason: must not be blank Property: v.pool Value: "11" Origin: … Reason: must be less than or equal to 10 Action: Update your application's configuration
  name 与 pool 正常,inner.url(嵌套对象,没有 @Valid)缺失,inner2.url 填了:
    V{name=ok, pool=5, inner.url=null, inner2.url=u2}
  name 与 pool 正常,inner2.url(嵌套对象,有 @Valid)缺失,inner.url 填了:
    启动失败:Description: Binding to target demo.V failed: Property: v.inner2.url Value: "null" Reason: must not be blank Action: Update your application's configuration
  类上没有 @Validated,字段上写了 @NotBlank,name 缺失:
    NoVal{name=null}(类上没有 @Validated)
  类上有 @Validated,但运行时 classpath 里没有 hibernate-validator(只有 spring-boot-starter),name 空白、pool=11:
    V{name=, pool=11, inner.url=null, inner2.url=null}

需要 @Validated 加 classpath 里的校验实现;嵌套对象要在字段上加 @Valid;没有校验器时不报错也不校验。

JavaBean 与构造器绑定

实测 · 本机执行输出
  字段是 final、只有 getter 没有 setter,配置 g.name=changed:
    启动失败:Description: Failed to bind properties under 'g' to demo.Getters: Property: g.name Value: "changed" Origin: … Reason: java.lang.IllegalStateException: No setter found for property: name Action: Update your application's configuration
  字段不是 final、只有 getter 没有 setter,配置 g.count=7:
    启动失败:Description: Failed to bind properties under 'g' to demo.Getters: Property: g.count Value: "7" Origin: … Reason: java.lang.IllegalStateException: No setter found for property: count Action: Update your application's configuration
  同样的类,g.name、g.count 都不配:
    Getters{name=init, count=1}(两个字段都只有 getter,没有 setter)
  record,什么都不配:
    Rec[name=null, count=0, tags=null, m=null, in=null, inDef=Inner[url=null, port=8080], withDef=dflt]
  record,配置 rec.name=n、rec.count=3、rec.tags=x,y、rec.m.k=v、rec.in.url=u、rec.in-def.url=u2、rec.with-def=given:
    Rec[name=n, count=3, tags=[x, y], m={k=v}, in=Inner[url=u, port=8080], inDef=Inner[url=u2, port=8080], withDef=given]
  类有两个构造器、都没有 @ConstructorBinding,配置 multi.a=1:
    启动失败(没有 FailureAnalyzer 报告,堆栈里的异常):Caused by: java.lang.NoSuchMethodException: demo.Multi.<init>()
  类有两个构造器、其中一个标了 @ConstructorBinding,配置 multi2.a=1、multi2.b=2:
    Multi2{a=1, b=2}
  类只有一个带参构造器(没有注解)又有 setter,配置 both.a=1、both.b=2:
    Both{a=1, b=field-default}

只有 getter 的字段配了值会启动失败;record 的嵌套对象与集合缺失时是 null;多个构造器要标 @ConstructorBinding;单个带参构造器会让 setter 属性不再绑定。

绑定只发生一次

实测 · 本机执行输出
  app.first-name=initial 启动,启动之后再往 Environment 里加新值:
    firstName=initial, maxConnections=0, hosts=[], labels={}
    启动之后在 Environment 最前面加 app.first-name=changed:
      Environment.getProperty        = changed
      @ConfigurationProperties bean  = initial
      @Value 字段                    = initial
      Binder.get(env).bind(...) 重新绑定 = changed

Environment 之后变了,已绑定的 bean 和 @Value 字段不会更新,需要重新绑定。

分步演示

@ConfigurationProperties
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8(spring-boot-starter 与 spring-boot-starter-validation,非 Web 模式)的单个程序,每个场景一个独立 JVM;属性写在工作目录的 application.properties、-D 系统属性或操作系统环境变量里;类型转换一节没有逐个用 @ConfigurationProperties 类重做,而是直接用 Binder + MapConfigurationPropertySource;「没有 hibernate-validator」的场景是用带 validation 的 classpath 编译、用只有 spring-boot-starter 的 classpath 运行
    • 没有验证:Spring Cloud 的 @RefreshScope 或配置中心下的重新绑定;@ConfigurationPropertiesScan 与 @Bean 方法上的 @ConfigurationProperties;校验分组(groups)与自定义 Validator;Kotlin 的数据类;Windows 上环境变量名大小写不敏感带来的差别;configuration-processor 生成的元数据(见第 67 篇);带 @ConstructorBinding 之后再加 setter 的混合写法;为什么单个带参构造器会让 setter 属性不再绑定(我只观察到结果,没有读绑定器的源码)
    京ICP备2026067502号-1京公网安备11010502063487号