Spring Boot 4.1.1 深度源码解析-第 09 章 · @ConfigurationProperties 全生命周期:注册、绑定、校验与 AOT
本章定位
第 08 章我们把
Binder拆到了源码级。但Binder只是引擎------谁来调用它?答案是
ConfigurationPropertiesBindingPostProcessor。而这个"调用者"本身有一整套复杂机制:
- Bean 是怎么注册的?
@ConfigurationProperties本身不是 stereotype 注解(它不含@Component),所以光加注解不会有任何事发生。必须有@EnableConfigurationProperties、@ConfigurationPropertiesScan、或@Component三者之一- 注解可以写在
@Bean方法上 ,而不只是类上------这两种位置的语义不同(ConfigurationPropertiesBean专门处理这个差异)- 校验(JSR-303)是怎么接进去的? 通过
ValidationBindHandler,它是BindHandler装饰器链的一环ignoreUnknownFields/ignoreInvalidFields是怎么实现的? 也是装饰器链的一环- 构造器绑定怎么被识别?
BindMethodAttribute+ConfigurationPropertiesBindConstructorProvider- AOT 下怎么办? 有两个专门的 AotProcessor
本章拆解这套"引擎之上的调度层"。
第 1 节 · 四种注册方式:为什么加个注解没反应
1.1 最常见的困惑
java
@ConfigurationProperties(prefix = "my")
public class MyProps {
private String name;
// getter/setter
}
然后:
java
@Autowired
private MyProps myProps; // ❌ NoSuchBeanDefinitionException
原因 :@ConfigurationProperties 的定义里没有 @Component:
java
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {
// ...
}
(确切的元注解以源码为准,但关键是:它不含 @Component,所以组件扫描不会捡起它。)
1.2 四种注册方式
| 方式 | 机制 | 适用场景 |
|---|---|---|
@Component + @ConfigurationProperties |
组件扫描当普通 Bean 注册,再由 BeanPostProcessor 绑定 |
简单场景(但有陷阱,见 1.3) |
@EnableConfigurationProperties(MyProps.class) |
EnableConfigurationPropertiesRegistrar 注册 |
推荐:显式、可控 |
@ConfigurationPropertiesScan |
扫描指定包下所有 @ConfigurationProperties 类 |
大量配置类时 |
@Bean 方法上标 @ConfigurationProperties |
方法返回的对象被绑定 | 第三方类的配置化 |
1.3 @Component + @ConfigurationProperties 的陷阱
java
@Component
@ConfigurationProperties(prefix = "my")
public class MyProps { ... }
这能工作,但有一个重要限制:
组件扫描(
@ComponentScan)必须覆盖这个类所在的包。如果你的@ConfigurationProperties类在@SpringBootApplication所在包的外面,就不会被扫描到。
而 @EnableConfigurationProperties / @ConfigurationPropertiesScan 明确列出类名/包名,不受此限。
实践建议 :优先用
@EnableConfigurationProperties或@ConfigurationPropertiesScan。@Component方式在大型项目里容易造成"为什么这个配置类没生效"的困惑。
1.4 @ConfigurationPropertiesScan 的一个关键约束(Javadoc 实证)
java
/**
* ...
* Note: Classes annotated or meta-annotated with {@link Component @Component} will not be
* picked up by this annotation.
*
* @author Madhura Bhave
* @since 2.2.0
*/
"被
@Component标注或元标注的类不会被本注解捡起。"
这是一个显式的设计决策:
@Component+@ConfigurationProperties→ 由组件扫描处理- 纯
@ConfigurationProperties(无@Component)→ 由@ConfigurationPropertiesScan处理
两者互斥 。为什么?为了避免重复注册------如果一个类同时被两个机制捡到,会注册两次 Bean(同名则覆盖,不同名则产生两个实例)。
实践含义 :不要给
@ConfigurationProperties类加@Component,如果你打算用@ConfigurationPropertiesScan。加了反而不会被扫描到,造成"静默失效"。
1.5 @ConfigurationPropertiesScan 自带 @EnableConfigurationProperties
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigurationPropertiesScanRegistrar.class)
@EnableConfigurationProperties // ★ 注意这一行
public @interface ConfigurationPropertiesScan {
String[] value() default {};
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
@ConfigurationPropertiesScan 元标注了 @EnableConfigurationProperties。
这意味着:用了 @ConfigurationPropertiesScan,就自动 有了 @EnableConfigurationProperties(无参数版),后者会注册 ConfigurationPropertiesBindingPostProcessor 等基础设施。
这是一个贴心的设计:不需要同时写两个注解。
1.6 扫描的实现
java
private void scan(BeanDefinitionRegistry registry, Set<String> packages) {
ConfigurationPropertiesBeanRegistrar registrar = new ConfigurationPropertiesBeanRegistrar(registry);
ClassPathScanningCandidateComponentProvider scanner = getScanner(registry);
for (String basePackage : packages) {
for (BeanDefinition candidate : scanner.findCandidateComponents(basePackage)) {
String beanClassName = candidate.getBeanClassName();
Assert.state(beanClassName != null, "'beanClassName' must not be null");
register(registrar, beanClassName);
}
}
}
private ClassPathScanningCandidateComponentProvider getScanner(BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.setEnvironment(this.environment);
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter(ConfigurationProperties.class));
TypeExcludeFilter typeExcludeFilter = new TypeExcludeFilter();
typeExcludeFilter.setBeanFactory((BeanFactory) registry);
scanner.addExcludeFilter(typeExcludeFilter);
return scanner;
}
三个关键点:
(a)new ClassPathScanningCandidateComponentProvider(false)
构造参数 false = 不使用默认的过滤器 。默认的过滤器是"捡起所有 @Component",这里要完全自定义,所以关掉。
这印证了 1.4 节的互斥设计 :扫描器只认
@ConfigurationProperties(AnnotationTypeFilter),不认@Component。而@Component类即使同时有@ConfigurationProperties......
等等,这里有个逻辑矛盾:如果一个类同时有 @Component 和 @ConfigurationProperties,AnnotationTypeFilter(ConfigurationProperties.class) 会匹配它。那 Javadoc 为什么说"不会被捡起"?
答案在 excludeFilters:TypeExcludeFilter 。它是 Boot 的扩展机制,测试切片(@WebMvcTest 等)用它来排除不该加载的类。但这里更关键的是------
实际上,让我重新审视。AnnotationTypeFilter(ConfigurationProperties.class) 的默认 considerMetaAnnotations = true,所以元标注也算。而 @Component 类的排除可能来自 ConfigurationPropertiesBeanRegistrar 内部的检查。
诚实的边界声明 :Javadoc 的这条约束("@Component 类不会被捡起")我确认了它的存在,但没有逐行追踪到实现它的具体代码 。可能的机制是
ClassPathScanningCandidateComponentProvider的isCandidateComponent检查(要求类是"独立的且具体的"),或 registrar 内的二次过滤。这不影响使用------Javadoc 是权威的行为声明。但我不对实现机制做断言。
(b)TypeExcludeFilter 的加入
java
TypeExcludeFilter typeExcludeFilter = new TypeExcludeFilter();
typeExcludeFilter.setBeanFactory((BeanFactory) registry);
scanner.addExcludeFilter(typeExcludeFilter);
TypeExcludeFilter 是 Spring Boot 测试框架的核心机制------@WebMvcTest、@DataJpaTest 等切片测试通过它排除无关的组件。
setBeanFactory 是必需的 :TypeExcludeFilter 从 BeanFactory 里查找所有 TypeExcludeFilter 类型的 Bean 来做实际的排除判断。忘记设置会导致 IllegalArgumentException。
(c)register 用 ClassUtils.forName
java
private void register(ConfigurationPropertiesBeanRegistrar registrar, String className) throws LinkageError {
try {
register(registrar, ClassUtils.forName(className, null));
}
// ...
}
用 ASM 元数据扫描(不加载类)找到候选,然后真正加载类 来注册。这是 ClassPathScanningCandidateComponentProvider 的标准两段式。
1.7 @EnableConfigurationProperties 的两种用法
java
// 方式一:列出类
@EnableConfigurationProperties({MyProps.class, OtherProps.class})
// 方式二:列出"配置类"(其中的 @Bean 方法带 @ConfigurationProperties)
@EnableConfigurationProperties(MyConfig.class)
第二种用于:
java
@Configuration(proxyBeanMethods = false)
public class MyConfig {
@Bean
@ConfigurationProperties("my")
public SomeThirdPartyType someBean() {
return new SomeThirdPartyType(); // 第三方类型,不能加注解
}
}
这是"给第三方类型做配置化"的唯一方式 ------你无法修改第三方类的源码加
@ConfigurationProperties,但可以在@Bean方法上加。
第 2 节 · ConfigurationPropertiesBean:两种注解位置的统一抽象
2.1 类的职责
java
/**
* Provides access to {@link ConfigurationProperties @ConfigurationProperties} bean
* details, regardless of if the annotation was used directly or on a {@link Bean @Bean}
* factory method. This class can be used to access {@link #getAll(ApplicationContext)
* all} configuration properties beans in an ApplicationContext, or
* {@link #get(ApplicationContext, Object, String) individual beans} on a case-by-case
* basis (for example, in a {@link BeanPostProcessor}).
*
* @author Phillip Webb
* @since 2.2.0
*/
public final class ConfigurationPropertiesBean {
private final String name;
private final @Nullable Object instance;
private final Bindable<?> bindTarget;
// ...
}
@since 2.2.0。它的存在理由写在 Javadoc 第一句:
"regardless of if the annotation was used directly or on a
@Beanfactory method"
即:统一"注解在类上"和"注解在 @Bean 方法上"两种情况的差异。
2.2 两种情况的差异在哪
| 注解在类上 | 注解在 @Bean 方法上 |
|
|---|---|---|
| 类型来源 | 类本身 | 方法的返回类型 |
| 注解来源 | 类上的注解 | 方法上的注解 |
| 实例 | 由容器创建 | 由 @Bean 方法创建 |
| 泛型 | 通常无 | 可能是 List<X> 等复杂泛型 |
Bindable 的构造必须同时携带类型 和注解 (第 08 章讲过 ResolvableTypeDescriptor 需要注解)。所以:
- 类上:
Bindable.of(type).withAnnotations(类上的注解) - 方法上:
Bindable.of(返回类型的 ResolvableType).withAnnotations(方法上的注解)
ConfigurationPropertiesBean 屏蔽了这个差异,对外只暴露 asBindTarget()。
2.3 getAnnotation() 的语义
java
public ConfigurationProperties getAnnotation() {
ConfigurationProperties annotation = this.bindTarget.getAnnotation(ConfigurationProperties.class);
Assert.state(annotation != null, "'annotation' must not be null");
return annotation;
}
从 bindTarget 的 annotations 里取------而这些注解是在构造 Bindable 时放进去的(来自类或方法)。
这印证了第 08 章的
ResolvableTypeDescriptor:注解被装进Bindable,一路传到TypeDescriptor,最终能被 Converter 读到(@DurationUnit等)。
2.4 getAll(ApplicationContext) 与 Actuator
java
public static Map<String, ConfigurationPropertiesBean> getAll(ApplicationContext applicationContext)
返回所有 @ConfigurationProperties Bean(包括通过 @Bean 方法注册的)。
这是 Actuator /actuator/configprops 端点的数据来源之一 (另一个是 BoundPropertiesTrackingBindHandler,第 08 章第 17 节讲过)。
2.5 instance 可以为 null
java
private final @Nullable Object instance;
因为在 BeanPostProcessor 的某些阶段,实例可能还没创建(比如处理 BeanDefinition 时)。
第 3 节 · ConfigurationPropertiesBindingPostProcessor:绑定时机
3.1 它是一个 BeanPostProcessor
java
public class ConfigurationPropertiesBindingPostProcessor
implements BeanPostProcessor, BeanFactoryAware, EnvironmentAware, ApplicationContextAware, InitializingBean, Ordered {
(确切的接口清单以源码为准,但核心是 BeanPostProcessor。)
BeanPostProcessor 意味着绑定发生在 Bean 初始化阶段 ------具体是 postProcessBeforeInitialization。
3.2 时间线
scss
ApplicationContext.refresh()
├─ invokeBeanFactoryPostProcessors(...) ← BeanDefinition 注册(@EnableConfigurationProperties 在这里)
├─ registerBeanPostProcessors(...) ← ConfigurationPropertiesBindingPostProcessor 注册
├─ finishBeanFactoryInitialization(...) ← 实例化所有非懒加载单例
│ ├─ createBean(...)
│ │ ├─ instantiate
│ │ ├─ populateBean(依赖注入)
│ │ └─ initializeBean
│ │ ├─ postProcessBeforeInitialization ← ★ 绑定在这里
│ │ ├─ invokeInitMethods(@PostConstruct)
│ │ └─ postProcessAfterInitialization
└─ finishRefresh()
关键 :绑定发生在
@PostConstruct之前 。所以在@PostConstruct里能读到绑定后的值。但注意:构造器注入发生在绑定之前。所以构造器里读不到配置值(第 08 章讲过构造器绑定的例外------构造器绑定时值通过参数传入,不是读字段)。
3.3 构造器的绑定:VALUE_OBJECT 的跳过
java
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// ...
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
return bean;
}
而:
java
private void bind(@Nullable ConfigurationPropertiesBean bean) {
if (bean == null) {
return;
}
Assert.state(bean.asBindTarget().getBindMethod() != BindMethod.VALUE_OBJECT,
() -> "Cannot bind value object ...");
// ...
this.binder.bind(bean);
}
Assert.state 拒绝 VALUE_OBJECT ------构造器绑定的 Bean 不能 走 BeanPostProcessor 路径。
为什么?因为构造器绑定的对象在构造时 就已经完整了(值通过构造参数传入),实例是不可变 的。BeanPostProcessor 是"创建后修改",对一个 final 字段的 record 无能为力。
这是构造器绑定与 setter 绑定的根本差异:
- setter 绑定 :容器创建实例(无参构造器)→
BeanPostProcessor调 setter 填值- 构造器绑定 :注册阶段就把值准备好,直接通过构造器传入
这也是为什么构造器绑定需要
ConfigurationPropertiesBeanRegistrationAotProcessor等额外机制------它必须在 BeanDefinition 层面就介入。
3.4 BindMethodAttribute:构造器绑定的标记
java
// BindMethodAttribute.java
public final class BindMethodAttribute {
// 在 BeanDefinition 上存/取 BindMethod
}
(类名与存在性已确认;具体 API 以源码为准。)
它的作用:在 BeanDefinition 上记录这个 Bean 应该用哪种绑定方法。因为在 postProcessBeforeInitialization 时,需要知道"这是构造器绑定的吗"(3.3 的 Assert),而这个信息在注册时就确定了。
这是一个典型的"跨阶段传递元信息"的模式 :注册阶段(BeanFactoryPostProcessor)算出结论 → 存到
BeanDefinition的 attribute → 初始化阶段(BeanPostProcessor)读出来用。Spring 里同类模式还有
@Lazy、@Primary等------它们都通过 BeanDefinition 的 attribute 在不同阶段间传递。
第 4 节 · ConfigurationPropertiesBinder 与 BindHandler 装饰器链(本章核心)
4.1 getBindHandler 的完整组装
java
private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
List<Validator> validators = getValidators(target);
BindHandler handler = getHandler();
handler = new ConfigurationPropertiesBindHandler(handler);
if (annotation.ignoreInvalidFields()) {
handler = new IgnoreErrorsBindHandler(handler);
}
if (!annotation.ignoreUnknownFields()) {
// ...
handler = new NoUnboundElementsBindHandler(handler, filter);
}
if (!validators.isEmpty()) {
handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
}
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
// ...
}
return handler;
}
这是本章最重要的代码 。它把 @ConfigurationProperties 的每个属性翻译成一个 BindHandler 装饰器:
| 注解属性 | 对应的 Handler | 作用 |
|---|---|---|
| (总是) | getHandler()(推测为 IgnoreTopLevelConverterNotFoundBindHandler) |
忽略顶层转换失败 |
| (总是) | ConfigurationPropertiesBindHandler |
Boot 自己的处理(如 @NestedConfigurationProperty 支持) |
ignoreInvalidFields=true |
IgnoreErrorsBindHandler |
忽略字段级转换错误 |
ignoreUnknownFields=false |
NoUnboundElementsBindHandler |
未绑定属性检测 |
| 有 Validator | ValidationBindHandler |
校验 |
| --- | 所有 ConfigurationPropertiesBindHandlerAdvisor |
扩展点 |
4.2 装饰器链的顺序决定语义
java
handler = getHandler(); // 最内层
handler = new ConfigurationPropertiesBindHandler(handler);
if (ignoreInvalidFields) handler = new IgnoreErrorsBindHandler(handler);
if (!ignoreUnknownFields) handler = new NoUnboundElementsBindHandler(handler, filter);
if (!validators.isEmpty()) handler = new ValidationBindHandler(handler, ...);
// advisors 最外层
return handler;
后加的在外层 。回忆第 08 章 AbstractBindHandler 的委托方向:外层 handler 的 onSuccess 先执行,然后调 super.onSuccess 委托给内层。
所以执行顺序(以 onSuccess 为例):
scss
advisors → ValidationBindHandler → NoUnboundElements → IgnoreErrors → ConfigurationPropertiesBindHandler → getHandler()
这个顺序有实际意义:
IgnoreErrorsBindHandler在NoUnboundElements之内 ------ 意味着:如果某个字段转换失败被 IgnoreErrors 吞掉(返回 null),NoUnboundElements仍然会检测到"这个属性没被成功绑定"?
不对。看第 08 章的 NoUnboundElementsBindHandler.onSuccess:
java
@Override
public @Nullable Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context,
Object result) {
this.boundNames.add(name); // 无条件记录
return super.onSuccess(name, target, context, result);
}
它无条件 记录 boundNames(不看 result 是否为 null)。而 IgnoreErrorsBindHandler 在内层------它的 onFailure 被触发时(内层先抛异常),外层的 NoUnboundElements 的 onSuccess 根本不会被调用 (因为异常路径走 onFailure)。
所以:ignoreInvalidFields=true 时,转换失败的属性不会被记入 boundNames,如果同时 ignoreUnknownFields=false,会被报告为"未绑定"。
这是一个真实的交互后果 :
ignoreInvalidFields=true+ignoreUnknownFields=false会产生"明明配了却说未绑定"的困惑。因为该属性绑定失败了(被忽略),所以没被计为已绑定。我在第 08 章第 3.3 节讲过
onFailure能"救回"绑定------但IgnoreErrorsBindHandler的"救回"是返回 null,这不触发外层的onSuccess。
ValidationBindHandler 在最外层(advisors 之前):校验在所有绑定完成后才进行(因为它在最外层,能"看到"最终的对象)。
4.3 ConfigurationPropertiesBindHandlerAdvisor:扩展点
java
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
// ...
}
java
private volatile @Nullable List<ConfigurationPropertiesBindHandlerAdvisor> bindHandlerAdvisors;
volatile + 懒加载(缓存,避免每次绑定都查 BeanFactory)。
ConfigurationPropertiesBindHandlerAdvisor 是一个 SPI:任何 Bean 实现它,就能往绑定链里插入自己的 handler。
典型用途 :Spring Cloud 用它插入"解密 handler"(
{cipher}...的解密)------在绑定过程中解密配置值。这是 Boot 为生态预留的扩展点 ,与第 07 章的
ConfigDataLocationResolver、第 06 章的EnvironmentPostProcessor是同一思路:在关键流程的每一步都留 SPI。
4.4 bind 与 bindOrCreate
java
BindHandler bindHandler = getBindHandler(target, annotation);
// ...
Object bindOrCreate(ConfigurationPropertiesBean propertiesBean) {
// ...
return getBinder().bindOrCreate(annotation.prefix(), target, bindHandler);
}
用 bindOrCreate 而不是 bind ------保证 @ConfigurationProperties Bean 永远非 null,即使配置文件里一个相关属性都没有。
这与第 08 章的对比:
spring.main.*用bind(第 05 章):绑定到已有实例,绑定失败就保持原样@ConfigurationProperties用bindOrCreate:一定产出实例语义差异的原因:
@ConfigurationProperties是容器里的 Bean ,必须是非 null 的(否则注入点会 NPE)。而ApplicationProperties是内部字段,有默认值即可。
第 5 节 · 校验:JSR-303 与 ValidationBindHandler
5.1 两种校验器
java
private final @Nullable Validator configurationPropertiesValidator;
this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext);
this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext);
java
private @Nullable Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext) {
// ...
return applicationContext.getBean(VALIDATOR_BEAN_NAME, Validator.class);
// ...
}
configurationPropertiesValidator:容器里名为configurationPropertiesValidator的 Bean(Spring Boot 提供的默认实现)- JSR-303:jakarta validation 的实现(Hibernate Validator)
5.2 ConfigurationPropertiesJsr303Validator.isJsr303Present
java
this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext);
显式检测 JSR-303 是否在 classpath 上。这又是一个"能力模块化 → 运行时检测"的例子(第 01、05 章讲过同类模式)。
实践含义 :如果没有引入
spring-boot-starter-validation,@Validated+@NotNull等注解不会生效,且可能没有明确提示。这正是"为什么我的校验不生效"的第一排查点:检查 classpath 上有没有 validation 实现。
5.3 @Validated 的位置
java
@ConfigurationProperties("my")
@Validated // ★ 必须有
public class MyProps {
@NotNull
private String name;
}
@Validated(org.springframework.validation.annotation.Validated)出现在 ConfigurationPropertiesBean 的 import 清单里------说明 Boot 专门处理它。
@Validated 在 @ConfigurationProperties 类上是触发校验的开关 。没有它,即使字段上有 @NotNull 也不校验。
5.4 ValidationBindHandler 在链上的位置
第 4.2 节讲过它在外层。它的工作时机(onSuccess/onFinish)意味着:校验在对象构造完成后执行。
所以校验的是"最终对象的状态",而不是"逐个字段"。这支持跨字段校验 (自定义 Validator 可以检查多个字段的关系)。
5.5 校验失败的异常
校验失败抛 BindException(由 ValidationBindHandler 抛出),最终被 ConfigurationPropertiesBindException 包装。
而 ConfigurationPropertiesBindException 有对应的 FailureAnalyzer------这就是启动时看到的:
yaml
Description:
Binding to target org.springframework.boot.context.properties.bind.BindException:
Failed to bind properties under 'my' to com.example.MyProps:
Property: my.name
Value: null
Reason: must not be null
Action:
Update your application's configuration
第 6 节 · BindMethod 的确定:ConfigurationPropertiesBindConstructorProvider
(第 08 章讲过 BindConstructorProvider,这里补充 @ConfigurationProperties 场景的特殊处理。)
6.1 EnableConfigurationPropertiesRegistrar 的职责
@EnableConfigurationProperties 通过 @Import(EnableConfigurationPropertiesRegistrar.class) 生效。它做两件事:
- 注册
ConfigurationPropertiesBindingPostProcessor(基础设施) - 为每个列出的类注册 BeanDefinition
6.2 构造器绑定时的 BeanDefinition 特殊处理
构造器绑定的 Bean 不能走 BeanPostProcessor(3.3 节),所以必须在 BeanDefinition 层面就准备好值:
markdown
注册阶段:
- 判断是否需要构造器绑定(BindMethodAttribute)
- 如果是:为 BeanDefinition 设置 InstanceSupplier
- InstanceSupplier 内部:调 Binder 绑定 → 用结果调构造器
InstanceSupplier是 Spring Framework 5.x 引入的机制:允许 BeanDefinition 直接提供一个"实例供给函数",绕过常规的构造器推断。这是构造器绑定能工作的技术基础------
BeanDefinition不再描述"用哪个构造器、传什么参数",而是直接说"调用这个函数拿实例"。
6.3 ConstructorBound 注解
文件清单里有 ConstructorBound.java。从名字看是标记"这个类型应该用构造器绑定"。
(具体语义我未逐行核实,不做断言。)
6.4 @ConstructorBinding 与 BindMethod.VALUE_OBJECT
第 08 章讲过:@ConstructorBinding 用在多构造器 场景指定用哪个。而 BindMethod.VALUE_OBJECT 是显式强制构造器绑定。
两者的关系:
@ConstructorBinding:注解在构造器上,"用这个构造器"BindMethod.VALUE_OBJECT:编程式指定,"用构造器绑定模式"
第 7 节 · AOT:两个专门的 Processor
文件清单里有两个 AOT 相关类:
ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java
ConfigurationPropertiesBeanRegistrationAotProcessor.java
7.1 ConfigurationPropertiesBeanRegistrationAotProcessor
在 BeanDefinition 注册阶段做 AOT 处理。
职责(推测,未逐行核实):
- 为
@ConfigurationProperties类型登记反射 hints(构造器、setter、getter) - 记录绑定元信息,供 native 下快速绑定
7.2 ConfigurationPropertiesBeanFactoryInitializationAotProcessor
在 BeanFactory 初始化阶段做 AOT 处理。
7.3 AOT 下的行为差异
| 方面 | JVM | AOT/native |
|---|---|---|
| 反射绑定 | 直接反射 | 需要 hints |
| 配置值 | 运行时读取 | 构建期读取(部分固化) |
| 校验 | 运行时 | 运行时(校验逻辑本身不固化) |
重要提醒 :
@ConfigurationProperties的值 在 native 下仍是运行时读取的(不像 profiles 那样被固化)。所以application.properties里的值在 native 下可以通过环境变量覆盖(但 profile 不行,见第 06/07 章)。这是"值"与"profile"在 AOT 下的不同待遇:profile 是构建期常量,配置值不是。
第 8 节 · ConfigurationPropertiesBeanRegistrar:注册的实现细节
8.1 Bean 名的生成规则
java
void register(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation) {
String name = getName(type, annotation);
// ...
registerBeanDefinition(name, type, annotation);
}
private String getName(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation) {
// ...
return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
}
Bean 名 = 前缀-类全限定名(有前缀时),否则就是类全限定名。
例如:
java
@ConfigurationProperties(prefix = "my")
public class MyProps { ... }
→ Bean 名:my-com.example.MyProps
这个命名规则有几个实践含义:
- Bean 名是确定的、可预测的 ------ 你可以
@Autowired @Qualifier("my-com.example.MyProps")- 同名冲突:两个不同包但前缀相同的类不会冲突(因为用了全限定名)
- 但同一个类被注册两次会覆盖 (同名 Bean)------ 这正是
@Component+@ConfigurationPropertiesScan要互斥的原因之一
8.2 deduceBindMethod:绑定模式的推断
java
BindMethod bindMethod = ConfigurationPropertiesBean.deduceBindMethod(type);
BindMethodAttribute.set(definition, bindMethod);
if (bindMethod == BindMethod.VALUE_OBJECT) {
// 构造器绑定的特殊处理
}
deduceBindMethod 是构造器绑定识别的入口 。它在注册阶段 就确定绑定模式,并写入 BindMethodAttribute(第 3.4 节讲过跨阶段传递)。
推断逻辑(基于第 08 章的 DefaultBindConstructorProvider 规则):
css
类只有一个构造器 且 该构造器有参数
→ VALUE_OBJECT
类有多个构造器 且 其中一个标了 @ConstructorBinding
→ VALUE_OBJECT
类有无参构造器
→ JAVA_BEAN
实践规则(Boot 3+/4.x):
record类 → 自动VALUE_OBJECT- 只有全参构造器的普通类 → 自动
VALUE_OBJECT- 有无参构造器 + setter →
JAVA_BEAN- 多个构造器 → 必须标
@ConstructorBinding
8.3 重复的检查
java
private void registerBeanDefinition(String beanName, Class<?> type, /* ... */) {
// ...
Assert.state(!this.registry.containsBeanDefinition(beanName),
// "... annotation found on '" + type.getName() + "'.");
BeanDefinitionReaderUtils.registerBeanDefinition(beanDefinition, this.registry);
}
Assert.state 检查重复注册------如果同名 BeanDefinition 已存在,直接失败。
这是一个"早失败"的设计:重复注册会导致"哪个生效"的不确定性,Boot 选择在注册时就报错,而不是让后注册者静默覆盖。
8.4 MergedAnnotation 的使用
java
void register(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation)
注意参数类型是 MergedAnnotation<ConfigurationProperties> 而不是 ConfigurationProperties。
MergedAnnotation 是 Spring Framework 5.2+ 的注解抽象,它支持:
- 元注解(注解上的注解)
- 属性别名 (
@AliasFor) - 属性覆盖
这让
@ConfigurationProperties可以被"组合":
java@ConfigurationProperties(prefix = "my") @Target(ElementType.TYPE) public @interface MyModuleProperties { }然后
@MyModuleProperties标注的类也会被识别为配置属性类。这是"自定义组合注解"的基础------Spring Boot 自己的很多注解就是这么组合的。
第 9 节 · 配置元数据体系
9.1 @ConfigurationPropertiesSource:跨模块的元数据提示
java
/**
* Indicates that the annotated type is a source of configuration properties metadata.
* <p>
* This annotation has no effect on the actual binding process, but serves as a hint to
* the {@code spring-boot-configuration-processor} to generate full metadata for the type.
* <p>
* Typically, this annotation is only required for types located in a different module
* than the {@code @ConfigurationProperties} class that references them. When both types
* are in the same module, the annotation processor can automatically discover full
* metadata as long as the source is available.
* <p>
* Use this annotation when metadata for types located outside the module is needed:
* <ol>
* <li>Nested types annotated by {@code @NestedConfigurationProperty}</li>
* <li>Base classes that a {@code @ConfigurationProperties}-annotated type extends
* from</li>
* </ol>
* ...
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationPropertiesSource {
}
Javadoc 说得很清楚:
"This annotation has no effect on the actual binding process"(对绑定过程没有任何影响)
它纯粹是给注解处理器(spring-boot-configuration-processor)的提示 ,用于生成 spring-configuration-metadata.json。
为什么需要它?
元数据生成是编译期 的(注解处理器)。处理器能看到当前模块的源码,但看不到依赖 jar 里的源码(只有 class 文件)。所以:
java
// 模块 A
@ConfigurationProperties("example.server")
class ServerProperties {
@NestedConfigurationProperty
private final Host host = new Host(); // Host 在模块 B
}
// 模块 B
public class Host { ... } // 处理器看不到源码
处理器知道 host 属性的类型 是 Host,但不知道 Host 里有哪些字段、每个字段的 description 和默认值。
加上 @ConfigurationPropertiesSource 并在模块 B 里也启用 spring-boot-configuration-processor,模块 B 编译时会为 Host 生成完整元数据,模块 A 就能用上。
实践场景 :如果你维护一个多模块项目,且配置类拆分在不同模块,跨模块的嵌套配置类的 IDE 补全需要这个注解。这是个少有人知但很实用的细节。
9.2 @NestedConfigurationProperty
用于标记"这个字段是一个嵌套的配置对象,它的属性应该平铺 在父前缀下,而不是作为 prefix.field 的一个值"。
java
@ConfigurationProperties("my")
public class MyProps {
@NestedConfigurationProperty
private Server server = new Server();
}
// my.server.host=xxx ← 而不是 my.server={"host":"xxx"}
为什么需要显式标记? 因为 Boot 无法可靠地区分"这个字段是要绑定成一个对象"还是"这个字段是要从单个字符串转换来的"。
对比:
java
private Duration timeout; // 从字符串 "30s" 转换 → 不是嵌套
private Server server; // 绑定成对象 → 是嵌套
Duration 能靠"没有可绑定的属性"来排除,但自定义类型(如 Server)如果既有 setter 又能从字符串转换,就有歧义。
@NestedConfigurationProperty是消除歧义的显式声明。实际上,即使不加这个注解,绝大多数情况 Boot 也能正确处理(因为
JavaBeanBinder会尝试绑定嵌套)。它的主要价值是元数据生成------告诉处理器"展开这个类型的属性"。
9.3 @DeprecatedConfigurationProperty
标记一个配置属性已弃用,会在元数据 JSON 里生成 deprecation 节点(第 05 章讲过 spring.main.show-banner 的例子)。
java
public class MyProps {
@Deprecated
@DeprecatedConfigurationProperty(reason = "use my.new-name", replacement = "my.new-name")
public void setOldName(String value) { ... }
}
三个信息:reason、replacement、since(推测)。
这体现了 Boot 对"配置属性"的兼容性承诺强度:配置写在 yaml/环境变量/CI 脚本里,改一个属性名的破坏力远大于改一个方法名。所以弃用必须有明确的迁移指引。
9.4 元数据与 IDE 的关系
元数据 JSON 的位置:
bash
META-INF/spring-configuration-metadata.json
它由 spring-boot-configuration-processor 在编译期生成,来源:
@ConfigurationProperties类的字段(类型、getter/setter)- 字段上的 javadoc(作为
description) - 字段的初始值(作为
defaultValue) @DeprecatedConfigurationProperty(作为deprecation)
这就是为什么给
@ConfigurationProperties字段写 javadoc 是有实际价值的------它会变成 IDE 里看到的提示文本。
第 10 节 · IncompatibleConfigurationException:跨属性校验
java
/**
* Exception thrown when the application has configured an incompatible set of
* {@link ConfigurationProperties} keys.
*
* @author Brian Clozel
* @since 2.4.0
*/
public class IncompatibleConfigurationException extends RuntimeException {
private final List<String> incompatibleKeys;
public IncompatibleConfigurationException(String... incompatibleKeys) {
super("The following configuration properties have incompatible values: " + Arrays.toString(incompatibleKeys));
this.incompatibleKeys = Arrays.asList(incompatibleKeys);
}
public Collection<String> getIncompatibleKeys() {
return this.incompatibleKeys;
}
}
@since 2.4.0,用于表达"一组配置属性之间存在冲突"。
典型场景:
java
@ConfigurationProperties("spring.mvc")
public class WebMvcProperties {
// spring.mvc.servlet.path 与 spring.mvc.servlet.load-on-startup 的组合冲突
}
或:
java
// 同时设置了两个互斥的属性
my.mode=A
my.legacy-mode=B // 冲突
用法 :在 @ConfigurationProperties 类里做一个"自检方法",在 @PostConstruct 或自定义 Validator 里抛出。
这是一个专门的异常类型,说明 Boot 把"配置冲突"当作一类独立的错误------它比"单个属性无效"更需要上下文(要看多个属性才能判断)。
10.1 IncompatibleConfigurationFailureAnalyzer
文件清单里有 IncompatibleConfigurationFailureAnalyzer.java。它把该异常转成可读的 FailureAnalysis:
yaml
Description:
The following configuration properties have incompatible values: [my.mode, my.legacy-mode]
Action:
...
第 11 节 · NotConstructorBoundInjectionFailureAnalyzer:错误信息设计的典范
这是本章最值得学习的一个类------它把一类极难排查的错误变成了明确的启动失败信息。
11.1 完整源码
java
/**
* An {@link AbstractInjectionFailureAnalyzer} for
* {@link ConfigurationProperties @ConfigurationProperties} that are intended to use
* {@link ConstructorBinding constructor binding} but did not.
*
* @author Andy Wilkinson
*/
class NotConstructorBoundInjectionFailureAnalyzer
extends AbstractInjectionFailureAnalyzer<NoSuchBeanDefinitionException> implements Ordered {
@Override
public int getOrder() {
return 0;
}
@Override
protected @Nullable FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause,
@Nullable String description) {
InjectionPoint injectionPoint = findInjectionPoint(rootFailure);
if (isConstructorBindingConfigurationProperties(injectionPoint)) {
String simpleName = injectionPoint.getMember().getDeclaringClass().getSimpleName();
String action = "Update your configuration so that " + simpleName + " is defined via @"
+ ConfigurationPropertiesScan.class.getSimpleName() + " or @"
+ EnableConfigurationProperties.class.getSimpleName() + ".";
return new FailureAnalysis(
simpleName + " is annotated with @" + ConstructorBinding.class.getSimpleName()
+ " but it is defined as a regular bean which caused dependency injection to fail.",
action, cause);
}
return null;
}
@Contract("null -> false")
private boolean isConstructorBindingConfigurationProperties(@Nullable InjectionPoint injectionPoint) {
return injectionPoint != null && injectionPoint.getMember() instanceof Constructor<?> constructor
&& isConstructorBindingConfigurationProperties(constructor);
}
private boolean isConstructorBindingConfigurationProperties(Constructor<?> constructor) {
Class<?> declaringClass = constructor.getDeclaringClass();
BindMethod bindMethod = ConfigurationPropertiesBean.deduceBindMethod(declaringClass);
return MergedAnnotations.from(declaringClass, SearchStrategy.TYPE_HIERARCHY)
.isPresent(ConfigurationProperties.class) && bindMethod == BindMethod.VALUE_OBJECT;
}
private @Nullable InjectionPoint findInjectionPoint(Throwable failure) {
// ...
}
}
11.2 它解决什么问题
场景:
java
@ConfigurationProperties(prefix = "my")
@Component // ← 用 @Component 注册
public class MyProps {
private final String name;
@ConstructorBinding
public MyProps(String name) { this.name = name; }
}
这样写会失败 。原因(第 3.3 节讲过):构造器绑定必须在 BeanDefinition 注册阶段 就准备好值(用 InstanceSupplier),而 @Component 走的是普通组件扫描路径,没有这个特殊处理 → 容器尝试用无参构造器或直接构造 → 失败 → NoSuchBeanDefinitionException。
用户看到的原始错误可能只是:
bash
NoSuchBeanDefinitionException: No qualifying bean of type 'MyProps'
或一堆构造器参数无法解析的错误。完全看不出真正原因。
有了这个 Analyzer 后:
vbnet
Description:
MyProps is annotated with @ConstructorBinding but it is defined as a regular bean which caused dependency injection to fail.
Action:
Update your configuration so that MyProps is defined via @ConfigurationPropertiesScan or @EnableConfigurationProperties.
直接告诉你要做什么。
11.3 三个值得学习的设计点
(a)检测逻辑的精确性
java
return MergedAnnotations.from(declaringClass, SearchStrategy.TYPE_HIERARCHY)
.isPresent(ConfigurationProperties.class) && bindMethod == BindMethod.VALUE_OBJECT;
两个条件同时满足:
- 类上有
@ConfigurationProperties(用MergedAnnotations+TYPE_HIERARCHY支持元注解和继承) deduceBindMethod推断为VALUE_OBJECT
复用 deduceBindMethod 而不是自己重新推断------保证与实际的绑定行为一致。
(b)implements Ordered 且 getOrder() == 0
java
@Override
public int getOrder() {
return 0;
}
FailureAnalyzer 是有顺序的(多个 analyzer 可能匹配同一个异常)。0 表示较高优先级------这个 analyzer 应该在其他通用 analyzer 之前尝试。
为什么需要高优先级? 因为
NoSuchBeanDefinitionException是一个非常通用的异常,可能有其他 analyzer(如"No qualifying bean"的通用分析器)也会匹配。这个更具体的诊断应该先被尝试。
(c)Action 里给出两个具体的替代方案
java
String action = "Update your configuration so that " + simpleName + " is defined via @"
+ ConfigurationPropertiesScan.class.getSimpleName() + " or @"
+ EnableConfigurationProperties.class.getSimpleName() + ".";
注意它用的是 xxx.class.getSimpleName() 而不是硬编码字符串 "@ConfigurationPropertiesScan"。
这是一个防御性细节 :如果注解改名了,错误信息自动跟着改,不会出现"信息过期"。虽然注解改名会破坏兼容性,但这体现了 Boot 在避免魔法字符串上的习惯。
11.4 这一模式在 Boot 里的普遍性
NotConstructorBoundInjectionFailureAnalyzer 是 Boot "FailureAnalyzer 体系"的一员(第 01 章讲过)。整个体系的理念是:
不要让用户看堆栈。把每种常见的配置/装配错误,翻译成一句"发生了什么"+ 一句"你该做什么"。
这个文件清单里我们能看到的 analyzer 就有:
NotConstructorBoundInjectionFailureAnalyzer(本章)IncompatibleConfigurationFailureAnalyzerConfigDataNotFoundFailureAnalyzer(第 07 章)
写作/工程洞察 :这是"错误信息即产品"的理念。Boot 在这上面投入的工程量不小------每个 analyzer 都要写出准确的检测逻辑和有指导性的 Action 文本。这是 Boot 易用性的重要来源,也是最容易被自研框架忽略的部分。
第 12 节 · ConversionServiceDeducer 与 PropertySourcesDeducer
12.1 两个"推断器"
文件清单里:
ConversionServiceDeducer.java
PropertySourcesDeducer.java
从名字和 Deducer 后缀(第 01 章讲过 Boot 用 Deducer 命名"推断器")可以判断它们的职责:
ConversionServiceDeducer:从ApplicationContext里推断出应该用于绑定的ConversionServicePropertySourcesDeducer:推断出应该用于绑定的PropertySources
为什么需要"推断"而不是直接取?
因为 ConfigurationPropertiesBindingPostProcessor 可能运行在各种上下文里:
- 普通
ApplicationContext - 有父上下文的场景
- 测试上下文
- 手动创建的
Binder
它需要从可用资源里"推断"出最合适的。
ConversionServiceDeducer的存在印证了第 06 章的结论 :Environment的 ConversionService 和 BeanFactory 的不是同一个。而绑定应该用哪个?由这个 Deducer 决定。
(具体推断规则我未逐行核实,不做断言。可确认的是它们存在,且职责如上。)
12.2 Deducer 命名约定
第 01 章讲过:WebApplicationType.Deducer 是 4.x 新增的 SPI。而这里有两个非 SPI 的 Deducer 类。
这说明
Deducer是 Boot 内部的一个命名约定 :"从环境推断出某个值"的类就叫XxxDeducer。这个约定值得在自己的项目里借鉴------它比XxxResolver更明确(Resolver 通常指"解析一个输入",Deducer 指"从上下文推断")。
第 13 节 · BoundConfigurationProperties 与运行时查询
文件清单里有 BoundConfigurationProperties.java。
从名字推断:它提供"某个 @ConfigurationProperties Bean 实际绑定了哪些属性"的信息。
典型用途 :Actuator 的 /actuator/configprops 用它展示每个配置 Bean 绑定了哪些属性(配合第 08 章的 BoundPropertiesTrackingBindHandler)。
这两个类的分工:
BoundPropertiesTrackingBindHandler:绑定过程中 收集ConfigurationPropertyBoundConfigurationProperties:绑定完成后提供查询接口
(具体实现我未逐行核实,不做断言。)
第 14 节 · PropertyMapper:Boot 内部的映射工具
文件清单里有 PropertyMapper.java(注意与 bind/source/PropertyMapper 不同,这是 context/properties/ 包下的)。
它是 Boot 内部的一个流式映射工具,用于"从源对象取值,转换,设置到目标对象"。
典型用法(示意):
java
PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
mapper.from(source::getPort).to(target::setPort);
mapper.from(source::getAddress).asInt().to(target::setAddress);
它与 Binder 的区别:
Binder |
PropertyMapper |
|
|---|---|---|
| 数据来源 | Environment(配置属性) |
任意 Java 对象(getter) |
| 典型用途 | 配置 → 对象 | 对象 A → 对象 B(如 Boot 属性 → 第三方库配置) |
| 空值处理 | 属性不存在 | alwaysApplyingWhenNonNull() 等策略 |
典型场景 :Boot 的自动配置里,把
ServerProperties的值映射到 Tomcat 的Connector上:
javaPropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); mapper.from(properties::getPort).to(connector::setPort);这不是配置绑定,而是"Boot 属性对象 → 第三方 API 对象"的映射。
PropertyMapper让这个映射简洁且空值安全。这是一个可以在自己项目里复用的工具类(虽然是 Boot 内部的,但思路值得借鉴)。
第 15 节 · 4.x 相对 3.x 的变化
| 方面 | 3.x | 4.1.1 |
|---|---|---|
@ConfigurationProperties 注解本身 |
--- | 基本稳定 |
BindMethod |
3.0.8 引入 | 沿用 |
ConfigurationPropertiesScan |
2.2.0 引入 | 沿用 |
| AOT processor | 有 | 沿用(两个) |
ConfigurationPropertiesSource |
有 | 沿用 |
NotConstructorBoundInjectionFailureAnalyzer |
有 | 沿用 |
| 包名 | org.springframework.boot.context.properties |
不变(未被 4.x 迁移影响) |
值得注意 :
context.properties包没有 被 4.x 的大规模包迁移波及(对比env→support的迁移)。这说明它被视为"核心稳定 API"。第 06 章讲过一批类从
org.springframework.boot.env/context.config迁到了org.springframework.boot.support。而context.properties保持不动------这是一个兼容性信号 :升级到 4.x 时,@ConfigurationProperties相关的代码基本不用改。
第 16 节 · 常见故障与源码级解释
故障一:加了 @ConfigurationProperties 但注入失败
现象 :NoSuchBeanDefinitionException: MyProps
源码解释 :@ConfigurationProperties 不含 @Component,组件扫描不捡它。
解法 :@EnableConfigurationProperties(MyProps.class) 或 @ConfigurationPropertiesScan。
故障二:@Component + @ConfigurationPropertiesScan 不生效
现象 :类上有 @Component,@ConfigurationPropertiesScan 扫不到
源码解释:Javadoc 明确说 "@Component 类不会被捡起"(1.4 节)
解法 :去掉 @Component。
故障三:构造器绑定 + @Component 失败
现象 :NoSuchBeanDefinitionException,但错误信息被 NotConstructorBoundInjectionFailureAnalyzer 转成明确提示
源码解释 :构造器绑定需要在 BeanDefinition 层面准备值,@Component 路径没有(3.3、11.2 节)
解法 :用 @EnableConfigurationProperties 或 @ConfigurationPropertiesScan
故障四:校验不生效
现象 :@NotNull 没起作用
排查顺序:
- 类上有没有
@Validated?(5.3 节) - classpath 上有没有 validation 实现?(5.2 节的
isJsr303Present) @ConfigurationProperties的ignoreInvalidFields是不是 true?(会吞掉错误)
故障五:ignoreUnknownFields=false 但没报错
源码解释 :NoUnboundElementsBindHandler 只对 IterableConfigurationPropertySource 检查(第 08 章第 7.3 节)
解法:确认你的配置源是可枚举的。
故障六:属性值没绑定(字段为 null)
排查顺序:
- prefix 写对了?(
my.name对应prefix="my"+ 字段name) - 用的是 kebab-case?(第 06 章讲过 camelCase 的问题)
- 有 setter 吗?(JavaBean 绑定)
- 是构造器绑定吗?(有 setter 也不走)
- 该属性被
ignoreInvalidFields吞掉了吗?
第 17 节 · 端到端追踪:从启动到绑定的完整链路
把第 06-09 章的内容串成一条完整的时间线。假设:
java
@SpringBootApplication
@EnableConfigurationProperties(MyProps.class)
public class App { }
@ConfigurationProperties(prefix = "my")
@Validated
public class MyProps {
@NotNull private String name;
private Duration timeout = Duration.ofSeconds(10);
// getter/setter
}
阶段 1:SpringApplication.run() 的前半程
(第 04-07 章)
scss
run()
├─ prepareEnvironment
│ ├─ getOrCreateEnvironment → ApplicationEnvironment
│ ├─ configureEnvironment
│ │ ├─ setConversionService(new ApplicationConversionService()) ← 第 06 章
│ │ ├─ configurePropertySources(commandLineArgs/defaultProperties/applicationInfo)
│ │ └─ configureProfiles(空实现)
│ ├─ ConfigurationPropertySources.attach(environment)
│ ├─ listeners.environmentPrepared
│ │ └─ EnvironmentPostProcessorApplicationListener
│ │ ├─ ConfigDataEnvironmentPostProcessor ← 第 07 章(六步)
│ │ ├─ RandomValuePropertySourceEnvironmentPostProcessor
│ │ ├─ SpringApplicationJsonEnvironmentPostProcessor
│ │ └─ SystemEnvironmentPropertySourceEnvironmentPostProcessor
│ ├─ ApplicationInfoPropertySource.moveToEnd / DefaultPropertiesPropertySource.moveToEnd
│ ├─ bindToSpringApplication(spring.main.*)
│ ├─ EnvironmentConverter.convertEnvironmentIfNecessary
│ └─ ConfigurationPropertySources.attach(第二次)
├─ printBanner
└─ createApplicationContext
此时 :Environment 里有完整的属性(命令行 > 系统属性 > 环境变量 > ConfigData > defaultProperties)。
阶段 2:prepareContext 与 refresh
scss
prepareContext
├─ context.setEnvironment(environment)
├─ applyInitializers
└─ load(注册 primarySources)
refreshContext → AbstractApplicationContext.refresh()
├─ invokeBeanFactoryPostProcessors(beanFactory)
│ ├─ ConfigurationClassPostProcessor 处理 @Configuration
│ │ └─ 处理 @EnableConfigurationProperties
│ │ └─ EnableConfigurationPropertiesRegistrar.registerBeanDefinitions
│ │ ├─ 注册 ConfigurationPropertiesBindingPostProcessor ★
│ │ └─ 为 MyProps 注册 BeanDefinition ★
│ │ ├─ name = "my-com.example.MyProps"
│ │ ├─ deduceBindMethod(MyProps) → JAVA_BEAN
│ │ └─ BindMethodAttribute.set(definition, JAVA_BEAN)
│ └─ 自动配置的 @AutoConfiguration 类也被处理
│ └─ 每个自动配置类上的 @EnableConfigurationProperties 同样生效
├─ registerBeanPostProcessors(beanFactory) ★ ConfigurationPropertiesBindingPostProcessor 注册为 BPP
└─ finishBeanFactoryInitialization(beanFactory)
└─ 实例化 MyProps
├─ instantiate(无参构造器)
├─ populateBean(依赖注入)------ MyProps 通常无依赖
└─ initializeBean
├─ postProcessBeforeInitialization
│ └─ ConfigurationPropertiesBindingPostProcessor.bind(...) ★★ 绑定
├─ invokeInitMethods(@PostConstruct)
└─ postProcessAfterInitialization
阶段 3:绑定内部
(第 08 章 + 本章第 4 节)
scss
ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization
└─ ConfigurationPropertiesBean.get(context, bean, beanName)
└─ 构造 Bindable(类型 + 注解)
└─ binder.bind(propertiesBean)
└─ ConfigurationPropertiesBinder.bindOrCreate
├─ getBindHandler(target, annotation)
│ ├─ getHandler()(IgnoreTopLevelConverterNotFound)
│ ├─ new ConfigurationPropertiesBindHandler(...)
│ ├─ ignoreInvalidFields? → IgnoreErrorsBindHandler
│ ├─ !ignoreUnknownFields? → NoUnboundElementsBindHandler
│ ├─ validators 非空? → ValidationBindHandler ★ 校验
│ └─ advisors
└─ getBinder().bindOrCreate("my", target, bindHandler)
└─ Binder.bind(...)
├─ onStart
├─ bindObject
│ └─ bindDataObject → JavaBeanBinder
│ ├─ bind("my.name") → "demo"
│ └─ bind("my.timeout") → Duration
├─ onSuccess(每层)
│ ├─ ValidationBindHandler 记录
│ ├─ NoUnboundElements 记录 boundNames
│ └─ ConfigurationPropertiesBindHandler
└─ onFinish(depth==0)
├─ NoUnboundElements.checkNoUnboundElements ★ 未绑定检测
└─ ValidationBindHandler 执行校验 ★ 校验
阶段 4:校验
ini
ValidationBindHandler.onFinish(depth == 0)
└─ 对每个 Validator:
├─ configurationPropertiesValidator(Spring 的)
└─ JSR-303 Validator(如果有)
└─ @NotNull 检查 MyProps.name
└─ 失败 → ConstraintViolationException
→ ValidationBindHandler 抛 BindException
→ ConfigurationPropertiesBindException
→ FailureAnalyzer → 可读错误信息
完整链路结束。四个阶段分别对应第 06-09 章。
第 18 节 · @ConfigurationProperties 在自动配置中的角色
18.1 为什么 Boot 自己的配置都用 @ConfigurationProperties
Boot 有几百个自动配置,每个都需要"读取配置 → 决定是否启用 / 如何配置"。
如果不这样做,代码会是:
java
// 反例:直接用 Environment
String port = environment.getProperty("server.port");
if (port != null) {
connector.setPort(Integer.parseInt(port)); // 无类型转换、无默认值、无校验
}
有了 @ConfigurationProperties:
java
@AutoConfiguration
@EnableConfigurationProperties(ServerProperties.class)
public class TomcatAutoConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatCustomizer(ServerProperties properties) {
return new TomcatWebServerFactoryCustomizer(properties);
}
}
四个好处:
- 类型安全 :
properties.getPort()是Integer,不是String - 默认值 :
private int port = 8080; - 元数据 :IDE 能补全
server.port - 校验 :可以加
@Validated
18.2 自动配置的"三段式"模式
Boot 的自动配置几乎都遵循这个模式:
less
① @AutoConfiguration
② @ConditionalOnXxx(条件判断)
③ @EnableConfigurationProperties(XxxProperties.class)
④ 定义 Bean,把 Properties 注入进去
⑤ 用 PropertyMapper 把 Properties 映射到第三方 API
java
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConfigurationProperties("spring.datasource.hikari") // ★ 注解在 @Bean 方法上
public HikariDataSource dataSource(DataSourceProperties properties) {
// ...
}
}
注意第 ④ 行的 @ConfigurationProperties 在 @Bean 方法上 ------这是把第三方类型(HikariDataSource)配置化的标准手法(第 1.7 节讲过)。
18.3 PropertyMapper 在自动配置里的典型用法
java
private void customizeTomcat(Tomcat tomcat, ServerProperties properties) {
PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
mapper.from(properties::getPort).to(tomcat::setPort);
mapper.from(properties::getAddress).to(tomcat::setAddress);
// ...
}
alwaysApplyingWhenNonNull() 是关键------只在源值非 null 时才设置。这样"用户没配置"就不会覆盖第三方的默认值。
这解决了一个重要问题 :Boot 的 Properties 对象的默认值(如
port = 8080)与第三方库的默认值(Tomcat 的8080)可能重复或冲突。PropertyMapper的空值处理 + Boot 的默认值设计共同保证行为正确。
18.4 一个真实的设计权衡:默认值放哪
Boot 的惯例是:默认值写在 @ConfigurationProperties 类的字段初始化里。
java
public class ServerProperties {
private int port = 8080; // ← 默认值
}
而不是:
java
// 反例
String port = environment.getProperty("server.port", "8080");
理由:
- 默认值会被元数据处理器提取,显示在 IDE 提示里(
defaultValue字段) - 默认值是类型安全的
- 集中在一处,易维护
实践建议 :迁移老代码时,把散落的
getProperty(key, default)收拢到@ConfigurationProperties类的字段初始化里。这不只是"整洁",它能让 IDE 提示更准确。
第 19 节 · 性能考量
19.1 绑定的成本来源
一次 @ConfigurationProperties 绑定的成本来自:
- 反射 :
JavaBeanBinder用Introspector发现属性(有缓存) - 属性查找 :每个属性都要遍历
ConfigurationPropertySource链 containsDescendantOf:判断是否有后代属性(第 08 章讲过缓存策略)- 转换:每次值转换都要查 Converter
19.2 优化手段
(a)BindConverter 的共享实例(第 08 章 15.5 节)
(b)ConfigurationPropertyCaching(第 08 章 2.4 节)
(c)containsNoDescendantOf 短路(第 08 章 4.2 节)
(d)isUnbindableBean 跳过(第 08 章第 10 节)
(e)bindHandlerAdvisors 的 volatile 缓存
java
private volatile @Nullable List<ConfigurationPropertiesBindHandlerAdvisor> bindHandlerAdvisors;
避免每次绑定都查 BeanFactory。
19.3 构造器绑定的性能优势
构造器绑定(record / 全参构造器)比 JavaBean 绑定更快:
- 不需要
Introspector(直接读构造参数) - 不需要 setter 反射调用
- 对象是不可变的,容器可以优化
实践建议 :在配置类很多的大型项目里,用构造器绑定(record)能略微加快启动。但差异通常不大------不要为了性能而改用构造器绑定,应该为了不可变性。
19.4 AOT 下的优化
AOT 模式下,@ConfigurationProperties 的反射元信息在构建期就确定了,native 下不需要运行时反射发现。这是 AOT 启动快的原因之一。
但注意:配置值本身仍是运行时读取的(第 7.3 节)。
第 20 节 · 三个完整的实战示例
20.1 多模块项目的元数据配置
模块 B(配置类型):
java
package com.example.common.config;
@ConfigurationPropertiesSource // ★ 关键:让模块 A 的处理器能看到
public class RetryPolicy {
/**
* Maximum number of retry attempts.
*/
private int maxAttempts = 3;
/**
* Backoff interval between retries.
*/
private Duration backoff = Duration.ofSeconds(1);
// getter/setter
}
模块 B 的 build.gradle:
groovy
dependencies {
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}
模块 A(配置类):
java
package com.example.app;
@ConfigurationProperties(prefix = "my.client")
public class ClientProperties {
@NestedConfigurationProperty
private final RetryPolicy retry = new RetryPolicy();
// getter
}
效果 :IDE 里输入 my.client.retry. 会提示 max-attempts 和 backoff,且带描述和默认值。
没有
@ConfigurationPropertiesSource的话 :my.client.retry能被识别(因为类型是RetryPolicy),但它的内部属性没有描述和默认值提示。
20.2 给第三方类型做配置化
java
@Configuration(proxyBeanMethods = false)
public class HttpClientConfiguration {
@Bean
@ConfigurationProperties("my.http-client") // ★ 注解在方法上
public OkHttpClient.Builder okHttpClientBuilder() {
return new OkHttpClient.Builder();
}
}
配置:
yaml
my:
http-client:
connect-timeout: 5s
read-timeout: 30s
原理 :ConfigurationPropertiesBean 从 @Bean 方法的返回类型构造 Bindable,绑定后写回(通过 setter)。
注意 :第三方类型必须有无参构造器 + setter (JavaBean 绑定)。
OkHttpClient.Builder正好符合。
20.3 带校验和默认值的不可变配置
java
@ConfigurationProperties(prefix = "my.service")
@Validated
public record ServiceProperties(
@NotNull String endpoint,
@DefaultValue("30s") Duration timeout,
@DefaultValue("3") int maxRetries,
@Min(1) @Max(100) int poolSize) {
public ServiceProperties {
// record 的紧凑构造器:额外的不变式检查
Assert.isTrue(timeout.toMillis() > 0, "timeout must be positive");
}
}
注册:
java
@SpringBootApplication
@ConfigurationPropertiesScan
public class App { }
四个机制同时生效:
@ConfigurationPropertiesScan扫描注册(第 1 节)deduceBindMethod推断为VALUE_OBJECT(record,第 8.2 节)@DefaultValue提供默认值(第 08 章 5.4 节)@Validated+@NotNull/@Min校验(第 5 节)
这是 Boot 3+/4.x 推荐的现代写法 :record + 构造器绑定 +
@DefaultValue+ 校验。不可变、类型安全、有默认值、能校验。
第 21 节 · 14 条反直觉结论汇总
-
@ConfigurationProperties不含@Component------ 光加注解不会有 Bean。必须有@EnableConfigurationProperties/@ConfigurationPropertiesScan/@Component之一。 -
@Component与@ConfigurationPropertiesScan互斥 ------ Javadoc 明确说"被@Component标注的类不会被@ConfigurationPropertiesScan捡起"。加了@Component反而扫不到。 -
@ConfigurationPropertiesScan自带@EnableConfigurationProperties------ 不需要同时写两个。 -
构造器绑定的 Bean 不能走
BeanPostProcessor------Assert.state显式拒绝VALUE_OBJECT。它必须在 BeanDefinition 层面(InstanceSupplier)就准备好值。 -
绑定发生在
@PostConstruct之前,但在依赖注入之后 ------ 所以@PostConstruct里能读到绑定后的值,但构造器里不能。 -
bindOrCreate保证非 null ------@ConfigurationPropertiesBean 永远是实例,即使一个相关属性都没配。这与spring.main.*用的bind不同。 -
ignoreInvalidFields=true+ignoreUnknownFields=false会产生困惑 ------ 转换失败的属性被 IgnoreErrors 吞掉(走onFailure),不会被记入boundNames,所以会被报告为"未绑定"。 -
ValidationBindHandler在装饰器链最外层 ------ 校验在所有绑定完成后执行,支持跨字段校验。 -
@Validated是校验的开关 ------ 没有它,字段上的@NotNull不生效。 -
JSR-303 需要 classpath 上有实现 ------
ConfigurationPropertiesJsr303Validator.isJsr303Present显式检测。没有spring-boot-starter-validation就静默不校验。 -
Bean 名 =
前缀-类全限定名------ 可预测,可用于@Qualifier;也意味着同类重复注册会被Assert.state拦下。 -
@ConfigurationPropertiesSource对绑定完全无影响** ------ 它纯粹是给编译期注解处理器的提示,用于跨模块生成元数据。 -
NotConstructorBoundInjectionFailureAnalyzer用xxx.class.getSimpleName()而非硬编码字符串 ------ 错误信息不会因改名而过期。 -
context.properties包在 4.x 大迁移中保持不动 ------ 对比env→support的迁移。这是"核心稳定 API"的信号。
补充:
-
PropertyMapper与Binder是两套东西 ------ 前者是"Java 对象 → Java 对象"的映射(自动配置里把 Properties 映射到第三方 API),后者是"配置 → Java 对象"的绑定。 -
默认值应写在字段初始化里 ,而不是
getProperty(key, default)------ 前者会被元数据处理器提取,IDE 能显示。 -
MergedAnnotation让@ConfigurationProperties可被组合 ------ 自定义注解元标注它,同样生效。 -
ConversionServiceDeducer/PropertySourcesDeducer的存在印证了第 06 章的结论:Environment 的 ConversionService 与 BeanFactory 的不是同一个,绑定时需要"推断"用哪个。
第 22 节 · 动手验证清单
java
// 1. 验证 @ConfigurationProperties 不含 @Component
@ConfigurationProperties("my")
public class MyProps { ... }
@Autowired MyProps props; // ❌ NoSuchBeanDefinitionException
// 2. 验证四种注册方式
@EnableConfigurationProperties(MyProps.class) // ✅
@ConfigurationPropertiesScan // ✅(且自动带 EnableConfigurationProperties)
@Component + @ConfigurationProperties // ✅(但不被 Scan 捡起)
// 3. 验证 Bean 名
ctx.getBeanDefinitionNames() // 含 "my-com.example.MyProps"
// 4. 验证构造器绑定不能走 BPP
@Component
@ConfigurationProperties("my")
public record MyProps(String name) { }
// → 启动失败,错误信息由 NotConstructorBoundInjectionFailureAnalyzer 生成
// 5. 验证 bindOrCreate 保证非 null
// 不配任何 my.* → MyProps 实例存在,字段为默认值
// 6. 验证 @Validated 是开关
@ConfigurationProperties("my")
public class MyProps { @NotNull private String name; }
// 不生效(无 @Validated)
@Validated
@ConfigurationProperties("my")
public class MyProps { @NotNull private String name; }
// 生效
// 7. 验证 JSR-303 依赖 classpath
// 移除 spring-boot-starter-validation → 校验静默失效
// 8. 验证 @DefaultValue
public record MyProps(@DefaultValue("30s") Duration timeout) { }
// 不配 my.timeout → 30s
// 9. 验证 @Bean 方法上的注解
@Bean @ConfigurationProperties("my.client")
public OkHttpClient.Builder builder() { ... }
// 10. 验证绑定时机
@PostConstruct
void init() { System.out.println(name); } // ✅ 能读到绑定后的值
bash
# 11. 查看所有配置属性 Bean
curl http://localhost:8080/actuator/configprops
# 12. 查看元数据
cat build/classes/java/main/META-INF/spring-configuration-metadata.json
# 13. 验证跨模块元数据
# 模块 B 加 @ConfigurationPropertiesSource + 注解处理器
# → 模块 A 的 IDE 补全显示描述和默认值
# 14. 验证 ignoreInvalidFields 与 ignoreUnknownFields 的交互
@ConfigurationProperties(prefix="my", ignoreInvalidFields=true, ignoreUnknownFields=false)
# 配置一个转换失败的属性 → 报告为"未绑定"
第 23 节 · 源码索引
| 主题 | 文件 |
|---|---|
| 核心注解 | context/properties/ConfigurationProperties.java |
| Bean 抽象 | context/properties/ConfigurationPropertiesBean.java |
| 注册器 | context/properties/ConfigurationPropertiesBeanRegistrar.java |
| 绑定后处理器 | context/properties/ConfigurationPropertiesBindingPostProcessor.java |
| 绑定器 | context/properties/ConfigurationPropertiesBinder.java |
| 启用注解 | context/properties/EnableConfigurationProperties.java、EnableConfigurationPropertiesRegistrar.java |
| 扫描注解 | context/properties/ConfigurationPropertiesScan.java、ConfigurationPropertiesScanRegistrar.java |
| 绑定模式标记 | context/properties/BindMethodAttribute.java |
| 构造器绑定标记 | context/properties/ConstructorBound.java |
| 已绑定属性 | context/properties/BoundConfigurationProperties.java |
| 元数据提示 | context/properties/ConfigurationPropertiesSource.java |
| 嵌套属性 | context/properties/NestedConfigurationProperty.java |
| 弃用标记 | context/properties/DeprecatedConfigurationProperty.java |
| 校验器 | context/properties/ConfigurationPropertiesJsr303Validator.java |
| Handler 扩展点 | context/properties/ConfigurationPropertiesBindHandlerAdvisor.java |
| 冲突异常 | context/properties/IncompatibleConfigurationException.java |
| 冲突分析器 | context/properties/IncompatibleConfigurationFailureAnalyzer.java |
| 构造器绑定分析器 | context/properties/NotConstructorBoundInjectionFailureAnalyzer.java |
| 推断器 | context/properties/ConversionServiceDeducer.java、PropertySourcesDeducer.java |
| 映射工具 | context/properties/PropertyMapper.java |
| AOT | context/properties/ConfigurationPropertiesBeanFactoryInitializationAotProcessor.java、ConfigurationPropertiesBeanRegistrationAotProcessor.java |
| 绑定异常 | context/properties/ConfigurationPropertiesBindException.java |
| 校验 handler | context/properties/bind/validation/ValidationBindHandler.java |
本章未逐行核实 / 待你实测的项
- Javadoc 中"@Component 类不会被捡起"的实现代码未追踪到(文中已声明)。
getHandler()返回IgnoreTopLevelConverterNotFoundBindHandler是推测(文中已声明)。ConfigurationPropertiesBindingPostProcessor的完整流程只讲了主干。@ConfigurationPropertiesScan与*.imports的交互未实测。- AOT 下构造器绑定的代码生成细节在第 28 章。
下一章 :第 10 章进入 Profile 语义与多文档------spring.config.activate.*、profile-specific 文件与多文档解析。
阶段性小结(第一次收束)
@ConfigurationProperties 是 Spring Boot 配置体系的用户接口层 。它把第 08 章的 Binder 引擎包装成一个"容器管理的、可校验的、有元数据的"Bean。
五个核心机制:
-
注册 :
@ConfigurationProperties本身不注册 Bean------需要@EnableConfigurationProperties/@ConfigurationPropertiesScan/@Component之一。ConfigurationPropertiesBeanRegistrar负责实际的 BeanDefinition 注册,并在此时就确定BindMethod(写入BindMethodAttribute)。 -
统一抽象 :
ConfigurationPropertiesBean屏蔽"注解在类上"与"注解在@Bean方法上"的差异------这是"给第三方类型做配置化"的技术基础。 -
装饰器链 :
getBindHandler把@ConfigurationProperties的每个属性翻译成一个BindHandler,按顺序组装。ignoreInvalidFields→IgnoreErrorsBindHandler;ignoreUnknownFields=false→NoUnboundElementsBindHandler;校验 →ValidationBindHandler。这是"配置驱动的行为"以组合而非分支实现的典范。 -
绑定时机 :
BeanPostProcessor的postProcessBeforeInitialization,在依赖注入之后、@PostConstruct之前。但构造器绑定不走这条路------必须在 BeanDefinition 层面准备值。 -
诊断 :
NotConstructorBoundInjectionFailureAnalyzer这类 FailureAnalyzer 把难排查的装配错误翻译成"发生了什么 + 你该做什么"。
三个最有实践价值的结论:
@Component+@ConfigurationPropertiesScan会静默失效(互斥),这是最容易踩的坑。ignoreInvalidFields=true与ignoreUnknownFields=false同时用会产生反直觉的"未绑定"报告。- 校验需要两个条件 :类上有
@Validated,且 classpath 上有 validation 实现。
下一章(第 10 章)我们进入条件注解体系 ------@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等。它们是"自动配置如何决定自己是否生效"的机制,也是 Boot "约定优于配置" 的技术实现。
第 24 节 · getHandler() 的完整实证(修正第 4 节的推测)
第 4 节我推测 getHandler() 可能是 IgnoreTopLevelConverterNotFoundBindHandler。现在有了源码实证------推测正确,而且实际情况比推测更精细:
java
private IgnoreTopLevelConverterNotFoundBindHandler getHandler() {
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.applicationContext);
return (bound != null)
? new IgnoreTopLevelConverterNotFoundBindHandler(new BoundPropertiesTrackingBindHandler(bound::add))
: new IgnoreTopLevelConverterNotFoundBindHandler();
}
三条信息:
(a)返回类型就是 IgnoreTopLevelConverterNotFoundBindHandler ------ 它是链的最内层。
(b)内部可选地包一个 BoundPropertiesTrackingBindHandler ------ 但只当 BoundConfigurationProperties.get(context) 非 null 时。
BoundConfigurationProperties 是从 ApplicationContext 里取的(推测是作为一个 Bean 或 attribute)。它是 Actuator 需要的数据容器------所以"是否收集已绑定属性"是条件性的:只有 Actuator(或类似功能)存在时才收集。
这是一个优雅的性能优化 :没有 Actuator 时,不做无用的属性收集。这体现了 Boot 的一贯做法------可观测性功能的开销只在启用时产生。
(c)这印证了第 08 章第 17 节的推断 ------ 我在那里说 "/actuator/configprops 的数据来源之一是 BoundPropertiesTrackingBindHandler",现在有了直接的组装证据。
24.1 完整的装饰器链(最终确认版)
从内到外:
ini
① IgnoreTopLevelConverterNotFoundBindHandler ← getHandler()
└(可选)② BoundPropertiesTrackingBindHandler
③ ConfigurationPropertiesBindHandler
④ IgnoreErrorsBindHandler (若 ignoreInvalidFields=true)
⑤ NoUnboundElementsBindHandler (若 ignoreUnknownFields=false)
⑥ ValidationBindHandler (若有 Validator)
⑦... advisors(按 @Order 排序)
执行顺序(onSuccess 为例) :从 ⑦ 到 ①(外层先执行,再委托内层)。 执行顺序(onStart 为例):同样从外到内。
注意 ② 的位置 :它在最内层 (仅次于 ①)。这意味着它在
ConfigurationPropertiesBindHandler之后才被调用......等等,不对。
BoundPropertiesTrackingBindHandler是 ① 的构造参数 ,即它是 ① 的 parent(内层)。所以onSuccess的执行顺序是:③ → ① → ②(③ 先,然后委托给 ①,① 委托给 ②)。而 ② 的检查条件是
context.getConfigurationProperty() != null && name.equals(...getName())。因为它在最内层,只有真正到达"属性绑定成功"这一步才会执行 ------中间层的对象绑定(如my.server)在被 ③ 处理后...... 嗯,实际上onSuccess会在每层都调用,每层都会委托到最内层。所以 ② 的条件判断(第 08 章 17.2 节讲过)是必要的:它过滤掉中间层的对象绑定,只记录叶子属性。
24.2 一个重要的推论
IgnoreTopLevelConverterNotFoundBindHandler 在最内层意味着:它是最后 才处理 onFailure 的(因为 onFailure 从外层往内层传播,外层先有机会"救回")。
但 IgnoreErrorsBindHandler(④)在外层------它先拿到错误,如果它决定"忽略",错误就不会传播到 ①。
这解释了第 21 节第 7 条 :
ignoreInvalidFields=true时错误被 ④ 吞掉,① 根本不知道发生过错误。而NoUnboundElementsBindHandler(⑤)在 ④ 的外层 ------它的onSuccess在 ④ 之前执行,而 ④ 吞错走的是onFailure路径,⑤ 的onSuccess不会被调用(异常路径)。
完整因果链确认:
csharp
属性绑定失败
→ ③ ConfigurationPropertiesBindHandler.onFailure(默认:抛)
→ ④ IgnoreErrorsBindHandler.onFailure(吞掉,返回 null)
→ ⑤ NoUnboundElementsBindHandler 的 onSuccess **未被调用**(因为走了异常路径)
→ 该属性未加入 boundNames
→ onFinish 时被报告为"未绑定"
这个链条现在有了完整的源码证据。第 21 节第 7 条的结论从"推断"升级为"实证"。
第 25 节 · getValidators:@Validated 是开关的源码证据
java
private List<Validator> getValidators(Bindable<?> target) {
List<Validator> validators = new ArrayList<>(3);
if (this.configurationPropertiesValidator != null) {
validators.add(this.configurationPropertiesValidator);
}
if (this.jsr303Present && target.getAnnotation(Validated.class) != null) {
// ...
}
// ...
}
两个条件都必须满足才加 JSR-303 校验器:
this.jsr303Present------ classpath 上有 JSR-303 实现target.getAnnotation(Validated.class) != null------ 目标上有@Validated注解
而第二个条件用的是 target.getAnnotation(...) ------ 即 Bindable 的注解!
这完美印证了第 2.3 节的论述 :注解被装进
Bindable(来自类或@Bean方法),通过ResolvableTypeDescriptor传递,最终在这里被读取。三类注解通过同一条通道传递:
ConfigurationProperties(getAnnotation()读,用于 prefix)Validated(getValidators读,用于决定是否校验)@DurationUnit/@DataSizeUnit(Converter 通过TypeDescriptor读)
25.1 new ArrayList<>(3) 的容量预设
java
List<Validator> validators = new ArrayList<>(3);
预设容量 3------因为最多可能有:
configurationPropertiesValidator- JSR-303 validator
- 可能的第三个(如
@Validated上的自定义 validator)
这是一个微优化,但也反映了作者对"最多几个"的清晰认识。
25.2 configurationPropertiesValidator 的查找
java
private @Nullable Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext) {
if (applicationContext.containsBean(VALIDATOR_BEAN_NAME)) {
return applicationContext.getBean(VALIDATOR_BEAN_NAME, Validator.class);
}
return null;
}
先 containsBean 再 getBean ------ 避免 NoSuchBeanDefinitionException 的抛出/捕获开销。
这是一个值得借鉴的习惯 :用
containsBean做存在性检查,比 try-catch 更清晰、更快(异常构造要填充栈轨迹)。
VALIDATOR_BEAN_NAME 的值是 configurationPropertiesValidator(Spring Boot 约定的 Bean 名)。
第 26 节 · ConfigurationPropertiesCharSequenceToObjectConverter:ThreadLocal 防递归
这是一个只有 80 行但极其精巧的类。
26.1 类的来历
java
/**
* Copy of package-private
* {@code org.springframework.boot.convert.CharSequenceToObjectConverter}, renamed for
* differentiation.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
final class ConfigurationPropertiesCharSequenceToObjectConverter implements ConditionalGenericConverter {
它是 CharSequenceToObjectConverter 的一个副本(因为原类是包私有,无法复用,所以复制了一份)。
这是一个值得注意的代码异味 :重复代码。但考虑到 Spring 的包结构限制(
org.springframework.boot.convert与org.springframework.boot.context.properties不同包),复制是代价最小的方案。Boot 在 Javadoc 里明确说明了这是副本及其原因------这比"静默复制"好得多。
26.2 它解决什么问题
配置属性的原始值类型不一定是 String:
application.properties解析出来通常是String- 但
ConfigTreePropertySource返回的是Value(实现CharSequence) - 某些自定义 source 可能返回
CharSequence的其他实现 - 甚至可能是
OriginTrackedValue(包装的CharSequence)
而 ConvertiblePair 的匹配是精确类型匹配 ------(CharSequence, Object) 不会匹配 (String, Duration)。
所以需要一个 CharSequence → Object 的兜底转换器:把 CharSequence toString() 后,再走 String → Object 的转换。
java
@Override
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
return this.conversionService.convert(source.toString(), STRING, targetType);
}
26.3 ThreadLocal<Boolean> disable:防止无限递归
这是本类最精彩的部分。
java
private final ThreadLocal<Boolean> disable = new ThreadLocal<>();
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType.getType() == String.class || this.disable.get() == Boolean.TRUE) {
return false;
}
this.disable.set(Boolean.TRUE);
try {
boolean canDirectlyConvertCharSequence = this.conversionService.canConvert(sourceType, targetType);
if (canDirectlyConvertCharSequence && !isStringConversionBetter(sourceType, targetType)) {
return false;
}
return this.conversionService.canConvert(STRING, targetType);
}
finally {
this.disable.remove();
}
}
递归风险 :matches 内部调用 this.conversionService.canConvert(...),而 this.conversionService 就是包含本 converter 的那个 ConversionService。所以:
scss
canConvert(CharSequence, Duration)
→ 遍历 converters,找到本 converter
→ 调 matches(CharSequence, Duration)
→ 内部调 canConvert(CharSequence, Duration) ← 又回来了!无限递归
解法 :ThreadLocal<Boolean> disable。
- 进入
matches时,先检查disable.get() == TRUE,是则返回 false(不再递归) - 否则设
disable = TRUE - 执行内部的
canConvert(此时递归回来会因 disable 而短路) finally中disable.remove()(注意是remove()不是set(null),避免 ThreadLocal 内存泄漏)
这是一个教科书级的"重入保护"模式 。同样的模式在 Spring 的其他地方也能见到(如
AbstractApplicationContext的prototypesCurrentlyInCreation)。
remove()vsset(null)的区别值得记住:ThreadLocal.set(null)会在当前线程的 map 里留一个null值的 entry;remove()才是真正的清理。虽然现代 JDK 对set(null)有优化,但remove()是明确的正确写法。
26.4 isStringConversionBetter:什么时候 String 转换更好
java
private boolean isStringConversionBetter(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (this.conversionService instanceof ApplicationConversionService applicationConversionService) {
if (applicationConversionService.isConvertViaObjectSourceType(sourceType, targetType)) {
// If an ObjectTo... converter is being used then there might be a
// better StringTo... version
return true;
}
}
// StringToArrayConverter / StringToCollectionConverter are better than
// ObjectToArrayConverter / ObjectToCollectionConverter
return (targetType.isArray() || targetType.isCollection()) && !targetType.equals(BYTE_ARRAY);
}
两种情况认为"String 转换更好":
(a)当前走的是 ObjectTo... 转换器
isConvertViaObjectSourceType 是第 06 章讲过的 ApplicationConversionService 方法------它检查转换器是否声明了 Object.class 作为 source type。
如果当前能用的转换器是"从任意 Object 出发"的(如 ObjectToStringConverter 的逆向、ObjectToCollectionConverter),那么结果往往是通用的 (如把一个 CharSequence 当一个对象处理),而 StringTo... 版本通常更精确(如 StringToDurationConverter)。
(b)目标是数组或集合(除 byte[])
java
return (targetType.isArray() || targetType.isCollection()) && !targetType.equals(BYTE_ARRAY);
ObjectToArrayConverter 会把 CharSequence 转成"单元素数组"(把整个字符串当一个元素); StringToArrayConverter 会做逗号分隔 ("a,b,c" → [a,b,c])。
后者才是配置绑定想要的。
byte[] 被排除 :因为 byte[] 通常表示二进制内容(如证书),不应该做逗号分隔。InputStreamSourceToByteArrayConverter 处理这种情况。
注释写得极好:
java// StringToArrayConverter / StringToCollectionConverter are better than // ObjectToArrayConverter / ObjectToCollectionConverter两行注释解释了整个方法的动机。这是 Boot 源码注释质量的又一例。
26.5 这个转换器为什么单独存在
因为它要挂在一个专门的 ConversionService 上(用于配置绑定),而不是全局的 ApplicationConversionService。
(具体挂载点我未逐行核实,但类的存在说明配置绑定有自己独立的转换链,与第 06 章讲的"两个 ConversionService 实例"呼应。)
第 27 节 · PropertySourcesDeducer 与 Binder 的构造
java
ConfigurationPropertiesBinder(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources();
this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext);
this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext);
}
PropertySourcesDeducer 在构造时被调用 ,推断出用于绑定的 PropertySources。
而 getBinder():
java
private Binder getBinder() {
Binder binder = this.binder;
// ... 懒加载
}
volatile @Nullable Binder binder ------ 懒加载 + 缓存。因为 Binder 是不可变的(构造后 sources 不变),可以缓存复用。
对比第 08 章的
BindConverter:BindConverter不能 缓存(非线程安全,每次新建),而Binder可以缓存。差异在于:
Binder本身无状态(状态在Context里,每次bind新建Context),而BindConverter持有有状态的TypeConverterConversionService。这是一个清晰的设计边界:无状态对象可共享,有状态对象 per-operation。Boot 在这里划得很清楚。
第 28 节 · 配置体系全景回顾(第 06-09 章)
css
用户视角
│
@ConfigurationProperties
│
┌───────────────────┼───────────────────┐
│ │ │
注册机制 绑定调度 元数据
(本章第 1-3 节) (本章第 4 节) (本章第 9 节)
│ │ │
EnableConfiguration ConfigurationProperties 注解处理器
Properties / Scan Binder(装饰器链) (编译期)
│ │ │
└───────────────────┼───────────────────┘
│
Binder(第 08 章)
│
┌─────────────┼─────────────┐
│ │ │
ConfigurationPropertySources
│ │ │
(第 06 章的适配层) │ │
│ │ │
▼ ▼ ▼
MutablePropertySources(有序列表,第 06 章)
│
├── commandLineArgs
├── systemProperties
├── systemEnvironment
├── ConfigData 加载的源(第 07 章)
├── defaultProperties
└── applicationInfo
│
▼
Environment(第 06 章)
四层的职责边界:
| 层 | 职责 | 扩展点 |
|---|---|---|
@ConfigurationProperties |
用户接口、注册、校验 | @EnableConfigurationProperties、ConfigurationPropertiesBindHandlerAdvisor |
Binder |
字符串 → 对象 | BindHandler、Converter、BindConstructorProvider |
Environment |
属性存储与查找 | PropertySource、EnvironmentPostProcessor |
ConfigData |
属性加载 | ConfigDataLocationResolver、ConfigDataLoader、PropertySourceLoader |
每一层都有明确的 SPI,且互不影响。这是 Boot 配置体系能被 Spring Cloud 等生态扩展的根本原因。
第 29 节 · 补充的 5 条结论
-
getHandler()返回IgnoreTopLevelConverterNotFoundBindHandler,内部可选包BoundPropertiesTrackingBindHandler------ 且只在BoundConfigurationProperties存在时(即 Actuator 启用时)才收集。可观测性开销只在启用时产生。 -
@Validated是通过target.getAnnotation(Validated.class)检测的 ------ 从Bindable的注解里读,印证了"注解通过ResolvableTypeDescriptor传递"的机制。 -
ConfigurationPropertiesCharSequenceToObjectConverter用ThreadLocal防止matches的无限递归 ------ 因为matches内部调用了包含自己的 ConversionService。 -
isStringConversionBetter判断"String 转换优于 Object 转换" ------ 两种情况:ObjectTo...转换器,或目标是数组/集合(除byte[])。这保证了"a,b,c"被逗号分隔成数组,而不是当单元素。 -
Binder可缓存(volatile懒加载),BindConverter不可缓存 ------ 前者无状态(状态在Context),后者持有有状态的TypeConverterConversionService。
第 30 节 · 本章边界(诚实声明)
以下内容我没有逐行核实:
ConfigurationPropertiesBeanRegistrar中VALUE_OBJECT分支的具体实现(InstanceSupplier的设置方式)ConstructorBound注解的确切语义BoundConfigurationProperties.get(applicationContext)的具体机制PropertySourcesDeducer/ConversionServiceDeducer的推断规则ConfigurationPropertiesBindHandler的内部逻辑(如@NestedConfigurationProperty的处理)- 两个 AOT processor 的具体实现
ValidationBindHandler的执行时机细节(推断为onFinish,依据是它在链最外层)
凡涉及推断的地方都在正文标注了"推测""未核实"。
本章小结
@ConfigurationProperties 是 Spring Boot 配置体系的用户接口层 。它把第 08 章的 Binder 引擎包装成"容器管理的、可校验的、有元数据的"Bean。
五个核心机制:
-
注册 ------
@ConfigurationProperties本身不注册 Bean。需要@EnableConfigurationProperties/@ConfigurationPropertiesScan/@Component之一。ConfigurationPropertiesBeanRegistrar在注册时就确定BindMethod并写入BindMethodAttribute(跨阶段传递元信息)。 -
统一抽象 ------
ConfigurationPropertiesBean屏蔽"注解在类上"与"注解在@Bean方法上"的差异,这是"给第三方类型做配置化"的技术基础。 -
装饰器链 ------
getBindHandler把每个注解属性翻译成一个BindHandler。这是"配置驱动的行为以组合而非分支实现"的典范 :从内到外是IgnoreTopLevelConverterNotFound→ConfigurationPropertiesBindHandler→IgnoreErrors(可选)→NoUnboundElements(可选)→Validation(可选)→ advisors。 -
绑定时机 ------
BeanPostProcessor.postProcessBeforeInitialization,在依赖注入之后、@PostConstruct之前。但构造器绑定不走这条路 (Assert.state显式拒绝VALUE_OBJECT)。 -
诊断 ------ FailureAnalyzer 体系把难排查的装配错误翻译成"发生了什么 + 你该做什么"。
NotConstructorBoundInjectionFailureAnalyzer是最好的例子------它甚至用xxx.class.getSimpleName()避免硬编码字符串过期。
三个最有实践价值的结论:
@Component+@ConfigurationPropertiesScan会静默失效(互斥),最容易踩的坑。ignoreInvalidFields=true+ignoreUnknownFields=false会产生反直觉的"未绑定"报告(现在有完整源码证据)。- 校验需要两个条件 :类上有
@Validated,且 classpath 上有 validation 实现。
下一章(第 10 章)进入条件注解体系 ------@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等。它们是"自动配置如何决定自己是否生效"的机制,也是 Boot "约定优于配置"的技术实现。
附录 A · 配置体系综合排障手册(第 06-09 章整合)
本附录把四章的排障知识整合成一份可查手册。建议收藏------遇到配置问题时按图索骥。
A.1 症状 → 排查路径
症状 1:"配置值不对 / 没生效"
markdown
1. dump PropertySource 列表(第 06 章第 9 节)
├─ 目标 source 不在列表?→ 文件没被加载(见症状 2)
└─ 在 → 继续
2. 打印每个 source 里该 key 的值 → 找出"第一个命中的"是谁
└─ 更高优先级的 source 有意外的值 → 找到了
3. key 是合法 ConfigurationPropertyName 吗?
├─ 否(含 @、空元素等)→ 走路径 B,宽松绑定失效(第 06 章 7.3)
└─ 是 → 继续
4. 用哪种读取方式?
├─ @ConfigurationProperties → Binder 路径(第 08 章)
├─ @Value → BeanFactory 的 ConversionService(第 06 章 3.3)
└─ environment.getProperty → 第 06 章路径 A/B
5. profile 是否如你所想?(第 06 章 1.2.1:Boot 关掉了 AbstractEnvironment 的自动读取)
6. AOT/native?profiles 是构建期常量(第 06 章 6.2、第 07 章 11)
症状 2:"配置文件没被加载"
arduino
1. 用 spring.config.location 了?→ 它会替换默认位置(第 07 章 3.5)
└─ 改用 additional-location 或 import
2. profile 文件名对吗?(application-{profile}.properties)
3. spring.config.name 被改了吗?
4. 打开 TRACE:--logging.level.org.springframework.boot.context.config=TRACE
└─ 看 "Imported N resources [...]"
5. 位置写对了吗?相对路径按父目录解析(第 07 章 3.6)
6. optional: 前缀?非 optional 找不到会启动失败
症状 3:"@ConfigurationProperties Bean 注入失败"
scss
1. 有没有注册方式?(@EnableConfigurationProperties / Scan / @Component)
2. 用了 @Component + @ConfigurationPropertiesScan?→ 互斥,扫不到
3. 是构造器绑定(record)吗?
├─ 且有 @Component?→ 必须用 Enable/Scan(NotConstructorBoundInjectionFailureAnalyzer)
└─ 有多个构造器?→ 标 @ConstructorBinding
4. 包名在扫描范围内吗?(@ComponentScan 的包)
症状 4:"字段是 null / 默认值"
markdown
1. prefix 对吗?
2. 属性名用 kebab-case 吗?(第 06 章:camelCase 不能被 kebab 读到)
3. 有 setter 吗?(JavaBean 绑定)
4. 是构造器绑定吗?(有 setter 也不走)
5. 被 ignoreInvalidFields 吞掉了吗?(转换失败静默)
6. 集合下标连续吗?(第 08 章 6.2)
7. 该属性所在的 source 优先级够高吗?
症状 5:"校验不生效"
markdown
1. 类上有 @Validated 吗?(第 09 章第 5 节)
2. classpath 上有 spring-boot-starter-validation 吗?
3. ignoreInvalidFields 是 true 吗?
症状 6:"启动时报配置错误"
ini
1. 读 Action 部分------Boot 的 FailureAnalyzer 通常给出了明确指引
2. 常见类型:
├─ ConfigDataNotFoundException → 加 optional: 前缀
├─ InvalidConfigDataPropertyException → 见第 07 章 8.1 的两张映射表
├─ UnboundConfigurationPropertiesException → 有拼错的属性(ignsoreUnknownFields=false 时)
├─ BindException(转换失败)→ 看 Value 和 Reason
└─ NotConstructorBound... → 注册方式不对
A.2 五条"一定会踩"的坑
| # | 坑 | 机制 | 规避 |
|---|---|---|---|
| 1 | spring.config.location 替换而非追加 |
orElse(DEFAULT_SEARCH_LOCATIONS) |
用 additional-location 或 import |
| 2 | optional: 只作用于单片段 |
split(";") 后逐段解析 |
每段都加 optional: |
| 3 | 集合下标不连续 | for(i=0;;) break on null |
yaml 列表语法 |
| 4 | 集合不跨 source 合并 | 第一个非空 source 胜出 | 用 Map |
| 5 | @Component + Scan 互斥 |
Javadoc 明确声明 | 二选一 |
A.3 三个"打开就明白"的调试开关
bash
# 1. ConfigData 全流程(最有用)
--logging.level.org.springframework.boot.context.config=TRACE
# 2. 属性查找过程
--logging.level.org.springframework.core.env=TRACE
--logging.level.org.springframework.boot.context.properties.source=TRACE
# 3. 自动配置决策(第 10 章主题)
--debug
A.4 一份可复用的 dump 代码
java
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class ConfigDump implements ApplicationRunner {
private final Environment environment;
ConfigDump(Environment environment) {
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
if (!(this.environment instanceof ConfigurableEnvironment env)) {
return;
}
System.out.println("\n===== PropertySources (高 → 低) =====");
int i = 0;
for (PropertySource<?> ps : env.getPropertySources()) {
System.out.printf("[%2d] %-45s %s%n", i++, ps.getName(),
ps.getClass().getSimpleName());
}
System.out.println("\n===== activeProfiles =====");
System.out.println(Arrays.toString(env.getActiveProfiles()));
System.out.println("\n===== 关键 key 的命中情况 =====");
for (String key : List.of("server.port", "spring.profiles.active")) {
System.out.println(key + " => " + env.getProperty(key));
}
System.out.println("\n===== ConversionService 实例 =====");
System.out.println("env: " + System.identityHashCode(env.getConversionService()));
if (env instanceof ConfigurableApplicationContext) { /* ... */ }
}
}
提示 :把这个类放进项目的
devprofile 下,遇到配置问题时临时启用。
A.5 配置体系的"心智模型"三句话
- 优先级 = 数组下标 ------
MutablePropertySources是一个CopyOnWriteArrayList,第一个命中胜出,没有权重计算。 - 四层独立,各有 SPI ------ Environment(PropertySource)→ ConfigData(Resolver/Loader)→ Binder(Handler/Converter)→
@ConfigurationProperties(注册/校验)。扩展任一层的 SPI 都不影响其他层。 - Boot 选择严格而非宽容 ------ 配置拼错、位置不合法、profile 冲突,都倾向于启动失败 而不是静默忽略。如果遇到"静默失效",先怀疑是不是某个
ignoreXxx开关导致的。
附录 B · 配置相关属性速查表
B.1 spring.config.*(第 07 章)
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
spring.config.name |
String[] |
application |
配置文件名(不含扩展名)。不支持通配 * |
spring.config.location |
ConfigDataLocation[] |
DEFAULT_SEARCH_LOCATIONS |
替换默认位置 |
spring.config.additional-location |
ConfigDataLocation[] |
空 | 追加位置(默认位置仍生效) |
spring.config.import |
ConfigDataLocation[] |
空 | 导入任意位置,可在任何配置文件里出现,支持嵌套 |
spring.config.on-not-found |
ConfigDataNotFoundAction |
FAIL |
全局控制"找不到时是否失败" |
spring.config.activate.on-profile |
String[](表达式) |
空 | 该文档块/文件在哪些 profile 下生效 |
spring.config.activate.on-cloud-platform |
CloudPlatform |
空 | 该文档块在哪个云平台下生效 |
ConfigDataLocation 语法:
ini
[optional:]<prefix>:<path>
多个位置用 ; 分隔(optional: 只作用于单片段)
目录必须以 / 结尾;文件必须有可识别的扩展名
方括号可给扩展名提示:myfile[.properties]
B.2 spring.profiles.*(第 07 章第 5 节)
| 属性 | 说明 | 限制 |
|---|---|---|
spring.profiles.active |
激活的 profile | 不能写在 profile-specific 文件里 |
spring.profiles.default |
默认 profile | 不能写在 profile-specific 文件里 |
spring.profiles.include |
额外包含的 profile | 不能写在 profile-specific 文件里 |
spring.profiles.group.<name> |
profile 组,激活 name 时展开成一组 |
支持循环定义(Set.add 防无限展开) |
spring.profiles(无后缀)已被移除 ------ 改用 spring.config.activate.on-profile(InvalidConfigDataPropertyException.ERRORS 里有明确映射)。
B.3 spring.main.*(第 05 章)
| 属性 | 默认 | 说明 |
|---|---|---|
spring.main.web-application-type |
自动推断 | 唯一跨越绑定点的属性(会触发 Environment 替换) |
spring.main.banner-mode |
结构化日志下 OFF,否则 CONSOLE |
只写属性(getter 带参) |
spring.main.lazy-initialization |
false |
|
spring.main.allow-bean-definition-overriding |
false |
2.1 起改 |
spring.main.allow-circular-references |
false |
2.6 起改 |
spring.main.keep-alive |
false |
无非守护线程时保持 JVM 存活 |
spring.main.log-startup-info |
true |
|
spring.main.register-shutdown-hook |
true |
关闭超时 10 分钟 |
spring.main.sources |
空 | Set<String>,AOT 下失效 |
spring.main.cloud-platform |
空 | 幽灵属性 (元数据有,类里没有);必须写 CLOUD_FOUNDRY 而非 cloud-foundry |
spring.main.show-banner |
--- | 已弃用(since 1.3.0),用 banner-mode |
spring.main.web-environment |
--- | 已弃用 |
spring.main.environment-prefix 不能用属性设置 (Assert.state 禁止)------只能编程式 setEnvironmentPrefix(...)。
B.4 @ConfigurationProperties 注解属性(本章)
| 属性 | 默认 | 对应机制 |
|---|---|---|
prefix |
"" |
绑定前缀 |
value |
--- | prefix 的别名 |
ignoreUnknownFields |
true |
false → 加 NoUnboundElementsBindHandler |
ignoreInvalidFields |
false |
true → 加 IgnoreErrorsBindHandler |
注意 :这两个 ignoreXxx 是独立 的开关,可任意组合。但 ignoreInvalidFields=true + ignoreUnknownFields=false 会产生反直觉的"未绑定"报告(第 24.2 节有完整源码证据)。
B.5 相关注解速查
| 注解 | 位置 | 作用 |
|---|---|---|
@ConfigurationProperties |
类 / @Bean 方法 |
声明配置属性 |
@EnableConfigurationProperties |
配置类 | 注册(列出类或配置类) |
@ConfigurationPropertiesScan |
配置类 | 扫描注册(自带 @EnableConfigurationProperties) |
@ConfigurationPropertiesSource |
类型 | 仅元数据提示,对绑定无影响 |
@NestedConfigurationProperty |
字段 | 标记嵌套对象(主要影响元数据生成) |
@DeprecatedConfigurationProperty |
字段/setter | 元数据里标记弃用 |
@ConstructorBinding |
构造器 | 多构造器时指定用哪个 |
@DefaultValue |
构造参数 | 构造器绑定的默认值 |
@Validated |
类 | 校验的开关 |
@DurationUnit / @DataSizeUnit |
字段 | 注解驱动的单位转换(通过 TypeDescriptor 传递) |
@ConfigurationPropertiesBinding |
Converter Bean |
限定该转换器只用于配置绑定 |
B.6 五种常见组合模式
| 场景 | 推荐写法 |
|---|---|
| 可变配置类(需要 setter 语义) | 无参构造器 + setter + @EnableConfigurationProperties |
| 不可变配置类(推荐) | record + @DefaultValue + @ConfigurationPropertiesScan |
| 给第三方类型配置化 | @Bean 方法 + 方法上的 @ConfigurationProperties |
| 带校验 | 上述任一 + @Validated + 字段上的 JSR-303 注解 + classpath 有 validation |
| 跨模块嵌套配置 | 嵌套类型加 @ConfigurationPropertiesSource + 该模块启用注解处理器 |
一份"现代写法"模板
java
// 配置类(不可变 + 默认值 + 校验)
@ConfigurationProperties(prefix = "my.service")
@Validated
public record ServiceProperties(
@NotNull String endpoint,
@DefaultValue("30s") Duration timeout,
@DefaultValue("3") int maxRetries) {
public ServiceProperties {
Assert.hasText(endpoint, "endpoint must not be blank");
Assert.isTrue(maxRetries >= 0, "maxRetries must be >= 0");
}
}
// 注册
@SpringBootApplication
@ConfigurationPropertiesScan
public class App { }
// 使用
@Service
public class MyService {
private final ServiceProperties props;
MyService(ServiceProperties props) { this.props = props; }
}
这个模板同时用到了本章的六个机制:
@ConfigurationPropertiesScan扫描注册(自带@EnableConfigurationProperties)record→deduceBindMethod推断为VALUE_OBJECT- 构造器绑定(不走
BeanPostProcessor,BeanDefinition 层面准备值) @DefaultValue提供默认值@Validated+@NotNull校验(两条件:注解 + classpath)- 紧凑构造器做额外的不变式检查
附录结束。第 06-09 章构成了 Spring Boot 配置体系的完整图景。从第 10 章开始进入自动配置与条件注解------那是 Boot "约定优于配置" 的技术核心。