概要
背景是我在写测试类的时候,出现以下报错
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.data.redis.connection.RedisConnectionFactory' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
下面是我的依赖和配置类,主要是使用的springboot-starter-data-redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@Configuration
//@ConditionalOnProperty("spring.redis.host")
public class RedisConfig {
@Bean("uidGeneratorRedisTemplate")
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setDefaultSerializer(StringRedisSerializer.UTF_8);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
@ConditionalOnMissingBean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory factory = new LettuceConnectionFactory();
factory.setHostName("127.0.0.1");
factory.setPort(4040);
return factory;
}
}
在我的预想中,
- 由于springboot-web包引入了spring-boot-autoconfigure,其中加载了配置类RedisAutoConfiguration(见下图),
- 在LettuceConnectionConfiguration中配置了redisConnectionFactory,因此我自己写的RedisConnectionFactory bean应该不会生效才对。
@Bean
@ConditionalOnMissingBean({RedisConnectionFactory.class})
LettuceConnectionFactory redisConnectionFactory(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers, ClientResources clientResources) throws UnknownHostException {
LettuceClientConfiguration clientConfig = this.getLettuceClientConfiguration(builderCustomizers, clientResources, this.getProperties().getLettuce().getPool());
return this.createLettuceConnectionFactory(clientConfig);
}
但是事实恰恰相反,我在application.properties配置端口为4041,而在代码中配置为4040,看错误日志可看出实际是代码中的bean被加载了。
自动装配流程及调用链
针对上面问题先说结论:排查发现是装配顺序问题,由于先装配了自己写的bean,然后再去装配autoconfigure中的默认配置,导致后者的条件不满足@ConditionalOnMissingBean({RedisConnectionFactory.class})
我们先看看 @ConditionalOnMissingBean注解做了什么,其他同理:
OnBeanCondition源码:可以看出引入了OnBeanCondition
OnBeanCondition的继承结构:重点在于Condition接口的match方法
- org.springframework.context.annotation.ConditionEvaluator shouldSkip`
{
... // 获取Condition集合
do {
do {
if (!var4.hasNext()) {
return false;
}
condition = (Condition)var4.next();
requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition)condition).getConfigurationPhase();
}
} while(requiredPhase != null && requiredPhase != phase);
// 遍历condition逐个判断是否应该跳过bean注册流程
} while(condition.matches(this.context, metadata));
return true;
}
} else {
return false;
}
重点逻辑在condition.matches(this.context, metadata)
2.org.springframework.boot.autoconfigure.condition.SpringBootCondition matches(this.context, metadata)
其中调用了 this.getMatchOutcome(context, metadata);是一个抽象方法,由OnBeanCondition实现
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 用于保存检测结果
ConditionMessage matchMessage = ConditionMessage.empty();
// 获取注解
MergedAnnotations annotations = metadata.getAnnotations();
Spec spec;
MatchResult matchResult;
String reason;
if (annotations.isPresent(ConditionalOnBean.class)) {
spec = new Spec(context, metadata, annotations, ConditionalOnBean.class);
matchResult = this.getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
reason = this.createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE, matchResult.getNamesOfAllMatches());
}
......
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
spec = new Spec(context, metadata, annotations, ConditionalOnMissingBean.class);
matchResult = this.getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
reason = this.createOnMissingBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
}
return ConditionOutcome.match(matchMessage);
}
其中spec描述了Conditional注解类型和参数
4. this.getMatchingBeans(context, spec);检查是否存在参数中配置的每个类是否存在,最终根据不同的Conditional类别综合getMatchingBeans的结果判断是否应该跳过