在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 { }@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一起使用。
实现原理
该注解使用@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); } }