Nacos为Map和List注入数据

最近在开发过程中遇到了一个棘手的问题:有一些配置信息,需要以键值对或者列表的形式从配置文件中注入到代码里,配置中心使用的是Nacos。在正常情况下,使用Nacos我们都是key-value的配置,那么我们要是想将配置注入到List或者Map中,应该怎么办呢?

我们在使用spring本地配置的时候,可以使用@ConfigurationProperties注解将一个文件中的所有内容注入到类的各个对应属性上,在Nacos中,也有类似的注解@NacosConfigurationProperties,我们看下该类的解析工具com.alibaba.nacos.spring.context.properties.config.NacosConfigurationPropertiesBindingPostProcessor,该类实现了BeanPostProcessor接口。

拓展一下

它是SpringIOC容器给我们提供的一个扩展接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface BeanPostProcessor {

@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

}

BeanPostProcessor中有两个方法,当实现了该接口的类注册到SpringIOC容器后,在类实例初始化方法(afterProperties和自定义init等方法)调用之前,将会执行BeanPostProcessor#postProcessBeforeInitialization方法,在实例初始化方法执行完之后,会调用BeanPostProcessor#postProcessAfterInitialization方法,整个的调用顺序为:

image-20201029004452340

回归正题

首先看一下NacosConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization方法的代码:

1
2
3
4
5
6
7
8
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

NacosConfigurationProperties nacosConfigurationProperties = findAnnotation(bean.getClass(), NacosConfigurationProperties.class);
if (nacosConfigurationProperties != null) {
bind(bean, beanName, nacosConfigurationProperties);
}
return bean;
}

可以看到在每一个bean被初始化之前,都会获取一下该类是否被@NacosConfigurationProperties所装饰,如果被装饰了则执行bind方法,为了省篇幅,我们直接看到最底层的bind方法的代码,在类NacosBootConfigurationPropertiesBinder#doBind方法中。

为什么在这个类里呢?

NacosConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization方法中跟进代码,是进入到了NacosConfigurationPropertiesBinder#doBind方法啊,这里就要看下NacosBootConfigurationPropertiesBinder类的定义了:

1
2
public class NacosBootConfigurationPropertiesBinder
extends NacosConfigurationPropertiesBinder {}

该类是NacosConfigurationPropertiesBinder的子类,那么这个类是在哪里被注册到IOC容器里的呢?从代码中我们跟踪到NacosConfigBootBeanDefinitionRegistrar类中,该类使用注解@Configuration通知SpringIOC主动的将其加载到容器中,看一下该类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class NacosConfigBootBeanDefinitionRegistrar
implements ImportBeanDefinitionRegistrar, BeanFactoryAware {

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
// 步骤1
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
.rootBeanDefinition(NacosBootConfigurationPropertiesBinder.class);
// 步骤2
defaultListableBeanFactory.registerBeanDefinition(
NacosBootConfigurationPropertiesBinder.BEAN_NAME, beanDefinitionBuilder.getBeanDefinition());
}

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {}
}

类实现了BeanFactoryAwareImportBeanDefinitionRegistrar两个接口,这两个接口是干什么的呢?熟悉Spring的同学应该都狠清楚,这里简单的讲一下:BeanFactoryAware是一个可以将BeanFactory实例注入到当前类的一个接口,在类中可以通过实现BeanFactoryAware#setBeanFactory方法来注入BeanFactory实例,在Nacos中,通过在setBeanFactory方法中将NacosBootConfigurationPropertiesBinder类注入到IOC容器中;而ImportBeanDefinitionRegistrar接口是给Bean的注册提供了更加灵活的方式,实现了该接口的Bean并不是直接注册到IOC容器中,而是可以通过@Import注解动态的选择性的注入到容器中,让Bean的注册更加的灵活方便。

又说多了,现在我们来看NacosBootConfigurationPropertiesBinder中的doBind方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void doBind(Object bean, String beanName, String dataId, String groupId,
String configType, NacosConfigurationProperties properties, String content, ConfigService configService) {
String name = "nacos-bootstrap-" + beanName;
NacosPropertySource propertySource = new NacosPropertySource(name, dataId, groupId, content, configType);
environment.getPropertySources().addLast(propertySource);
Binder binder = Binder.get(environment);
ResolvableType type = getBeanType(bean, beanName);
Bindable<?> target = Bindable.of(type).withExistingValue(bean);
// important
binder.bind(properties.prefix(), target);
publishBoundEvent(bean, beanName, dataId, groupId, properties, content, configService);
publishMetadataEvent(bean, beanName, dataId, groupId, properties);
environment.getPropertySources().remove(name);
}

最关键的就在于binder.bind(properties.prefix(), target)方法,我们从debug到binder中,最终找到了bindObject方法,方法中有AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);代码,目的是什么呢?来看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context context) {
Class<?> resolvedType = target.getType().resolve(Object.class);
if (Map.class.isAssignableFrom(resolvedType)) {
return new MapBinder(context);
}
if (Collection.class.isAssignableFrom(resolvedType)) {
return new CollectionBinder(context);
}
if (target.getType().isArray()) {
return new ArrayBinder(context);
}
return null;
}

这里就很明显了,找出被注入熟悉的类型,然后迭代注入,所以我们的配置文件内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
error-code:
code-map:
10001001: 问题标题必填
10001002: 问题ID必填

user-name:
- 1
- 2
- 3
- 4
- 5
- 6
- 7

被注入类:

1
2
3
4
5
6
7
8
@Configuration
@NacosConfigurationProperties(prefix = "error-code", dataId = "starter_error", type = ConfigType.YAML)
public class ArgsErrorCode {

private Map<String, String> codeMap;

private List<String> userName;
}

done!