最近在开发过程中遇到了一个棘手的问题:有一些配置信息,需要以键值对或者列表的形式从配置文件中注入到代码里,配置中心使用的是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
方法,整个的调用顺序为:
回归正题
首先看一下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; BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder .rootBeanDefinition(NacosBootConfigurationPropertiesBinder.class); defaultListableBeanFactory.registerBeanDefinition( NacosBootConfigurationPropertiesBinder.BEAN_NAME, beanDefinitionBuilder.getBeanDefinition()); }
@Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {} }
|
类实现了BeanFactoryAware
和ImportBeanDefinitionRegistrar
两个接口,这两个接口是干什么的呢?熟悉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); 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!