记一次解决Nacos-SpringBoot不支持@ConditionalOnProperty

错误介绍

@ConditionalOnProperty配置生效问题

错误原因分析

在使用Nacos-Config-SpringBoot时,有使用者反馈说无法支持@ConditionalOnProperty注解,经过研究Nacos-Config-SpringBootNacos-Spring-Context源码以及调试跟踪SpringBoot的初始化流程,最终发现问题所在——@ConditionalOnProperty的解析时间与Nacos-Spring-Context相关Bean的注册以及工作时间存在先后问题(其本质原因就是Bean的加载顺序)

@ConditionalOnProperty解析时间

要想知道@ConditionalOnProperty注解何时被Spring解析,首先要看另外一个类——ConfigurationClassPostProcessor,这个类实现了BeanFactoryPostProcessor接口,该接口是在所有的BeanDefinition加载之后执行的,可以在此接口中对bean的定义进行修改。即Spring允许BeanFactoryPostProcessor在容器实例化任何其他bean之前读取配置元数据,并根据其进行修改,就比如@ConditionalOnProperty注解常用来根据配置文件的相关配置控制是否需要创建相应的bean

核心代码

ConfigurationClassPostProcessor执行、装载时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
refreshContext(context);

@Override
protected final void refreshBeanFactory() throws BeansException {
...
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

处理@Configuration等注解的处理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList();
String[] candidateNames = registry.getBeanDefinitionNames();
String[] var4 = candidateNames;
int var5 = candidateNames.length;

...
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);

Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
parser.parse(candidates);
parser.validate();
Set<ConfigurationClass> configClasses = new LinkedHashSet(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry());
}
...
}

在这里我们可以看到一个ConfigurationClassParser对象,进入他的构造函数看下

1
2
3
4
5
6
7
8
9
10
11
12
13
public ConfigurationClassParser(MetadataReaderFactory metadataReaderFactory,
ProblemReporter problemReporter, Environment environment, ResourceLoader resourceLoader,
BeanNameGenerator componentScanBeanNameGenerator, BeanDefinitionRegistry registry) {

this.metadataReaderFactory = metadataReaderFactory;
this.problemReporter = problemReporter;
this.environment = environment;
this.resourceLoader = resourceLoader;
this.registry = registry;
this.componentScanParser = new ComponentScanAnnotationParser(
environment, resourceLoader, componentScanBeanNameGenerator, registry);
this.conditionEvaluator = new ConditionEvaluator(registry, environment, resourceLoader);
}

重点关注这个ConditionEvaluator类,看看官方对这个类作用的介绍

Internal class used to evaluate {@link Conditional} annotations.

那么就清楚了,@ConditionalOnXXX注解的解析都是由这个类去做的,然后通过ConfigurationClassParser类的parse方法执行后,就会将满足@ConditionalOnXXX注解的Bean进行创建

在代码parser.getConfigurationClasses()执行后,会根据Environment中的配置元数据以及@ConditionalOnProperty中的设置,对bean进行过滤操作,不满条件的bean不会被转载进Spring Container

测试代码

people.enable=false时

nacos-server 配置信息-1

代码执行情况-false

people.enable=true时

nacos-server 配置信息-2

代码执行情况-true

(提醒:之所以这里nacos的配置能够与SpringBoot@ConditionalOnProperty,是因为我这进行了修改,初步解决了此问题)

Nacos-Spring-Context中相关的bean,都是在这之后才被解析、装载进入Spring Container的,为什么这么说?这里直接拉出官方对于ConfigurationClassPostProcessor的注释

{@link BeanFactoryPostProcessor} used for bootstrapping processing of
{@link Configuration @Configuration} classes.

Registered by default when using {@code context:annotation-config/} or
{@code context:component-scan/}. Otherwise, may be declared manually as
with any other BeanFactoryPostProcessor.
This post processor is priority-ordered as it is important that any
{@link Bean} methods declared in {@code @Configuration} classes have
their corresponding bean definitions registered before any other
{@link BeanFactoryPostProcessor} executes.

大致意思就是,ConfigurationClassPostProcessor的优先级是所有BeanFactoryPostProcessor实现中最高的,他必须在其他的BeanFactoryPostProcessor之前执行,因为其他的BeanFactoryPostProcessor需要ConfigurationClassPostProcessor进行解析装载进Spring Container

解决方案

清楚该ISSUE为什么会出现的原因之后,那么相应的解决方案就很快的出来了。之前说到ConfigurationClassPostProcessor它会去解析@ConditionalOnProperty,并且它的执行先于其他的BeanFactoryPostProcessor实现;那么,还有什么比它先执行呢?

ApplicationContextInitializer

1
prepareContext(context, environment, listeners, applicationArguments, printedBanner);

可以看出,ApplicationContextInitializer是在prepareContext执行的,而ConfigurationClassPostProcessor是在refreshContext执行的,因此,我们只需要将配置拉取的代码提前到ApplicationContextInitializer中即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Override
public void initialize(ConfigurableApplicationContext context) {
environment = context.getEnvironment();
if (isEnable()) {
CompositePropertySource compositePropertySource = new CompositePropertySource(NacosConfigConstants.NACOS_BOOTSTRAP_PROPERTY_APPLICATION);
CacheableEventPublishingNacosServiceFactory singleton = CacheableEventPublishingNacosServiceFactory.getSingleton();
singleton.setApplicationContext(context);
String[] dataIds;
String[] groupIds;
String[] namespaces;
try {
dataIds = environment.getProperty(NacosConfigConstants.NACOS_CONFIG_DATA_ID, String[].class, new String[]{});
groupIds = environment.getProperty(NacosConfigConstants.NACOS_CONFIG_GROUP_ID, String[].class, new String[dataIds.length]);
namespaces = environment.getProperty(NacosProperties.NAMESPACE, String[].class, new String[dataIds.length]);

for (int i = 0; i < dataIds.length; i ++) {
Properties buildInfo = properties(namespaces[i]);
ConfigService configService = singleton.createConfigService(buildInfo);

String group = StringUtils.isEmpty(groupIds[i]) ? Constants.DEFAULT_GROUP : groupIds[i];
String config = configService.getConfig(dataIds[i], group, 1000);
if (config == null) {
logger.error("nacos-config-spring-boot : get config failed");
continue;
}
String name = buildDefaultPropertySourceName(dataIds[i], groupIds[i], buildInfo);
NacosPropertySource nacosPropertySource = new NacosPropertySource(name, config);
compositePropertySource.addPropertySource(nacosPropertySource);
}
environment.getPropertySources().addFirst(compositePropertySource);
} catch (NacosException e) {
logger.error(e.getErrMsg());
}
}
}