Springboot annotation @ EnableConfigurationProperties, @ ConfigurationProperties, @ ConfigurationPropertiesScan differences

Posted by amal.barman on Mon, 21 Feb 2022 15:30:49 +0100

preface

In the SpringBoot project, we often need to bind some configuration items with specific prefixes to a configuration class. At this time, we can use @ EnableConfigurationProperties and @ ConfigurationProperties annotations to implement. In SpringBoot 2 The @ ConfigurationPropertiesScan annotation is also added in 2.0 to help us simplify the registration of configuration classes as a Bean. The following mainly explains the use and source code implementation of these three annotations.

Usage example: bind configuration item to configuration class

There are the following configuration items. We use @ ConfigurationProperties and @ EnableConfigurationProperties annotation methods to bind them to the configuration class, and these configuration classes will actually be registered as beans

#Bind to configuration class 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

#Bind to configuration item class 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

Method 1: use @ ConfigurationProperties

@In fact, the ConfigurationProperties annotation only specifies the prefix corresponding to the property in the Configuration class. When a Configuration class is only marked by @ ConfigurationProperties, the value of the Configuration item will not be bound to its property or registered as a Bean. You need to use @ Component annotation or @ component subclass annotation (for example, @ Configuration).
Example: configuration class com example. demo. config. ShardingProperties

@Component
@ConfigurationProperties(prefix = "sharding")
public class ShardingProperties {
    private Integer defaultDSIndex;
    private String column;
    private List<MyDataSourceProperties> dataSources;
    //Ignore other fields and getter/setter methods
}

public class MyDataSourceProperties {
    private String name;
    private String driverClassName;
    private String jdbcUrl;
    private String username;
    private String password;
    private Long connectionTimeout;
}

Method 2: use @ EnableConfigurationProperties

In addition to using mode 1, you can also bind property values by specifying specific configuration classes through @ EnableConfigurationProperties(value={xxx.calss}).

Example: configuration class com example. demo. config. MyBatisProperties

@ConfigurationProperties(prefix = "mybatis")
public class MyBatisProperties {
    private String basePackage;
    private String mapperLocations;
    private String typeAliasesPackage;
    private String markerInterface;
    //Ignore other fields and getter/setter methods
}

@EnableConfigurationProperties({MyBatisProperties.class})
@Configuration
public class EnableMyBatisConfig {

}

Use the values in the configuration class

/** 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() {
        //Print the values mapped from the configuration file in the configuration class
        System.out.println(JsonUtil.toJson(shardingProperties));
        System.out.println(JsonUtil.toJson(myBatisProperties));
    }
}

@ConfigurationProperties function

@ConfigurationProperties will not inject relevant processing classes into the Spring container, but only play the role of relevant tags. The relevant processing logic is completed by the processing class imported by @ EnableConfigurationProperties. Classes that are only annotated with @ ConfigurationProperties will not be registered as beans by default

public @interface ConfigurationProperties {
    //Equivalent to prefix, specifies the prefix of attribute binding
	@AliasFor("prefix")
	String value() default "";

	@AliasFor("value")
	String prefix() default "";

	//Whether to ignore the exception when an error occurs when the attribute value is bound to a field. It is not ignored by default, and an exception will be thrown
	boolean ignoreInvalidFields() default false;

	//When the configuration item is bound to the attribute in the entity class, the corresponding field is not found. Whether to ignore it. It is ignored by default and no exception is thrown.
	//If ignoreInvalidFields = true, ignoreUnknownFields = false will no longer take effect. It may be a bug in SpringBoot
	boolean ignoreUnknownFields() default true;
}

@Implementation principle of EnableConfigurationProperties

@EnableConfigurationProperties have two main functions

  1. Register the post processor ConfigurationPropertiesBindingPostProcessor, which is used to bind property values to the properties in the Bean when the Bean is initialized. This is why the first way to use @ ConfigurationProperties requires @ Component annotation. Otherwise, it is not a Bean and cannot be processed by the post processor of Spring, so the property value cannot be bound.
  2. Register a configuration class marked with @ ConfigurationProperties as a Bean of Spring. Classes not marked with @ ConfigurationProperties annotation cannot be used as parameters of @ EnableConfigurationProperties, otherwise an exception will be thrown. Using @ ConfigurationProperties alone will not register this class as a Bean
class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		registerInfrastructureBeans(registry);
		ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
		//Get the configuration class specified by the @ EnableConfigurationProperties annotation parameter and register it as a Bean
		//beanName is "prefix + configuration class full class name".
		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());
	}

	//Register the relevant post processor and Bean for binding
	static void registerInfrastructureBeans(BeanDefinitionRegistry registry) {
		ConfigurationPropertiesBindingPostProcessor.register(registry);
		BoundConfigurationProperties.register(registry);
		ConfigurationPropertiesBeanDefinitionValidator.register(registry);
		ConfigurationBeanFactoryMetadata.register(registry);
	}
}

Which beans are registered for property binding

ConfigurationPropertiesBinder.Factory
It is mainly used to create an instance of ConfigurationPropertiesBinder object

ConfigurationPropertiesBinder
ConfigurationPropertiesBinder is equivalent to a tool class for property binding between configuration items and configuration classes

ConfigurationPropertiesBindingPostProcessor
When the bean is initialized, it will go through the post processor to find out whether the class or Menthd in the class is marked with @ ConfigurationProperties. If it exists, it will call ConfigurationPropertiesBinder to bind the properties of the 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);
	}

Why can property binding method 1 in the example succeed without enabling @ EnableConfigurationProperties?

If you want to use the (annotation) property binding function in SpringBoot, you must turn on the @ EnableConfigurationProperties annotation. However, the annotation function has been turned on by default in SpringBoot, and many configuration classes turn on the annotation function. Therefore, developers do not need to display the code on themselves.

Will the use of @ EnableConfigurationProperties in multiple places in the project lead to repeated registration of imported bean s?

Open this annotation. When registering the post-processing of attribute binding in Spring, you will first judge whether it has been registered to avoid registering the same Bean repeatedly
Avoid duplicate registration of configuration classes springframework. boot. context. properties. EnableConfigurationPropertiesImportSelector. ConfigurationPropertiesBeanRegistrar

public static class ConfigurationPropertiesBeanRegistrar
			implements ImportBeanDefinitionRegistrar {

		@Override
		public void registerBeanDefinitions(AnnotationMetadata metadata,
				BeanDefinitionRegistry registry) {
		    //Register configuration class
			getTypes(metadata).forEach((type) -> register(registry,
					(ConfigurableListableBeanFactory) registry, type));
		}
        //Find configuration class on annotation
		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());
		}
        //Register configuration class
		private void register(BeanDefinitionRegistry registry,
				ConfigurableListableBeanFactory beanFactory, Class<?> type) {
			String name = getName(type);
			//Avoid duplicate annotations for configuration classes
			if (!containsBeanDefinition(beanFactory, name)) {
				registerBeanDefinition(registry, name, type);
			}
		}
		//......
	}

Avoid duplicate registration of post processor
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#register

public static void register(BeanDefinitionRegistry registry) {
		Assert.notNull(registry, "Registry must not be null");
		//Determine whether the ConfigurationPropertiesBindingPostProcessor has been registered
		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);
	}

Avoid duplicate registration of binding tool classes
org.springframework.boot.context.properties.ConfigurationPropertiesBinder#register

static void register(BeanDefinitionRegistry registry) {
        //Judge configurationpropertiesbinder Whether the factory has been registered,
		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);
		}
		//Judge whether the ConfigurationPropertiesBinder has been registered,
		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);
		}
	}

@Implementation principle of ConfigurationPropertiesScan

In springboot 2 After 2, if you want a configuration class with only @ ConfigurationProperties annotation to be registered as a bean, you can open it through @ ConfigurationProperties scan annotation. You no longer need to use it with @ Component.

Implementation principle

  1. This annotation uses the @ Import annotation to Import org. Org into the Spring container springframework. boot. context. properties. ConfigurationPropertiesScanRegistrar
  2. This class implements the ImportBeanDefinitionRegistrar interface. Spring will call back the method of this interface during startup.
  3. ConfigurationPropertiesScanRegistrar will scan the classes marked by @ ConfigurationProperties through package scanning
  4. Traverse the scanned class marked with @ ConfigurationProperties and exclude the class marked with @ Component to avoid repeated registration of the configuration class. Then register it as Bean and beanName as the full class name of prefix + configuration class.
  5. After the configuration class is registered as a bean, the post processor of @ EnableConfigurationProperties can bind its properties
class ConfigurationPropertiesScanRegistrar implements ImportBeanDefinitionRegistrar {
    //Some code ignored
	
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	    //Get the package scanning range, and scan the packages and sub packages of the class where @ ConfigurationPropertiesScan is located by default
		Set<String> packagesToScan = getPackagesToScan(importingClassMetadata);
		//Perform package scanning and scan only the classes marked by @ ConfigurationProperties
		scan(registry, packagesToScan);
	}

	private void register(ConfigurationPropertiesBeanRegistrar registrar, Class<?> type) {       
	    //If the scanned class is marked with @ Component annotation, it will not be registered. Otherwise, the registration will be repeated. However, due to the inaccessibility of beanName, the registration will be repeated
		if (!isComponent(type)) {
		    //Register the bean. The name of the bean is the full class name of prefix + configuration class
			registrar.register(type);
		}
	}
    
	private boolean isComponent(Class<?> type) {
		return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).isPresent(Component.class);
	}

}

 

Reprinted from: @EnableConfigurationProperties @ConfigurationProperties @ConfigurationPropertiesScan_Brucelwl's blog - CSDN blog

Topics: Spring Boot Configuration properties