前言
在SpringBoot工程中,我們常常需要將一些特定前綴的配置項綁定到一個配置類上。這時候我們就可以使用@EnableConfigurationProperties、@ConfigurationProperties註解來實現。在SpringBoot2.2.0中還添加@ConfigurationPropertiesScan註解來幫助我們簡化將配置類註冊成一個Bean。下面主要講解這三個註解的使用和源碼實現。
使用示例: 將配置項綁定到一個配置類
有如下配置項,我們分別採用@ConfigurationProperties和@EnableConfigurationProperties兩種註解方式,將其綁定到配置類上,並且這些配置類其實還會被註冊成Bean
#綁定到配置類 com.example.demo.config.MyBatisProperties
mybatis.basePackage= com.example.web.mapper
mybatis.mapperLocations= classpath*:mapper/*.xml
mybatis.typeAliasesPackage= com.example.web.model
mybatis.defaultStatementTimeoutInSecond= 5
mybatis.mapUnderscoreToCamelCase= false
#綁定到配置項類 com.example.demo.config.ShardingProperties
sharding.defaultDSIndex= 0
sharding.dataSources[0].driverClassName= com.mysql.jdbc.Driver
sharding.dataSources[0].jdbcUrl= jdbc:mysql://localhost:3306/lwl_db0?useSSL=false&characterEncoding=utf8
sharding.dataSources[0].username= root
sharding.dataSources[0].password= 123456
sharding.dataSources[0].readOnly= false
方式1、使用@ConfigurationProperties
@ConfigurationProperties註解其實只是指定了配置類中屬性所對應的前綴,當一個配置類僅僅被@ConfigurationProperties標記時,配置項的值是不會被綁定其屬性的,也不會將其註冊爲Bean,需要同時使用@Component註解或是@Component子類註解(例如@Configuration)。
示例:配置類 com.example.demo.config.ShardingProperties
@Component
@ConfigurationProperties(prefix = "sharding")
public class ShardingProperties {
private Integer defaultDSIndex;
private String column;
private List<MyDataSourceProperties> dataSources;
//忽略其他字段和getter/setter方法
}
public class MyDataSourceProperties {
private String name;
private String driverClassName;
private String jdbcUrl;
private String username;
private String password;
private Long connectionTimeout;
}
方式2、使用@EnableConfigurationProperties
除了使用方式1,還可以通過@EnableConfigurationProperties(value={xxx.calss})指定具體的配置類來綁定屬性值。
示例:配置類 com.example.demo.config.MyBatisProperties
@ConfigurationProperties(prefix = "mybatis")
public class MyBatisProperties {
private String basePackage;
private String mapperLocations;
private String typeAliasesPackage;
private String markerInterface;
//忽略其他字段和getter/setter方法
}
@EnableConfigurationProperties({MyBatisProperties.class})
@Configuration
public class EnableMyBatisConfig {
}
使用配置類中的值
/** Created by bruce on 2019/6/15 00:20 */
@Component
public class BinderConfig {
private static final Logger logger = LoggerFactory.getLogger(BinderConfig.class);
@Autowired
private ShardingProperties shardingProperties;
@Autowired
private MyBatisProperties myBatisProperties;
@PostConstruct
public void binderTest() {
//打印配置類中從配置文件中映射的值
System.out.println(JsonUtil.toJson(shardingProperties));
System.out.println(JsonUtil.toJson(myBatisProperties));
}
}
@ConfigurationProperties作用
@ConfigurationProperties不會向Spring容器注入相關處理類,只起到相關標記作用,相關處理邏輯由@EnableConfigurationProperties導入的處理類來完成。僅僅被標記@ConfigurationProperties註解的類,默認情況下也不會註冊爲Bean
public @interface ConfigurationProperties {
//等同於prefix,指定屬性綁定的前綴
@AliasFor("prefix")
String value() default "";
@AliasFor("value")
String prefix() default "";
//當屬性值綁定到字段,發生錯誤時,是否忽略異常。默認不忽略,會拋出異常
boolean ignoreInvalidFields() default false;
//當配置項向實體類中的屬性綁定時,沒有找到對應的字段,是否忽略。默認忽略,不拋出異常。
//如果ignoreInvalidFields = true 則 ignoreUnknownFields = false不再生效,可能是SpringBoot的bug
boolean ignoreUnknownFields() default true;
}
@EnableConfigurationProperties實現原理
@EnableConfigurationProperties主要有兩個作用
註冊後置處理器ConfigurationPropertiesBindingPostProcessor,用於在Bean被初始化時,給Bean中的屬性綁定屬性值。這也是爲什麼第一種方式使用@ConfigurationProperties需要使用@Component註解的原因,否則其不是Bean,無法被Spring處理的後置處理器處理則無法綁定屬性值。
將一個被標記@ConfigurationProperties的配置類註冊爲Spring的一個Bean,沒有被標記@ConfigurationProperties註解的類不能做爲@EnableConfigurationProperties的參數,否則拋出異常。僅僅使用@ConfigurationProperties也不會將這個類註冊爲一個Bean
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerInfrastructureBeans(registry);
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
//獲取@EnableConfigurationProperties註解參數指定的配置類,並將其註冊成Bean
//beanName爲 " prefix+配置類全類名"。
getTypes(metadata).forEach(beanRegistrar::register);
}
private Set<Class<?>> getTypes(AnnotationMetadata metadata) {
return metadata.getAnnotations().stream(EnableConfigurationProperties.class)
.flatMap((annotation) -> Arrays.stream(annotation.getClassArray(MergedAnnotation.VALUE)))
.filter((type) -> void.class != type).collect(Collectors.toSet());
}
//註冊相關後置處理器和Bean用於註定綁定
static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
ConfigurationPropertiesBindingPostProcessor.register(registry);
BoundConfigurationProperties.register(registry);
ConfigurationPropertiesBeanDefinitionValidator.register(registry);
ConfigurationBeanFactoryMetadata.register(registry);
}
}
註冊了哪些Bean用於屬性綁定
ConfigurationPropertiesBinder.Factory
主要用於創建ConfigurationPropertiesBinder對象實例
ConfigurationPropertiesBinder
ConfigurationPropertiesBinder相當於是一個工具類,用於配置項到配置類之間的屬性綁定
ConfigurationPropertiesBindingPostProcessor
當bean初始化時,會經過該後置處理器,會查找該類或類中的Menthd是否標記@ConfigurationProperties,如果存在則調用ConfigurationPropertiesBinder給bean進行屬性綁定。
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
return bean;
}
org.springframework.boot.context.properties.ConfigurationPropertiesBean#get(applicationContext, bean, beanName)
public static ConfigurationPropertiesBean get(ApplicationContext applicationContext, Object bean, String beanName) {
Method factoryMethod = findFactoryMethod(applicationContext, beanName);
return create(beanName, bean, bean.getClass(), factoryMethod);
}
private static ConfigurationPropertiesBean create(String name, Object instance, Class<?> type, Method factory) {
ConfigurationProperties annotation = findAnnotation(instance, type, factory, ConfigurationProperties.class);
if (annotation == null) {
return null;
}
Validated validated = findAnnotation(instance, type, factory, Validated.class);
Annotation[] annotations = (validated != null) ? new Annotation[] { annotation, validated }
: new Annotation[] { annotation };
ResolvableType bindType = (factory != null) ? ResolvableType.forMethodReturnType(factory)
: ResolvableType.forClass(type);
Bindable<Object> bindTarget = Bindable.of(bindType).withAnnotations(annotations);
if (instance != null) {
bindTarget = bindTarget.withExistingValue(instance);
}
return new ConfigurationPropertiesBean(name, instance, annotation, bindTarget);
}
爲什麼示例中屬性綁定方式1沒有開啓@EnableConfigurationProperties也可以成功
要想使用SpringBoot中的(註解)屬性綁定功能,是一定要開啓@EnableConfigurationProperties註解,但是SpringBoot中已經默認開啓了該註解功能,並且很多配置類,開啓了該註解功能,因此不需要開發者自己顯示編碼開啓。
項目中多處使用@EnableConfigurationProperties會不會導致導入的bean重複註冊
開啓該註解,在向Spring中註冊屬性綁定的後置處理時,會先判斷是否已經註冊了,避免重複註冊相同的Bean
避免配置類的重複註冊
org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesBeanRegistrar
public static class ConfigurationPropertiesBeanRegistrar
implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//註冊配置類
getTypes(metadata).forEach((type) -> register(registry,
(ConfigurableListableBeanFactory) registry, type));
}
//查找註解上的配置類
private List<Class<?>> getTypes(AnnotationMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
return collectClasses((attributes != null) ? attributes.get("value")
: Collections.emptyList());
}
//註冊配置類
private void register(BeanDefinitionRegistry registry,
ConfigurableListableBeanFactory beanFactory, Class<?> type) {
String name = getName(type);
//避免配置類被重複註解
if (!containsBeanDefinition(beanFactory, name)) {
registerBeanDefinition(registry, name, type);
}
}
//......
}
避免綁定工具類的重複註冊
org.springframework.boot.context.properties.ConfigurationPropertiesBinder#register
static void register(BeanDefinitionRegistry registry) {
//判斷ConfigurationPropertiesBinder.Factory是否已經註冊,
if (!registry.containsBeanDefinition(FACTORY_BEAN_NAME)) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationPropertiesBinder.Factory.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(ConfigurationPropertiesBinder.FACTORY_BEAN_NAME, definition);
}
//判斷ConfigurationPropertiesBinder是否已經註冊,
if (!registry.containsBeanDefinition(BEAN_NAME)) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationPropertiesBinder.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
definition.setFactoryBeanName(FACTORY_BEAN_NAME);
definition.setFactoryMethodName("create");
registry.registerBeanDefinition(ConfigurationPropertiesBinder.BEAN_NAME, definition);
}
}
@ConfigurationPropertiesScan 實現原理
在SpringBoot2.2之後,如果想讓一個僅有@ConfigurationProperties註解的配置類被註冊爲bean,可以通過@ConfigurationPropertiesScan註解開啓。則不再需要配合@Component一起使用。
實現原理
該註解使用@Import註解向Spring容器導入org.springframework.boot.context.properties.ConfigurationPropertiesScanRegistrar
該類實現了ImportBeanDefinitionRegistrar接口,Spring在啓動過程中會回調該接口的方法.
ConfigurationPropertiesScanRegistrar會通過包掃描,掃描被@ConfigurationProperties標記的類
遍歷掃描到的標有@ConfigurationProperties類,排除標有@Component的類,避免配置類被重複註冊,則將其註冊爲Bean,beanName爲prefix+配置類全類名。
當配置類註冊爲bean後,@EnableConfigurationProperties註冊的後置處理器則可以對其進行屬性綁定.
class ConfigurationPropertiesScanRegistrar implements ImportBeanDefinitionRegistrar {
//部分代碼忽略...
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//獲取包掃描範圍,默認掃描@ConfigurationPropertiesScan所在類的包和子包
Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
//執行包掃描,只掃描被@ConfigurationProperties標記的類
scan(registry, packagesToScan);
}
private void register(ConfigurationPropertiesBeanRegistrar registrar, Class<?> type) {
//如果被掃描到的類被標記了@Component註解,則不註冊,否則會重複註冊,但是由於beanName不通,會導致重複註冊.
if (!isComponent(type)) {
//註冊bean,bean的名稱爲prefix+配置類全類名
registrar.register(type);
}
}
private boolean isComponent(Class<?> type) {
return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(Component.class);
}
}