@ConfigurationProperties 绑定
概述
@ConfigurationProperties 是 Spring Boot 外部化配置的核心机制,它将 application.yml/application.properties 中的配置值绑定到 Java Bean 上。从注解注册到属性注入的完整链路涉及 12 个关键细节点。
本文将逐个拆解这些内部实现,涵盖注册、触发、绑定、类型转换、校验和异常处理的全流程。
本文基于 Spring Boot 3.x 源码分析。
1. 整体绑定流程
@SpringBootApplication
│
├─ @EnableAutoConfiguration
│ └─ @Import(AutoConfigurationImportSelector.class)
│ └─ AutoConfiguration 配置
│ └─ @EnableConfigurationProperties(XXXProperties.class)
│ │
│ └─ @Import(EnableConfigurationPropertiesRegistrar.class)
│ └─ Registrar → registerBeanDefinition
│ └─ ConfigurationPropertiesBindingPostProcessor
│ └─ BeanPostProcessor
│ │
│ └─ postProcessBeforeInitialization()
│ └─ ConfigurationPropertiesBinder.bind()
│ └─ Binder.get(env).bind(prefix, Bindable.ofInstance(target))
│ └─ 5 步绑定流程
│ ├─ BindHandler.onStart()
│ ├─ getPropertySources()
│ ├─ findProperty()
│ ├─ bindProperty()
│ └─ BindHandler.onSuccess()2. EnableConfigurationProperties.REGISTRAR 注册
2.1 注解结构
java
// EnableConfigurationProperties.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {
// 指定要绑定的属性类
Class<?>[] value() default {};
}2.2 EnableConfigurationPropertiesRegistrar 实现
java
// EnableConfigurationPropertiesRegistrar.java
public class EnableConfigurationPropertiesRegistrar
implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// 1. 注册 ConfigurationPropertiesBindingPostProcessor
// 这是最核心的一步——注册 BeanPostProcessor
registerBindingPostProcessor(registry);
// 2. 注册 @EnableConfigurationProperties.value() 中指定的属性类
registerConfigurationProperties(importingClassMetadata, registry);
}
private void registerBindingPostProcessor(BeanDefinitionRegistry registry) {
// 检查是否已注册
if (!registry.containsBeanDefinition(
CONFIGURATION_PROPERTIES_BINDING_POST_PROCESSOR_BEAN_NAME)) {
// 创建 RootBeanDefinition
RootBeanDefinition definition = new RootBeanDefinition(
ConfigurationPropertiesBindingPostProcessor.class);
// 注册到 BeanDefinitionRegistry
registry.registerBeanDefinition(
CONFIGURATION_PROPERTIES_BINDING_POST_PROCESSOR_BEAN_NAME,
definition);
}
}
}2.3 注册的 BeanPostProcessor
ConfigurationPropertiesBindingPostProcessor 实现了 BeanPostProcessor 接口,会在每个 Bean 初始化前被自动调用:
java
// ConfigurationPropertiesBindingPostProcessor.java
public class ConfigurationPropertiesBindingPostProcessor
implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// 检查 Bean 是否有 @ConfigurationProperties 注解
ConfigurationProperties annotation = AnnotationUtils
.findAnnotation(bean.getClass(), ConfigurationProperties.class);
if (annotation != null) {
// 执行属性绑定
bind(bean, beanName, annotation);
}
return bean;
}
}3. ConfigurationPropertiesBindingPostProcessor 触发时机
3.1 postProcessBeforeInitialization() 执行位置
AbstractAutowireCapableBeanFactory.doCreateBean()
│
├─ createBeanInstance() ← 反射创建 Bean 实例
│
├─ populateBean() ← 填充属性(@Autowired、@Value 等)
│ └─ applyPropertyValues()
│
├─ initializeBean()
│ ├─ invokeAwareMethods() ← BeanNameAware 等
│ │
│ ├─ applyBeanPostProcessorsBeforeInitialization() ← ★ 此处触发
│ │ └─ ConfigurationPropertiesBindingPostProcessor
│ │ └─ postProcessBeforeInitialization()
│ │ └─ bind(bean, beanName, annotation)
│ │ └─ ConfigurationPropertiesBinder.bind()
│ │
│ ├─ invokeInitMethods() ← @PostConstruct、InitializingBean
│ │
│ └─ applyBeanPostProcessorsAfterInitialization()
│
└─ registerDisposableBeanIfNecessary()3.2 为什么在 postProcessBeforeInitialization 而非 postProcessAfterInitialization 中绑定
- 保证
@PostConstruct可以访问已绑定的属性:如果在postProcessAfterInitialization中绑定,此时@PostConstruct已经执行完毕,无法读取配置值 - 与其他
BeanPostProcessor的兼容:后续的BeanPostProcessor可以基于已绑定的属性做决策
4. ConfigurationPropertiesBinder.bind() 内部
4.1 源码
java
// ConfigurationPropertiesBinder.java
class ConfigurationPropertiesBinder {
private final Binder binder;
ConfigurationPropertiesBinder(Environment environment) {
// 从 Environment 创建 Binder
this.binder = new Binder(environment);
}
void bind(ConfigurationProperties annotation, Object bean) {
// 获取 prefix
String prefix = annotation.prefix();
// 创建 Bindable,包装目标 Bean
Bindable<Object> bindable = Bindable.ofInstance(bean);
// 执行绑定
BindResult<Object> result = this.binder.bind(prefix, bindable);
// 如果定义了 excludeNames,排除指定属性
String[] excludeNames = annotation.excludeName();
if (excludeNames.length > 0) {
// 回退未排除的原始值
// ...
}
}
}4.2 Binder 的创建
java
// Binder.java
public class Binder {
private final List<PropertySource<?>> propertySources;
private final BindHandler defaultBindHandler;
private final ConversionService conversionService;
public Binder(Environment environment) {
// 通过 ConfigurationPropertySources 获取适配的属性源
this(ConfigurationPropertySources.get(environment),
new PropertySourcesPlaceholdersResolver(environment),
environment.getConversionService());
}
}ConfigurationPropertySources.get(environment) 将原始的 PropertySource<?> 包装为 ConfigurationPropertySource,使其支持宽松的名称匹配。
5. Binder.bind() 的 5 步流程
5.1 源码
java
// Binder.java
public <T> BindResult<T> bind(String name, Bindable<T> target) {
// 1. 创建绑定上下文
BindContext context = new BindContext(this, target, name);
// 2. 执行绑定
T result = bind(name, target, context, null);
// 3. 包装为 BindResult
return BindResult.of(result);
}
private <T> T bind(String name, Bindable<T> target, BindContext context,
BindHandler handler) {
handler = handler != null ? handler : this.defaultBindHandler;
// 步骤 1:onStart()
handler.onStart(name, target, context);
try {
// 步骤 2:获取所有 PropertySource
List<ConfigurationPropertySource> sources = getPropertySources();
// 步骤 3:查找属性
ConfigurationProperty property = findProperty(name, sources);
if (property != null) {
// 步骤 4:绑定属性值
Object bound = bindProperty(property, target, context);
// 步骤 5:onSuccess()
handler.onSuccess(name, target, context, bound);
return (T) bound;
} else {
// 属性不存在
return null;
}
} catch (Exception ex) {
// 异常处理
handler.onFailure(name, target, context, ex);
throw ex;
}
}5.2 5 步流程详解
Binder.bind("spring.datasource", Bindable.ofInstance(dataSourceProperties))
│
├─ 1. BindHandler.onStart()
│ └─ ValidationBindHandler.onStart() → 检查 @Validated 注解
│
├─ 2. getPropertySources()
│ └─ 遍历 PropertySource 链:
│ commandLineArgs → systemProperties → systemEnvironment → random → application.yml → ...
│
├─ 3. findProperty("spring.datasource.url", sources)
│ └─ 对每个 PropertySource 进行 RelaxedNames 匹配
│ "spring.datasource.url" → match → "spring.datasource.url" = "jdbc:mysql://..."
│
├─ 4. bindProperty(property, Bindable.ofInstance(target))
│ └─ 将属性值转换为目标字段的类型
│ "jdbc:mysql://..." → String → target.url = "jdbc:mysql://..."
│
└─ 5. BindHandler.onSuccess()
└─ ValidationBindHandler.onSuccess() → 执行 @Validated 校验6. BindHandler 拦截链
6.1 拦截链结构
java
// Binder.java
private BindHandler getBindHandler() {
// 默认的 BindHandler 链
BindHandler handler = this.defaultBindHandler;
// 添加 IgnoreNestedPropertiesBindHandler
handler = new IgnoreNestedPropertiesBindHandler(handler);
// 添加 NoUnboundElementsBindHandler
handler = new NoUnboundElementsBindHandler(handler);
// 添加 ValidationBindHandler
handler = new ValidationBindHandler(handler);
return handler;
}6.2 各 Handler 职责
| Handler | 职责 | 过滤时机 |
|---|---|---|
IgnoreNestedPropertiesBindHandler | 忽略未知的嵌套属性(不抛异常) | onSuccess() |
NoUnboundElementsBindHandler | 检查是否有未绑定的属性(ignoreUnknownFields=false 时抛异常) | onSuccess() |
ValidationBindHandler | 执行 @Validated 或 @Valid 校验 | onStart() + onSuccess() |
6.3 调用链
BindHandler.onStart()
│
├─ ValidationBindHandler.onStart()
│ └─ 检查被绑定对象是否有 @Validated 注解
│
├─ IgnoreNestedPropertiesBindHandler.onStart()
│ └─ 空操作(只需在 onSuccess 中拦截)
│
└─ NoUnboundElementsBindHandler.onStart()
└─ 空操作(只需在 onSuccess 中拦截)
BindHandler.onSuccess()
│
├─ IgnoreNestedPropertiesBindHandler.onSuccess()
│ └─ 如果属性未被绑定 → 忽略(不抛异常)
│
├─ NoUnboundElementsBindHandler.onSuccess()
│ └─ 如果配置中有属性未绑定 & ignoreUnknownFields=false → 抛出异常
│
└─ ValidationBindHandler.onSuccess()
└─ 调用 Validator.validate() 执行校验7. RelaxedNames 的实现细节
7.1 8 种变体生成
java
// RelaxedNames.java
public final class RelaxedNames implements Iterable<String> {
private final Set<String> values = new LinkedHashSet<>();
public RelaxedNames(String name) {
// 为 "my-app.data-source.url" 生成所有变体
initialize(name);
}
private void initialize(String name) {
// 1. 原始形式(不转换)
add(name); // "my-app.data-source.url"
// 2. 驼峰形式(去掉分隔符,首字母大写)
add(toCamelCase(name)); // "myApp.dataSource.url"
// 3. 下划线形式(- 转为 _)
add(toUnderscore(name)); // "my_app.data_source.url"
// 4. 大写形式(全大写 + _)
add(toUpperCase(name)); // "MY_APP.DATA_SOURCE.URL"
// 5. 消除点号(点号转为 -)
add(toDotted(name)); // "my-app.data-source.url"
// 6. 消除点号 + 驼峰
add(toDottedCamelCase(name)); // "myApp.dataSource.url"
// 7. 消除点号 + 下划线
add(toDottedUnderscore(name)); // "my_app.data_source.url"
// 8. 消除点号 + 大写
add(toDottedUpperCase(name)); // "MY_APP.DATA_SOURCE.URL"
}
}7.2 变体生成示例
yaml
# application.yml 中定义
my-app:
data-source:
url: jdbc:mysql://localhost:3306/dbRelaxedNames("my-app.data-source.url") 生成的变体:
| # | 变体 | 匹配 |
|---|---|---|
| ① | my-app.data-source.url | ✅ 完全匹配 |
| ② | myApp.dataSource.url | ✅ 驼峰匹配 |
| ③ | my_app.data_source.url | ✅ 下划线匹配 |
| ④ | MY_APP.DATA_SOURCE.URL | ✅ 大写匹配 |
| ⑤ | my-app.datasource.url | ✅ 消除分隔符 |
| ⑥ | myApp.datasource.url | ✅ 驼峰 + 消除 |
| ⑦ | my_app.datasource.url | ✅ 下划线 + 消除 |
| ⑧ | MY_APP.DATASOURCE.URL | ✅ 大写 + 消除 |
7.3 在 Binder 中的使用
java
// Binder.java 中查找属性时
private ConfigurationProperty findProperty(String name,
List<ConfigurationPropertySource> sources) {
for (ConfigurationPropertySource source : sources) {
// 使用 RelaxedNames 生成所有变体后逐个匹配
for (String relaxedName : new RelaxedNames(name)) {
ConfigurationProperty property = source
.getConfigurationProperty(relaxedName);
if (property != null) {
return property; // 找到即返回
}
}
}
return null;
}8. OriginAware 属性溯源
8.1 溯源链
java
// OriginAware 接口
public interface OriginAware {
void setOrigin(Origin origin);
Origin getOrigin();
}
// PropertyOrigin 包装
public class PropertyOrigin {
private final PropertySource<?> source; // 来源 PropertySource
private final String name; // 属性名
private final Origin origin; // 原始位置(行号)
}8.2 从 YAML 文件定位行号
java
// OriginTrackedMapPropertySource.java
public class OriginTrackedMapPropertySource extends MapPropertySource {
private final Map<String, Origin> origins;
@Override
public Object getProperty(String name) {
// 获取原始值
Object value = super.getProperty(name);
// 获取该值在源文件中的位置
Origin origin = this.origins.get(name);
if (origin != null && value instanceof OriginTrackedValue) {
// 将 origin 信息附加到绑定过程中
return ((OriginTrackedValue) value)
.withOrigin(origin);
}
return value;
}
}8.3 行号追踪的内部实现
yaml
# application.yml:15
spring:
datasource:
url: jdbc:mysql://localhost:3306/db # ← 第 17 行OriginTrackedYamlLoader 在解析 YAML 时记录行号:
java
// OriginTrackedYamlLoader.java(简化)
public class OriginTrackedYamlLoader extends YamlProcessor {
@Override
protected void process(MutablePropertySources sources) {
for (Document document : getDocuments()) {
// 遍历 YAML 的每个 key
for (Map.Entry<String, Object> entry :
document.getProperties().entrySet()) {
// 获取属性值及其行号
OriginTrackedValue value =
OriginTrackedValue.of(entry.getValue(),
document.getStartMark().getLine());
// 存入 PropertySource
source.getSource().put(entry.getKey(), value);
}
}
}
}8.4 溯源在排查问题中的应用
java
// Spring Boot Actuator /env 端点的使用
// GET /actuator/env/spring.datasource.url
// 返回:
{
"property": {
"source": "application.yml",
"value": "jdbc:mysql://localhost:3306/db",
"origin": "class path resource [application.yml]:17:5" // ← 第 17 行第 5 列
}
}9. 嵌套绑定 NestedConfigurationProperty
9.1 注解定义
java
// NestedConfigurationProperty.java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NestedConfigurationProperty {
// 标记嵌套属性的注解(无属性)
}9.2 使用示例
java
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
private String url;
private String username;
private String password;
@NestedConfigurationProperty // ← 标记嵌套对象
private HikariSettings hikari = new HikariSettings();
// getter / setter
}
public class HikariSettings {
private int maximumPoolSize = 10;
private long connectionTimeout = 30000;
// getter / setter
}yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
hikari:
maximum-pool-size: 20
connection-timeout: 50009.3 Binder 的递归绑定过程
java
// Binder.java 处理嵌套属性
private <T> T bindProperty(ConfigurationProperty property,
Bindable<T> target, BindContext context) {
Object value = property.getValue();
if (target.hasAnnotation(NestedConfigurationProperty.class)) {
// ★ 嵌套绑定 —— 递归
return bindNested(property, target, context);
}
// 普通绑定 —— 直接类型转换
return convert(value, target.getType());
}
private <T> T bindNested(ConfigurationProperty property,
Bindable<T> target, BindContext context) {
// 1. 创建子对象实例
T instance = BeanUtils.instantiateClass(target.getType().resolve());
// 2. 递归绑定子对象的属性
for (ConfigurationProperty childProperty :
getChildProperties(property)) {
bind(childProperty.getName(),
Bindable.ofInstance(instance),
context, getBindHandler());
}
return instance;
}9.4 与无 @NestedConfigurationProperty 的区别
| 场景 | 行为 | 示例 |
|---|---|---|
有 @NestedConfigurationProperty | 自动创建子对象实例并递归绑定 | hikari.maximumPoolSize = 20 |
无 @NestedConfigurationProperty | 不自动创建子对象,仍可绑定(需子对象已创建) | 需要手动 new HikariSettings() |
10. 集合/数组绑定
10.1 绑定示例
yaml
spring:
redis:
cluster:
nodes: # 列表
- 192.168.1.1:6379
- 192.168.1.2:6379
sentinels: # 逗号分隔字符串
"node1:26379,node2:26379"
timeout: 5000 # Duration 类型10.2 类型转换流程
java
// Binder 内部类型转换
private <T> Object convert(Object value, TypeDescriptor targetType) {
// 1. 获取 ConversionService
ConversionService conversionService = this.conversionService;
// 2. 判断转换目标
if (targetType.isArray()) {
// 数组转换: String[] → TargetType[]
return convertToArray(value, targetType);
}
if (targetType.isCollection()) {
// 集合转换: String[] → List<TargetType>
return convertToCollection(value, targetType);
}
if (targetType.isMap()) {
// Map 转换: Map<String, Object> → Map<String, TargetType>
return convertToMap(value, targetType);
}
// 3. 委托给 ConversionService 做类型转换
if (conversionService.canConvert(
TypeDescriptor.forObject(value), targetType)) {
return conversionService.convert(value, targetType);
}
return value;
}10.3 支持的集合/数组转换
| 源类型 | 目标类型 | 转换逻辑 |
|---|---|---|
String[] | List<String> | 逐个元素放入 List |
List<String> | String[] | 获取所有元素转为数组 |
String(逗号分隔) | List<String> | StringUtils.commaDelimitedListToStringArray() 拆分 |
List<Map<String, Object>> | List<SomePojo> | 每个 Map 通过对象绑定转换 |
Map<String, String> | Map<String, Duration> | 每个 value 通过 ConversionService 转换 |
10.4 String[] → List<SomePojo> 的完整绑定过程
yaml
my:
paging:
urls:
- path: /api/v1/users
pageSize: 20
- path: /api/v2/orders
pageSize: 50java
@ConfigurationProperties(prefix = "my.paging")
public class PagingProperties {
private List<PagingUrl> urls = new ArrayList<>();
}
public class PagingUrl {
private String path;
private int pageSize;
}绑定过程:
Binder.bind("urls", Bindable.listOf(PagingUrl.class))
│
├─ 1. 找到 urls 属性 → List<Map<String, Object>> 原始值
│ [{path="/api/v1/users", pageSize=20}, {path="/api/v2/orders", pageSize=50}]
│
├─ 2. 发现目标是 List<PagingUrl>
│ → 创建新的 ArrayList<PagingUrl>()
│
├─ 3. 遍历每个 Map,递归绑定
│ ├─ Map → PagingUrl(path, pageSize)
│ └─ Map → PagingUrl(path, pageSize)
│
└─ 4. 返回 List<PagingUrl>11. @Validated 校验触发
11.1 触发流程
java
// ValidationBindHandler.java
public class ValidationBindHandler extends AbstractBindHandler {
private final Validator validator;
@Override
public <T> Bindable<T> onStart(String name, Bindable<T> target,
BindContext context) {
// 检查目标对象是否有 @Validated 注解
if (target.getAnnotation(Validated.class) != null) {
// 记录需要校验
context.setAttribute(ValidationBindHandler.class, target);
}
return target;
}
@Override
public <T> T onSuccess(String name, Bindable<T> target,
BindContext context, T result) {
// 校验触发
if (context.getAttribute(ValidationBindHandler.class) != null) {
validate(result);
}
return result;
}
private <T> void validate(T result) {
// 使用 javax.validation.Validator 执行校验
Set<ConstraintViolation<T>> violations =
validator.validate(result);
if (!violations.isEmpty()) {
// 抛出 BindValidationException
throw new BindValidationException(violations);
}
}
}11.2 使用示例
java
@ConfigurationProperties(prefix = "spring.datasource")
@Validated // ← 开启 JSR-303 校验
public class DataSourceProperties {
@NotEmpty // ← 校验注解
private String url;
@Min(1)
@Max(65535)
private int port = 3306;
}yaml
spring:
datasource:
url: # url 为空 → 校验失败
port: 99999 # 端口超出范围 → 校验失败启动时会抛出:
Binding validation errors:
- url: must not be empty
- port: must be less than or equal to 6553512. 绑定失败异常处理
12.1 异常转换链
java
// ConfigurationPropertiesBindingPostProcessor.java
private void bind(ConfigurationProperties annotation, Object bean,
String beanName) {
try {
// 执行绑定
this.configurationPropertiesBinder.bind(annotation, bean);
} catch (BindException ex) {
// 1. Binder 抛出 BindException
// 2. 转换为 ConfigurationPropertiesBindException
throw new ConfigurationPropertiesBindException(
beanName, bean.getClass(), annotation, ex);
} catch (Exception ex) {
// 3. 其他异常直接包装
throw new ConfigurationPropertiesBindException(
beanName, bean.getClass(), annotation, ex);
}
}12.2 BindException 的类型
java
// Binder 绑定时可能抛出的异常
public class BindException extends RuntimeException {
private final List<BindException.Property> propertyExceptions;
// 包含每个绑定失败的属性及其原因
public static class Property {
private final String name; // 属性名
private final Class<?> type; // 目标类型
private final Exception cause; // 失败原因
}
}12.3 异常处理流程
绑定过程中
│
├─ 类型转换失败
│ ↓
│ BinderException: "Failed to convert value 'abc' to type 'int'"
│ ↓
│ ConfigurationPropertiesBindException:
│ "Binding to target [DataSourceProperties] failed.
│ Property: port
│ Value: 'abc'
│ Reason: Failed to convert from String to int"
│
├─ 属性不存在
│ ↓
│ BinderException: "No mapping for 'non-existent-property'"
│ ↓
│ ConfigurationPropertiesBindException:
│ "Binding to target [DataSourceProperties] failed.
│ Property: nonExistentProperty (ignoreUnknownFields=false)"
│
└─ 校验失败
↓
BindValidationException: "Validation failed for ..."
↓
ConfigurationPropertiesBindException:
"Binding to target [DataSourceProperties] failed.
Validation errors: url must not be empty"13. ConstructorBinding 构造器绑定
13.1 不可变对象的绑定
java
@ConfigurationProperties(prefix = "spring.datasource")
@ConstructorBinding // ← 标记使用构造器绑定
public class DataSourceProperties {
private final String url;
private final String username;
private final String password;
private final int maxPoolSize;
// 只有一个构造函数,自动匹配配置属性
public DataSourceProperties(
String url,
String username,
String password,
@DefaultValue("10") int maxPoolSize) {
this.url = url;
this.username = username;
this.password = password;
this.maxPoolSize = maxPoolSize;
}
// 只有 getter,没有 setter
public String getUrl() { return url; }
public String getUsername() { return username; }
public String getPassword() { return password; }
public int getMaxPoolSize() { return maxPoolSize; }
}13.2 @DefaultValue 注解
java
// 当配置中不提供该属性时,使用默认值
@ConfigurationProperties(prefix = "app.cache")
@ConstructorBinding
public class CacheProperties {
private final Duration ttl;
private final int maxSize;
public CacheProperties(
@DefaultValue("10m") Duration ttl, // ← 默认 10 分钟
@DefaultValue("1000") int maxSize) { // ← 默认 1000
this.ttl = ttl;
this.maxSize = maxSize;
}
}13.3 Binder 处理 @ConstructorBinding
java
// ConfigurationPropertiesBinder.java
private void bind(ConfigurationProperties annotation, Object bean) {
// 检查是否有 @ConstructorBinding 注解
if (isConstructorBinding(annotation, bean)) {
// ★ 构造器绑定 —— 通过构造器参数创建不可变对象
// 1. 解析构造器参数与配置属性的映射
Constructor<?> constructor = bean.getClass()
.getDeclaredConstructors()[0]; // 获取单个构造函数
Parameter[] parameters = constructor.getParameters();
// 2. 逐个参数从配置中绑定
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
String paramName = param.getName(); // 参数名
DefaultValue defaultValue = param
.getAnnotation(DefaultValue.class);
// 从配置中查找属性值
BindResult<?> result = this.binder.bind(
annotation.prefix() + "." + paramName,
Bindable.of(param.getType()));
if (result.isBound()) {
args[i] = result.get();
} else if (defaultValue != null) {
// 使用 @DefaultValue 提供的默认值
args[i] = convertDefaultValue(
defaultValue.value(), param.getType());
} else {
throw new BindException(...);
}
}
// 3. 通过构造器创建实例
BeanUtils.instantiateClass(constructor, args);
} else {
// 常规 setter 绑定
bindBean(annotation, bean);
}
}13.4 构造器绑定的优势
| 特性 | Setter 绑定 | Constructor 绑定 |
|---|---|---|
| 对象是否可变 | ✅ 可变(有 setter) | ❌ 不可变(只有 getter) |
| 必填字段约束 | 编译期无约束 | ✅ 构造器参数强制提供 |
| 不可变安全 | ❌ 可被意外修改 | ✅ 只有 getter,线程安全 |
| 代码简洁度 | 需要大量 setter | ✅ 更简洁 |
与 @DefaultValue 配合 | 用 @DefaultValue 注解参数 | ✅ 原生支持 |
总结
| # | 细节点 | 核心要点 |
|---|---|---|
| ① | EnableConfigurationProperties.REGISTRAR | ImportBeanDefinitionRegistrar 注册 ConfigurationPropertiesBindingPostProcessor |
| ② | postProcessBeforeInitialization() | 在 @PostConstruct 之前触发,确保初始化方法可访问绑定属性 |
| ③ | ConfigurationPropertiesBinder.bind() | new Binder(env) → bind(prefix, Bindable.ofInstance(target)) |
| ④ | Binder.bind() 5 步流程 | onStart() → getPropertySources() → findProperty() → bindProperty() → onSuccess() |
| ⑤ | BindHandler 拦截链 | IgnoreNestedPropertiesBindHandler + NoUnboundElementsBindHandler + ValidationBindHandler |
| ⑥ | RelaxedNames 8 种变体 | 原始 → 驼峰 → 下划线 → 大写 → 消除分隔符 → ... 共 8 种 |
| ⑦ | OriginAware 属性溯源 | OriginTrackedYamlLoader 记录行号 → PropertyOrigin 包装 → Actuator /env 端点展示 |
| ⑧ | @NestedConfigurationProperty 嵌套绑定 | 递归创建子对象实例并绑定 |
| ⑨ | 集合/数组绑定 | ConvertionService 支持 String[]→List<T>、String→List逗号分隔、List<Map>→List<POJO> |
| ⑩ | @Validated 校验 | ValidationBindHandler.onSuccess() → Validator.validate() → BindValidationException |
| ⑪ | 异常处理 | BindException → ConfigurationPropertiesBindException 统一转换 |
| ⑫ | ConstructorBinding | 构造器绑定不可变对象,@DefaultValue 提供默认值,无需 setter |