Configuration file@Configuration Properties reads List, Map parameters

Keywords: Programming Java Spring SpringBoot xml

background

In the SpringBoot environment, we have "incomplete" annotations.This is why SpringBoot replaces the xml configuration in traditional Spring projects.When using these annotations, we must understand the principles and conventions behind these annotations.

package org.springframework.boot.context.properties;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationProperties {
  ......
}

Supported Types

List
custom.config.config1.folders[0]=/root
custom.config.config1.folders[1]=/home/user1
custom.config.config1.folders[2]=/home/user2

Corresponding Java implementation

@ConfigurationProperties(prefix = "custom.config.config1")
public class Config1Properties{
	private List<String> folders;
	...
}
Map
custom.config.config1.map.key1=value1
custom.config.config1.map.key2=value2
custom.config.config1.map.key3=value3
custom.config.config1.map.key4=value4
custom.config.config1.map.key5=value5

Corresponding Java implementation

@ConfigurationProperties(prefix = "custom.config.config1")
public class Config1Properties{
	private Map<String, String> map;
	...
}
Object
custom.config.config1.server.host=host1
custom.config.config1.server.port=22
custom.config.config1.server.username=username1
custom.config.config1.server.password=password1

Corresponding Java implementation

@ConfigurationProperties(prefix = "custom.config.config1")
public class Config1Properties{
	private ServerProperties server;
	...
	public static class ServerProperties {
		private String host;
		private int port;
		private String username;
		private String password;
		...
	}
}
Object List
custom.config.config1.servers[0].host=host1
custom.config.config1.servers[0].port=22
custom.config.config1.servers[0].username=username1
custom.config.config1.servers[0].password=password1
custom.config.config1.servers[1].host=host2
custom.config.config1.servers[1].port=22
custom.config.config1.servers[1].username=username2
custom.config.config1.servers[1].password=password2

Corresponding Java implementation

@ConfigurationProperties(prefix = "custom.config.config1")
public class Config1Properties{
	private List<ServerProperties> servers;
	...
	public static class ServerProperties {
		private String host;
		private int port;
		private String username;
		private String password;
		...
	}
}

Use cases for Map

For example, if we need to connect multiple OSS (Ali Object Store) at the same time, then we can configure multiple in the way of Configuration Properties.It can also be dynamically injected into containers by Spring's loading.

Configuration center:

# OSS1 Configuration
oss.multi.clients.accout.accessKeyId=xxx
oss.multi.clients.accout.accessKeySecret=xxx
oss.multi.clients.accout.privateEndpoint=xxx
oss.multi.clients.accout.bucketName=bucket-b-test

# OSS2 Configuration
oss.multi.enabled=true
oss.multi.clients.xdtrans.accessKeyId=xxx
oss.multi.clients.xdtrans.accessKeySecret=xxx
oss.multi.clients.xdtrans.privateEndpoint=xxx
oss.multi.clients.xdtrans.bucketName=bucket-a-test

Corresponding Java implementation

@Data
@EqualsAndHashCode(callSuper = false)
@ConfigurationProperties(prefix = OssConstants.MULTI_CONFIG_PREFIX)
public class MultiOssProperties {
	private Map<String, OssProperties> clients;

	@Data
	public static class OssProperties {
		private String accessKeyId;
		private String accessKeySecret;
		private String publicEndpoint;
		private String privateEndpoint;
		private String bucketName;
		private String object;
	}

Dynamically define the BeanDefinition we need.

public class MultiOssScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {

	private ApplicationContext applicationContext;

	@Setter
	private MultiOssProperties multiOssProperties;

	@Override
	public void setBeanName(String name) {
		log.info("init bean {}", name);
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		Objects.requireNonNull(this.multiOssProperties, "multiOssProperties Cannot be empty");
		Objects.requireNonNull(this.applicationContext, "applicationContext Cannot be empty");
	}

  // Dynamic Definition Bean
	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
		String beanSuffixName = StringUtils.capitalize(OssConstants.BEAN_SUFFIX_NAME);
    // The productCodes actually match the xdtrans of oss.multi.clients.xdtrans
		multiOssProperties.getClients().forEach((productCode, ossProperties) -> {
			AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(OssClient.class,
					() -> OssClientUtils.buildOssClient(ossProperties))
					.getRawBeanDefinition();
			beanDefinition.setInitMethodName("init");
			beanDefinition.setDestroyMethodName("shutDown");
			beanDefinitionRegistry.registerBeanDefinition(productCode + beanSuffixName, beanDefinition);
		});
	}

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

}

binder to relate the configuration to the corresponding Java code:

@Configuration
@EnableConfigurationProperties(MultiOssProperties.class)
@ConditionalOnProperty(prefix = OssConstants.MULTI_CONFIG_PREFIX, value = "enabled")
public class MultiOssAutoConfiguration {

	/**
	 * Initialize multiple ossClient autoconfigurations
	 *
	 * @param environment Environment variable properties
	 * @return OssClient Automatic Scan Registrar
	 */
	@Bean
	public MultiOssScannerConfigurer multiOssScannerConfigurer(Environment environment) {
		Binder binder = Binder.get(environment);
		MultiOssProperties properties = binder.bind(OssConstants.MULTI_CONFIG_PREFIX, MultiOssProperties.class).get();
		MultiOssScannerConfigurer multiOssScannerConfigurer = new MultiOssScannerConfigurer();
		multiOssScannerConfigurer.setMultiOssProperties(properties);
		return multiOssScannerConfigurer;
	}
}

How to use it?

@Getter
@AllArgsConstructor
public enum OssTypeEnum {
		// Notice the beanName here to be consistent with the postProcessBeanDefinitionRegistry above
    XDtransOssClient("xdtransOssClient", "oss1"),
    DianDianOssClient("ddacctOssClient", "oss2"),
    ;

    private final String beanName;
    private final String desc;

    // Get it in the Spring container from BeanName
    public OssClient getBean() {
        return SpringContextHolder.getBean(beanName, OssClient.class);
    }

How does Binder map?

Bid with the code binder.bind above (OssConstants.MULTI_CONFIG_PREFIX, MultiOssProperties.class). get(); and

protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context, boolean allowRecursiveBinding) {
		context.clearConfigurationProperty();
		try {
			target = handler.onStart(name, target, context);
			if (target == null) {
				return null;
			}
			Object bound = bindObject(name, target, handler, context,allowRecursiveBinding);
			return handleBindResult(name, target, handler, context, bound);
		} catch (Exception ex) {
			return handleBindError(name, target, handler, context, ex);
		}
}

If our key is: oss.multi.clients.accout.xxx

It actually corresponds to Map, so its reference name is clients.The key is accout, and the corresponding value is OssProperties.

private Object bindBean(ConfigurationPropertyName name, Bindable<?> target,
		BindHandler handler, Context context, boolean allowRecursiveBinding) {
	if (containsNoDescendantOf(context.getSources(), name)
			|| isUnbindableBean(name, target, context)) {
		return null;
	}
	BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
			name.append(propertyName), propertyTarget, handler, context, false);
	Class<?> type = target.getType().resolve(Object.class);
	if (!allowRecursiveBinding && context.hasBoundBean(type)) {
		return null;
	}
	return context.withBean(type, () -> {
		Stream<?> boundBeans = BEAN_BINDERS.stream()
				.map((b) -> b.bind(name, target, context, propertyBinder));
		return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
	});
}

A specific bind case.

private static final List<BeanBinder> BEAN_BINDERS;

static {
	List<BeanBinder> binders = new ArrayList<>();
	binders.add(new JavaBeanBinder());
	BEAN_BINDERS = Collections.unmodifiableList(binders);
}

public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
		BeanPropertyBinder propertyBinder) {
	boolean hasKnownBindableProperties = hasKnownBindableProperties(name, context);
	Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
	if (bean == null) {
		return null;
	}
	BeanSupplier<T> beanSupplier = bean.getSupplier(target);
	boolean bound = bind(propertyBinder, bean, beanSupplier);
	return (bound ? beanSupplier.get() : null);
}
// Return the corresponding object
public BeanSupplier<T> getSupplier(Bindable<T> target) {
	return new BeanSupplier<>(() -> {
		T instance = null;
		if (target.getValue() != null) {
			instance = target.getValue().get();
		}
		if (instance == null) {
			instance = (T) BeanUtils.instantiateClass(this.resolvedType);
		}
		return instance;
	});
}

Reference Address

If you like my article, you can focus on your personal subscription number.Welcome to leave a message and exchange at any time.If you want to join the WeChat group and discuss it together, add lastpass4u, the administrator's culture assistant, who will pull you into the group.

Posted by gtal3x on Wed, 06 May 2020 09:32:15 -0700