@EnableConfigurationProperties @ConfigurationProperties @ConfigurationPropertiesScan

前言

在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主要有两个作用

  1. 注册后置处理器ConfigurationPropertiesBindingPostProcessor,用于在Bean被初始化时,给Bean中的属性绑定属性值。这也是为什么第一种方式使用@ConfigurationProperties需要使用@Component注解的原因,否则其不是Bean,无法被Spring处理的后置处理器处理则无法绑定属性值。

  2. 将一个被标记@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.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);
		}
	}

@ConfigurationPropertiesScan 实现原理

在SpringBoot2.2之后,如果想让一个仅有@ConfigurationProperties注解的配置类被注册为bean,可以通过@ConfigurationPropertiesScan注解开启。则不再需要配合@Component一起使用。

实现原理

  1. 该注解使用@Import注解向Spring容器导入org.springframework.boot.context.properties.ConfigurationPropertiesScanRegistrar
  2. 该类实现了ImportBeanDefinitionRegistrar接口,Spring在启动过程中会回调该接口的方法.
  3. ConfigurationPropertiesScanRegistrar会通过包扫描,扫描被@ConfigurationProperties标记的类
  4. 遍历扫描到的标有@ConfigurationProperties类,排除标有@Component的类,避免配置类被重复注册,则将其注册为Bean,beanName为prefix+配置类全类名
  5. 当配置类注册为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);
	}

}

你可能感兴趣的:(spring,spring,boot)