Spring Boot 通過 @ConfigurationProperties 將外部配置(YAML/Properties/環境變量)自動綁定到強類型 Java Bean,實現「配置即代碼」的優雅體驗。本文從使用示例切入,深入源碼解析 EnableConfigurationProperties、ConfigurationPropertiesBindingPostProcessor、PropertiesConfigurationFactory 等核心組件如何協作完成 Bean 註冊與屬性綁定。
@ConfigurationProperties(prefix = "org.dragonfei.demo")
public class DemoProperties {
private String name;
private String password;
private String test;
// getter/setter/toString 省略
}
org:
dragonfei:
demo:
name: dragonfei
password: password
test: test
@Configuration
@EnableConfigurationProperties(DemoProperties.class)
public class DemoConfiguration {}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = DemoConfiguration.class)
public class DemoPropertiesTest {
@Autowired private DemoProperties properties;
@Test public void test() { System.out.println(properties); }
// 輸出: DemoProperties{name='dragonfei', password='password', test='test'}
}
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)
@Documented @Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
Class<?>[] value() default {};
}
public class EnableConfigurationPropertiesImportSelector implements ImportSelector {
@Override public String[] selectImports(AnnotationMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
Object[] types = attrs == null ? null : (Object[]) attrs.getFirst("value");
if (types == null || types.length == 0) {
return new String[] { ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
}
return new String[] {
ConfigurationPropertiesBeanRegistrar.class.getName(),
ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName()
};
}
}
@ConfigurationProperties 的類註冊為 Bean。// ConfigurationPropertiesBeanRegistrar
@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
List<Class<?>> types = collectClasses(attributes.get("value"));
for (Class<?> type : types) {
String prefix = extractPrefix(type); // 從 @ConfigurationProperties(prefix) 獲取
String name = StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName();
if (!registry.containsBeanDefinition(name)) {
registerBeanDefinition(registry, type, name); // 註冊為單例 Bean
}
}
}
// ConfigurationPropertiesBindingPostProcessor 實現 BeanPostProcessor
@Override public Object postProcessBeforeInitialization(Object bean, String beanName) {
ConfigurationProperties ann = findAnnotation(bean.getClass());
if (ann != null) {
bindProperties(bean, ann); // 核心綁定邏輯
}
return bean;
}
private void bindProperties(Object bean, ConfigurationProperties ann) {
PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<>(bean);
// 1. 確定 PropertySources:優先 ann.locations(),否則使用環境 propertySources
// 2. 設置 Validator、ConversionService、忽略策略等
// 3. 關鍵:factory.bindPropertiesToTarget();
}
Spring 支持 Relaxed Binding,自動匹配多種命名風格:
prefix.propertyName(標準)prefix.property-name(kebab-case)prefix.property_name(snake_case)prefix.PROPERTY_NAME(環境變量風格)核心實現:
// PropertiesConfigurationFactory.getNames()
RelaxedNames relaxedNames = RelaxedNames.forCamelCase(propertyName);
for (String prefix : prefixes) {
for (String relaxed : relaxedNames) {
names.add(prefix + "." + relaxed);
names.add(prefix + "_" + relaxed);
}
}
// 最後從 PropertySources 中按 names 查找值,再由 DataBinder 完成類型轉換注入
prefix.nested.field 自動實例化嵌套 Bean。prefix.list[0].field、prefix.map.key。10s→Duration、10MB→DataSize)。@Validated + JSR-303 註解(@NotNull、@Max 等)。application-dev.yml、application-prod.yml 自動激活。@ConstructorBinding(Spring Boot 2.2+)實現不可變配置對象。@ConfigurationPropertiesScan 自動掃描,減少手動註冊。@TestPropertySource 或 ConfigDataApplicationContextInitializer 載入測試配置。