Skip to content

Commit 12ec18f

Browse files
committed
Consider fallback beans when evaluating ConditionalOnSingleCandidate
Closes gh-41580
1 parent 3561ab8 commit 12ec18f

File tree

2 files changed

+150
-57
lines changed

2 files changed

+150
-57
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java

Lines changed: 107 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
import java.util.List;
2929
import java.util.Locale;
3030
import java.util.Map;
31+
import java.util.Map.Entry;
3132
import java.util.Set;
33+
import java.util.function.Predicate;
3234

3335
import org.springframework.aop.scope.ScopedProxyUtils;
3436
import org.springframework.beans.factory.BeanFactory;
@@ -113,61 +115,90 @@ private ConditionOutcome getOutcome(Set<String> requiredBeanTypes, Class<? exten
113115

114116
@Override
115117
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
116-
ConditionMessage matchMessage = ConditionMessage.empty();
118+
ConditionOutcome matchOutcome = ConditionOutcome.match();
117119
MergedAnnotations annotations = metadata.getAnnotations();
118120
if (annotations.isPresent(ConditionalOnBean.class)) {
119121
Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
120-
MatchResult matchResult = getMatchingBeans(context, spec);
121-
if (!matchResult.isAllMatched()) {
122-
String reason = createOnBeanNoMatchReason(matchResult);
123-
return ConditionOutcome.noMatch(spec.message().because(reason));
122+
matchOutcome = evaluateConditionalOnBean(spec, matchOutcome.getConditionMessage());
123+
if (!matchOutcome.isMatch()) {
124+
return matchOutcome;
124125
}
125-
matchMessage = spec.message(matchMessage)
126-
.found("bean", "beans")
127-
.items(Style.QUOTE, matchResult.getNamesOfAllMatches());
128126
}
129127
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
130-
Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
131-
MatchResult matchResult = getMatchingBeans(context, spec);
132-
if (!matchResult.isAllMatched()) {
133-
return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
134-
}
135-
Set<String> allBeans = matchResult.getNamesOfAllMatches();
136-
if (allBeans.size() == 1) {
137-
matchMessage = spec.message(matchMessage).found("a single bean").items(Style.QUOTE, allBeans);
138-
}
139-
else {
140-
List<String> primaryBeans = getPrimaryBeans(context.getBeanFactory(), allBeans,
141-
spec.getStrategy() == SearchStrategy.ALL);
142-
if (primaryBeans.isEmpty()) {
143-
return ConditionOutcome
144-
.noMatch(spec.message().didNotFind("a primary bean from beans").items(Style.QUOTE, allBeans));
145-
}
146-
if (primaryBeans.size() > 1) {
147-
return ConditionOutcome
148-
.noMatch(spec.message().found("multiple primary beans").items(Style.QUOTE, primaryBeans));
149-
}
150-
matchMessage = spec.message(matchMessage)
151-
.found("a single primary bean '" + primaryBeans.get(0) + "' from beans")
152-
.items(Style.QUOTE, allBeans);
128+
Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata,
129+
metadata.getAnnotations());
130+
matchOutcome = evaluateConditionalOnSingleCandidate(spec, matchOutcome.getConditionMessage());
131+
if (!matchOutcome.isMatch()) {
132+
return matchOutcome;
153133
}
154134
}
155135
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
156136
Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
157137
ConditionalOnMissingBean.class);
158-
MatchResult matchResult = getMatchingBeans(context, spec);
159-
if (matchResult.isAnyMatched()) {
160-
String reason = createOnMissingBeanNoMatchReason(matchResult);
161-
return ConditionOutcome.noMatch(spec.message().because(reason));
138+
matchOutcome = evaluateConditionalOnMissingBean(spec, matchOutcome.getConditionMessage());
139+
if (!matchOutcome.isMatch()) {
140+
return matchOutcome;
162141
}
163-
matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
164142
}
165-
return ConditionOutcome.match(matchMessage);
143+
return matchOutcome;
144+
}
145+
146+
private ConditionOutcome evaluateConditionalOnBean(Spec<ConditionalOnBean> spec, ConditionMessage matchMessage) {
147+
MatchResult matchResult = getMatchingBeans(spec);
148+
if (!matchResult.isAllMatched()) {
149+
String reason = createOnBeanNoMatchReason(matchResult);
150+
return ConditionOutcome.noMatch(spec.message().because(reason));
151+
}
152+
return ConditionOutcome.match(spec.message(matchMessage)
153+
.found("bean", "beans")
154+
.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
155+
}
156+
157+
private ConditionOutcome evaluateConditionalOnSingleCandidate(Spec<ConditionalOnSingleCandidate> spec,
158+
ConditionMessage matchMessage) {
159+
MatchResult matchResult = getMatchingBeans(spec);
160+
if (!matchResult.isAllMatched()) {
161+
return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
162+
}
163+
Set<String> allBeans = matchResult.getNamesOfAllMatches();
164+
if (allBeans.size() == 1) {
165+
return ConditionOutcome
166+
.match(spec.message(matchMessage).found("a single bean").items(Style.QUOTE, allBeans));
167+
}
168+
Map<String, BeanDefinition> beanDefinitions = getBeanDefinitions(spec.context.getBeanFactory(), allBeans,
169+
spec.getStrategy() == SearchStrategy.ALL);
170+
List<String> primaryBeans = getPrimaryBeans(beanDefinitions);
171+
if (primaryBeans.size() == 1) {
172+
return ConditionOutcome.match(spec.message(matchMessage)
173+
.found("a single primary bean '" + primaryBeans.get(0) + "' from beans")
174+
.items(Style.QUOTE, allBeans));
175+
}
176+
if (primaryBeans.size() > 1) {
177+
return ConditionOutcome
178+
.noMatch(spec.message().found("multiple primary beans").items(Style.QUOTE, primaryBeans));
179+
}
180+
List<String> nonFallbackBeans = getNonFallbackBeans(beanDefinitions);
181+
if (nonFallbackBeans.size() == 1) {
182+
return ConditionOutcome.match(spec.message(matchMessage)
183+
.found("a single non-fallback bean '" + nonFallbackBeans.get(0) + "' from beans")
184+
.items(Style.QUOTE, allBeans));
185+
}
186+
return ConditionOutcome.noMatch(spec.message().found("multiple beans").items(Style.QUOTE, allBeans));
166187
}
167188

168-
protected final MatchResult getMatchingBeans(ConditionContext context, Spec<?> spec) {
169-
ClassLoader classLoader = context.getClassLoader();
170-
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
189+
private ConditionOutcome evaluateConditionalOnMissingBean(Spec<ConditionalOnMissingBean> spec,
190+
ConditionMessage matchMessage) {
191+
MatchResult matchResult = getMatchingBeans(spec);
192+
if (matchResult.isAnyMatched()) {
193+
String reason = createOnMissingBeanNoMatchReason(matchResult);
194+
return ConditionOutcome.noMatch(spec.message().because(reason));
195+
}
196+
return ConditionOutcome.match(spec.message(matchMessage).didNotFind("any beans").atAll());
197+
}
198+
199+
protected final MatchResult getMatchingBeans(Spec<?> spec) {
200+
ClassLoader classLoader = spec.getContext().getClassLoader();
201+
ConfigurableListableBeanFactory beanFactory = spec.getContext().getBeanFactory();
171202
boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
172203
Set<Class<?>> parameterizedContainers = spec.getParameterizedContainers();
173204
if (spec.getStrategy() == SearchStrategy.ANCESTORS) {
@@ -373,16 +404,32 @@ private void appendMessageForMatches(StringBuilder reason, Map<String, Collectio
373404
}
374405
}
375406

376-
private List<String> getPrimaryBeans(ConfigurableListableBeanFactory beanFactory, Set<String> beanNames,
377-
boolean considerHierarchy) {
378-
List<String> primaryBeans = new ArrayList<>();
407+
private Map<String, BeanDefinition> getBeanDefinitions(ConfigurableListableBeanFactory beanFactory,
408+
Set<String> beanNames, boolean considerHierarchy) {
409+
Map<String, BeanDefinition> definitions = new HashMap<>(beanNames.size());
379410
for (String beanName : beanNames) {
380411
BeanDefinition beanDefinition = findBeanDefinition(beanFactory, beanName, considerHierarchy);
381-
if (beanDefinition != null && beanDefinition.isPrimary()) {
382-
primaryBeans.add(beanName);
412+
definitions.put(beanName, beanDefinition);
413+
}
414+
return definitions;
415+
}
416+
417+
private List<String> getPrimaryBeans(Map<String, BeanDefinition> beanDefinitions) {
418+
return getMatchingBeans(beanDefinitions, BeanDefinition::isPrimary);
419+
}
420+
421+
private List<String> getNonFallbackBeans(Map<String, BeanDefinition> beanDefinitions) {
422+
return getMatchingBeans(beanDefinitions, Predicate.not(BeanDefinition::isFallback));
423+
}
424+
425+
private List<String> getMatchingBeans(Map<String, BeanDefinition> beanDefinitions, Predicate<BeanDefinition> test) {
426+
List<String> matches = new ArrayList<>();
427+
for (Entry<String, BeanDefinition> namedBeanDefinition : beanDefinitions.entrySet()) {
428+
if (test.test(namedBeanDefinition.getValue())) {
429+
matches.add(namedBeanDefinition.getKey());
383430
}
384431
}
385-
return primaryBeans;
432+
return matches;
386433
}
387434

388435
private BeanDefinition findBeanDefinition(ConfigurableListableBeanFactory beanFactory, String beanName,
@@ -420,7 +467,7 @@ private static Set<String> addAll(Set<String> result, String[] additional) {
420467
*/
421468
private static class Spec<A extends Annotation> {
422469

423-
private final ClassLoader classLoader;
470+
private final ConditionContext context;
424471

425472
private final Class<? extends Annotation> annotationType;
426473

@@ -442,7 +489,7 @@ private static class Spec<A extends Annotation> {
442489
.filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
443490
.collect(MergedAnnotationCollectors.toMultiValueMap(Adapt.CLASS_TO_STRING));
444491
MergedAnnotation<A> annotation = annotations.get(annotationType);
445-
this.classLoader = context.getClassLoader();
492+
this.context = context;
446493
this.annotationType = annotationType;
447494
this.names = extract(attributes, "name");
448495
this.annotations = extract(attributes, "annotation");
@@ -497,7 +544,7 @@ private Set<Class<?>> resolveWhenPossible(Set<String> classNames) {
497544
Set<Class<?>> resolved = new LinkedHashSet<>(classNames.size());
498545
for (String className : classNames) {
499546
try {
500-
resolved.add(resolve(className, this.classLoader));
547+
resolved.add(resolve(className, this.context.getClassLoader()));
501548
}
502549
catch (ClassNotFoundException | NoClassDefFoundError ex) {
503550
// Ignore
@@ -596,31 +643,35 @@ private SearchStrategy getStrategy() {
596643
return (this.strategy != null) ? this.strategy : SearchStrategy.ALL;
597644
}
598645

599-
Set<String> getNames() {
646+
private ConditionContext getContext() {
647+
return this.context;
648+
}
649+
650+
private Set<String> getNames() {
600651
return this.names;
601652
}
602653

603-
Set<String> getTypes() {
654+
protected Set<String> getTypes() {
604655
return this.types;
605656
}
606657

607-
Set<String> getAnnotations() {
658+
private Set<String> getAnnotations() {
608659
return this.annotations;
609660
}
610661

611-
Set<String> getIgnoredTypes() {
662+
private Set<String> getIgnoredTypes() {
612663
return this.ignoredTypes;
613664
}
614665

615-
Set<Class<?>> getParameterizedContainers() {
666+
private Set<Class<?>> getParameterizedContainers() {
616667
return this.parameterizedContainers;
617668
}
618669

619-
ConditionMessage.Builder message() {
670+
private ConditionMessage.Builder message() {
620671
return ConditionMessage.forCondition(this.annotationType, this);
621672
}
622673

623-
ConditionMessage.Builder message(ConditionMessage message) {
674+
private ConditionMessage.Builder message(ConditionMessage message) {
624675
return message.andCondition(this.annotationType, this);
625676
}
626677

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
2222
import org.springframework.context.annotation.Bean;
2323
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.context.annotation.Fallback;
2425
import org.springframework.context.annotation.Primary;
2526
import org.springframework.context.annotation.Scope;
2627
import org.springframework.context.annotation.ScopedProxyMode;
@@ -114,6 +115,17 @@ void singleCandidateMultipleCandidatesOnePrimary() {
114115
});
115116
}
116117

118+
@Test
119+
void singleCandidateTwoCandidatesOneNormalOneFallback() {
120+
this.contextRunner
121+
.withUserConfiguration(AlphaFallbackConfiguration.class, BravoConfiguration.class,
122+
OnBeanSingleCandidateConfiguration.class)
123+
.run((context) -> {
124+
assertThat(context).hasBean("consumer");
125+
assertThat(context.getBean("consumer")).isEqualTo("bravo");
126+
});
127+
}
128+
117129
@Test
118130
void singleCandidateMultipleCandidatesMultiplePrimary() {
119131
this.contextRunner
@@ -122,6 +134,14 @@ void singleCandidateMultipleCandidatesMultiplePrimary() {
122134
.run((context) -> assertThat(context).doesNotHaveBean("consumer"));
123135
}
124136

137+
@Test
138+
void singleCandidateMultipleCandidatesAllFallback() {
139+
this.contextRunner
140+
.withUserConfiguration(AlphaFallbackConfiguration.class, BravoFallbackConfiguration.class,
141+
OnBeanSingleCandidateConfiguration.class)
142+
.run((context) -> assertThat(context).doesNotHaveBean("consumer"));
143+
}
144+
125145
@Test
126146
void invalidAnnotationTwoTypes() {
127147
this.contextRunner.withUserConfiguration(OnBeanSingleCandidateTwoTypesConfiguration.class).run((context) -> {
@@ -208,6 +228,17 @@ String alpha() {
208228

209229
}
210230

231+
@Configuration(proxyBeanMethods = false)
232+
static class AlphaFallbackConfiguration {
233+
234+
@Bean
235+
@Fallback
236+
String alpha() {
237+
return "alpha";
238+
}
239+
240+
}
241+
211242
@Configuration(proxyBeanMethods = false)
212243
static class AlphaScopedProxyConfiguration {
213244

@@ -240,4 +271,15 @@ String bravo() {
240271

241272
}
242273

274+
@Configuration(proxyBeanMethods = false)
275+
static class BravoFallbackConfiguration {
276+
277+
@Bean
278+
@Fallback
279+
String bravo() {
280+
return "bravo";
281+
}
282+
283+
}
284+
243285
}

0 commit comments

Comments
 (0)