在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
@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;
}
除了使用方式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
不会向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
主要有两个作用
注册后置处理器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);
}
}
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);
}
要想使用SpringBoot中的(注解)属性绑定功能,是一定要开启@EnableConfigurationProperties注解,但是SpringBoot中已经默认开启了该注解功能,并且很多配置类,开启了该注解功能,因此不需要开发者自己显示编码开启。
开启该注解,在向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.ConfigurationPropertiesBindingPostProcessor#register
public static void register(BeanDefinitionRegistry registry) {
Assert.notNull(registry, "Registry must not be null");
//判断ConfigurationPropertiesBindingPostProcessor是否已经注册
if (!registry.containsBeanDefinition(BEAN_NAME)) {
GenericBeanDefinition definition = new GenericBeanDefinition();
definition.setBeanClass(ConfigurationPropertiesBindingPostProcessor.class);
definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN_NAME, definition);
}
ConfigurationPropertiesBinder.register(registry);
}
避免绑定工具类的重复注册
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);
}
}
在SpringBoot2.2之后,如果想让一个仅有@ConfigurationProperties注解的配置类被注册为bean,可以通过@ConfigurationPropertiesScan
注解开启。则不再需要配合@Component
一起使用。
实现原理
org.springframework.boot.context.properties.ConfigurationPropertiesScanRegistrar
ImportBeanDefinitionRegistrar
接口,Spring在启动过程中会回调该接口的方法.ConfigurationPropertiesScanRegistrar
会通过包扫描,扫描被@ConfigurationProperties
标记的类@ConfigurationProperties
类,排除标有@Component
的类,避免配置类被重复注册,则将其注册为Bean,beanName为prefix+配置类全类名
。@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);
}
}