深入源码分析SpringBoot中使用@ConditionalOnBean无效的问题

1,308 阅读2分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

一、前言

最近在使用SpringBoot的@ConditionalOnBean的时候遇到一个很很奇特的问题。即在@Bean中使用@ConditionalOnBean注解,在可以确保需要依赖的Bean一定在此之前已经存在了,但是@ConditionalOnBean仍然未生效。启动的时候报错:not loaded because @ConditionalOnBean (types: ***; SearchStrategy: all) did not find any beans of type ***。接下来我们一起来分析下该问题。

二、问题简介

首先我们进一步介绍下该问题。我们在jar中通过AutoConfiguration引入了RedisClientConfiguration,在其中通过@ConditionalOnBean的方式注解了一个Bean(如下图)。即需要在有Cluster实例存在的情况下才初始化注册RedicClient的是到Spring容器中。

然后我们在业务代码工程中通过XML的方式在Spring容器中定义了一个Cluster实例,只是这个实例是通过FactoryBean实现的而已。即下图中ReloadableJimClientFactoryBean是一个FactoryBean,其实际得到的是一个Cluster实例。

当我们在启动后台会报错,无法加载RedisClient实例,因为Cluster实例并不存在。

此时问题就出现了,明明在代码工程中我们在xml中定义了Cluster实例,但是为什么启动的过程中SpringBoot在OnCondition判断的却找不不到该实例呢?接下来我们就从源码的层面分下下。

三、问题分析

1、原因分析

首先我们找到ConditionalOnBean的处理源码。其在OnBeanCondition的collectBeanNamesForType中。这里就是SpringBoot通过Cluster类型去查找对应的Bean实例的地方,如果找不到对应的Bean就不会实例化。通过debug咱们可以看到此时却是无法通过beanFactory.getBeanNamesForType获取到Cluster的Bean实例。

然后咱们通过源码跟踪到具体获取代码地址:

DefaultListableBeanFactory.getBeanNamesForType 
-> DefaultListableBeanFactory.doGetBeanNamesForType 
-> AbstractBeanFactory.isTypeMatch 
-> AbstractAutowireCapableBeanFactory.getTypeForFactoryBean

然后我们跳转该源码位置,发现通过通过ReloadableJimClientFactoryBean去获取其实际类型的时候,获取到了一个?,即无法和需要的类型Cluster匹配。于是返回了ResolvableType.NONE,即表示没有找到Cluster类型的Bean。

那么问题来了,为什么从ReloadableJimClientFactoryBean的实例去获取具体类型的时候是一个?而并不是其实现的Cluster呢?带着这个问题我们查看ReloadableJimClientFactoryBean的源码。通过如下源码发下其在实现FactoryBean的时候并没有显示指定泛型的类型,到时候Spring不能获取到其实现的类型。

于是我们尝试重新实现一个ReloadableJimClientFactoryBean类,让其显示指定对应泛型的类型。代码如下:

然后我们在xml中注册该类,并启动工程。启动之后无报错,于是我们再次debug查看关键部分的参数。首先AbstractAutowireCapableBeanFactory.getTypeForFactoryBean中,其能够真实获取到该FactoryBean对应的类型,且该类型为Cluster。

然后我们在OnBeanCondition的collectBeanNamesForType中可以看到,能够通过beanFactory.getBeanNamesForType获取对应的Cluster实例了。因此能够正常实例化RedisClient了。

2、问题总结

该问题存在的原因还是因为开发的时候没有怎么按规范编写代码导致的。正常情况所有泛型如果已知类型都需要没明确表示。

可以看到如果FactoryBean的类没有指定泛型类型,会导致Spring无法获取到该FactoryBean对应的实际实例的类型。所以导致了ConditionalOnBean无法通过类型找到对应的Bean实例,从而引起ConditionalOnBean注解的Bean实例无法被实例化。

四、延伸问题

在定位该问题的过程中,我遇到了另外一个导致ConditionalOnBean无法生效的问题。我们与如下配置:在业务代码(非jar)中通过@ConditionalOnBean的方式注解了一个Bean(如下图)。即需要在有Cluster实例存在的情况下才初始化注册RedicClient的是到Spring容器中。同时我们在工程中通过xml注入如JimClientFactoryBean类(该类没有上述的FactoryBean泛型类型问题)。

然后启动代码,发现同样报错,无法正常实例化Person类。

通过从源码层面分析Configuration的加载顺序(如下图),可以看到在Configuration中,BeanMethod是在XML之前完成加载,因此加载BeanMethod的时候无法读取到xml中定义的实例。源码路径:ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass

于是我们不在xml中定义JimClientFactoryBean,而改在Application类中定义(如下),此时不会有任何错误,工程能够正常启动。

这里给大家一个思考问题,如果咱们将代码写成如下方式,那么工程能够正常启动么?

五、惯例

如果你对本文有任何疑问或者高见,欢迎添加公众号lifeofcoder共同交流探讨。