Spring 配置文件相關注解介紹

前言

在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);
	}

}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章