Skip to content

Commit 457d041

Browse files
committed
Make ConditionalOnBean a logical AND rather than an OR
Previously ConditionalOnBean was the inverse of ConditionalOnMissingBean. This meant that the former was a logical OR while the latter was a logical AND of the requiremnts declared via the annotation's attributes. This commit changes the logic for ConditionalOnBean so that it is now a logical AND. A side-effect of this change is that more information about what has and has not matched must be tracked during the evaluation. This extra information is now used to provide more informative messages in the condition evaluation report that indicate exactly why @ConditionalOnBean or @ConditionalOnMissingBean did not match. Closes gh-5279
1 parent 250a160 commit 457d041

File tree

3 files changed

+188
-42
lines changed

3 files changed

+188
-42
lines changed

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2016 the original author or authors.
2+
* Copyright 2012-2017 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.
@@ -45,30 +45,30 @@
4545
public @interface ConditionalOnBean {
4646

4747
/**
48-
* The class type of bean that should be checked. The condition matches when any of
49-
* the classes specified is contained in the {@link ApplicationContext}.
48+
* The class type of bean that should be checked. The condition matches when all of
49+
* the classes specified are contained in the {@link ApplicationContext}.
5050
* @return the class types of beans to check
5151
*/
5252
Class<?>[] value() default {};
5353

5454
/**
55-
* The class type names of bean that should be checked. The condition matches when any
56-
* of the classes specified is contained in the {@link ApplicationContext}.
55+
* The class type names of bean that should be checked. The condition matches when all
56+
* of the classes specified are contained in the {@link ApplicationContext}.
5757
* @return the class type names of beans to check
5858
*/
5959
String[] type() default {};
6060

6161
/**
6262
* The annotation type decorating a bean that should be checked. The condition matches
63-
* when any of the annotations specified is defined on a bean in the
63+
* when all of the annotations specified are defined on beans in the
6464
* {@link ApplicationContext}.
6565
* @return the class-level annotation types to check
6666
*/
6767
Class<? extends Annotation>[] annotation() default {};
6868

6969
/**
70-
* The names of beans to check. The condition matches when any of the bean names
71-
* specified is contained in the {@link ApplicationContext}.
70+
* The names of beans to check. The condition matches when all of the bean names
71+
* specified are contained in the {@link ApplicationContext}.
7272
* @return the name of beans to check
7373
*/
7474
String[] name() default {};

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

Lines changed: 178 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
import java.util.Arrays;
2222
import java.util.Collection;
2323
import java.util.Collections;
24+
import java.util.HashMap;
25+
import java.util.HashSet;
2426
import java.util.LinkedHashSet;
2527
import java.util.List;
28+
import java.util.Map;
2629
import java.util.Set;
2730

2831
import org.springframework.beans.factory.BeanFactory;
@@ -76,80 +79,163 @@ public ConditionOutcome getMatchOutcome(ConditionContext context,
7679
if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
7780
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
7881
ConditionalOnBean.class);
79-
List<String> matching = getMatchingBeans(context, spec);
80-
if (matching.isEmpty()) {
81-
return ConditionOutcome.noMatch(
82-
ConditionMessage.forCondition(ConditionalOnBean.class, spec)
83-
.didNotFind("any beans").atAll());
82+
MatchResult matchResult = getMatchingBeans(context, spec);
83+
if (!matchResult.isAllMatched()) {
84+
String reason = createOnBeanNoMatchReason(matchResult);
85+
return ConditionOutcome.noMatch(ConditionMessage
86+
.forCondition(ConditionalOnBean.class, spec).because(reason));
8487
}
8588
matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec)
86-
.found("bean", "beans").items(Style.QUOTE, matching);
89+
.found("bean", "beans")
90+
.items(Style.QUOTE, matchResult.getNamesOfAllMatches());
8791
}
8892
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
8993
BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata,
9094
ConditionalOnSingleCandidate.class);
91-
List<String> matching = getMatchingBeans(context, spec);
92-
if (matching.isEmpty()) {
95+
MatchResult matchResult = getMatchingBeans(context, spec);
96+
if (!matchResult.isAllMatched()) {
9397
return ConditionOutcome.noMatch(ConditionMessage
9498
.forCondition(ConditionalOnSingleCandidate.class, spec)
9599
.didNotFind("any beans").atAll());
96100
}
97-
else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matching,
101+
else if (!hasSingleAutowireCandidate(context.getBeanFactory(),
102+
matchResult.getNamesOfAllMatches(),
98103
spec.getStrategy() == SearchStrategy.ALL)) {
99104
return ConditionOutcome.noMatch(ConditionMessage
100105
.forCondition(ConditionalOnSingleCandidate.class, spec)
101106
.didNotFind("a primary bean from beans")
102-
.items(Style.QUOTE, matching));
107+
.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
103108
}
104109
matchMessage = matchMessage
105110
.andCondition(ConditionalOnSingleCandidate.class, spec)
106-
.found("a primary bean from beans").items(Style.QUOTE, matching);
111+
.found("a primary bean from beans")
112+
.items(Style.QUOTE, matchResult.namesOfAllMatches);
107113
}
108114
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
109115
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
110116
ConditionalOnMissingBean.class);
111-
List<String> matching = getMatchingBeans(context, spec);
112-
if (!matching.isEmpty()) {
117+
MatchResult matchResult = getMatchingBeans(context, spec);
118+
if (matchResult.isAnyMatched()) {
119+
String reason = createOnMissingBeanNoMatchReason(matchResult);
113120
return ConditionOutcome.noMatch(ConditionMessage
114121
.forCondition(ConditionalOnMissingBean.class, spec)
115-
.found("bean", "beans").items(Style.QUOTE, matching));
122+
.because(reason));
116123
}
117124
matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec)
118125
.didNotFind("any beans").atAll();
119126
}
120127
return ConditionOutcome.match(matchMessage);
121128
}
122129

123-
private List<String> getMatchingBeans(ConditionContext context,
124-
BeanSearchSpec beans) {
130+
private String createOnBeanNoMatchReason(MatchResult matchResult) {
131+
StringBuilder reason = new StringBuilder();
132+
appendMessageForNoMatches(reason, matchResult.unmatchedAnnotations,
133+
"annotated with");
134+
appendMessageForNoMatches(reason, matchResult.unmatchedTypes, "of type");
135+
appendMessageForNoMatches(reason, matchResult.unmatchedNames, "named");
136+
return reason.toString();
137+
}
138+
139+
private void appendMessageForNoMatches(StringBuilder reason,
140+
Collection<String> unmatched, String description) {
141+
if (!unmatched.isEmpty()) {
142+
if (reason.length() > 0) {
143+
reason.append(" and ");
144+
}
145+
reason.append("did not find any beans ");
146+
reason.append(description);
147+
reason.append(" ");
148+
reason.append(StringUtils.collectionToDelimitedString(unmatched, ", "));
149+
}
150+
}
151+
152+
private String createOnMissingBeanNoMatchReason(MatchResult matchResult) {
153+
StringBuilder reason = new StringBuilder();
154+
appendMessageForMatches(reason, matchResult.matchedAnnotations, "annotated with");
155+
appendMessageForMatches(reason, matchResult.matchedTypes, "of type");
156+
if (!matchResult.matchedNames.isEmpty()) {
157+
if (reason.length() > 0) {
158+
reason.append(" and ");
159+
}
160+
reason.append("found beans named ");
161+
reason.append(StringUtils
162+
.collectionToDelimitedString(matchResult.matchedNames, ", "));
163+
}
164+
return reason.toString();
165+
}
166+
167+
private void appendMessageForMatches(StringBuilder reason,
168+
Map<String, Collection<String>> matches, String description) {
169+
if (!matches.isEmpty()) {
170+
for (Map.Entry<String, Collection<String>> match : matches.entrySet()) {
171+
if (reason.length() > 0) {
172+
reason.append(" and ");
173+
}
174+
reason.append("found beans ");
175+
reason.append(description);
176+
reason.append("'");
177+
reason.append(match.getKey());
178+
reason.append("'");
179+
reason.append(
180+
StringUtils.collectionToDelimitedString(match.getValue(), ", "));
181+
}
182+
}
183+
}
184+
185+
private MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) {
125186
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
126187
if (beans.getStrategy() == SearchStrategy.ANCESTORS) {
127188
BeanFactory parent = beanFactory.getParentBeanFactory();
128189
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
129190
"Unable to use SearchStrategy.PARENTS");
130191
beanFactory = (ConfigurableListableBeanFactory) parent;
131192
}
132-
if (beanFactory == null) {
133-
return Collections.emptyList();
134-
}
135-
List<String> beanNames = new ArrayList<String>();
193+
MatchResult matchResult = new MatchResult();
136194
boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;
195+
List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(
196+
beans.getIgnoredTypes(), beanFactory, context, considerHierarchy);
137197
for (String type : beans.getTypes()) {
138-
beanNames.addAll(getBeanNamesForType(beanFactory, type,
139-
context.getClassLoader(), considerHierarchy));
140-
}
141-
for (String ignoredType : beans.getIgnoredTypes()) {
142-
beanNames.removeAll(getBeanNamesForType(beanFactory, ignoredType,
143-
context.getClassLoader(), considerHierarchy));
198+
Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,
199+
context.getClassLoader(), considerHierarchy);
200+
typeMatches.removeAll(beansIgnoredByType);
201+
if (typeMatches.isEmpty()) {
202+
matchResult.recordUnmatchedType(type);
203+
}
204+
else {
205+
matchResult.recordMatchedType(type, typeMatches);
206+
}
144207
}
145208
for (String annotation : beans.getAnnotations()) {
146-
beanNames.addAll(Arrays.asList(getBeanNamesForAnnotation(beanFactory,
147-
annotation, context.getClassLoader(), considerHierarchy)));
209+
List<String> annotationMatches = Arrays
210+
.asList(getBeanNamesForAnnotation(beanFactory, annotation,
211+
context.getClassLoader(), considerHierarchy));
212+
annotationMatches.removeAll(beansIgnoredByType);
213+
if (annotationMatches.isEmpty()) {
214+
matchResult.recordUnmatchedAnnotation(annotation);
215+
}
216+
else {
217+
matchResult.recordMatchedAnnotation(annotation, annotationMatches);
218+
}
148219
}
149220
for (String beanName : beans.getNames()) {
150-
if (containsBean(beanFactory, beanName, considerHierarchy)) {
151-
beanNames.add(beanName);
221+
if (!beansIgnoredByType.contains(beanName)
222+
&& containsBean(beanFactory, beanName, considerHierarchy)) {
223+
matchResult.recordMatchedName(beanName);
152224
}
225+
else {
226+
matchResult.recordUnmatchedName(beanName);
227+
}
228+
}
229+
return matchResult;
230+
}
231+
232+
private List<String> getNamesOfBeansIgnoredByType(List<String> ignoredTypes,
233+
ListableBeanFactory beanFactory, ConditionContext context,
234+
boolean considerHierarchy) {
235+
List<String> beanNames = new ArrayList<String>();
236+
for (String ignoredType : ignoredTypes) {
237+
beanNames.addAll(getBeanNamesForType(beanFactory, ignoredType,
238+
context.getClassLoader(), considerHierarchy));
153239
}
154240
return beanNames;
155241
}
@@ -227,15 +313,15 @@ private String[] getBeanNamesForAnnotation(
227313
}
228314

229315
private boolean hasSingleAutowireCandidate(
230-
ConfigurableListableBeanFactory beanFactory, List<String> beanNames,
316+
ConfigurableListableBeanFactory beanFactory, Set<String> beanNames,
231317
boolean considerHierarchy) {
232318
return (beanNames.size() == 1
233319
|| getPrimaryBeans(beanFactory, beanNames, considerHierarchy)
234320
.size() == 1);
235321
}
236322

237323
private List<String> getPrimaryBeans(ConfigurableListableBeanFactory beanFactory,
238-
List<String> beanNames, boolean considerHierarchy) {
324+
Set<String> beanNames, boolean considerHierarchy) {
239325
List<String> primaryBeans = new ArrayList<String>();
240326
for (String beanName : beanNames) {
241327
BeanDefinition beanDefinition = findBeanDefinition(beanFactory, beanName,
@@ -439,4 +525,64 @@ private BeanTypeDeductionException(String className, String beanMethodName,
439525

440526
}
441527

528+
static final class MatchResult {
529+
530+
private final Map<String, Collection<String>> matchedAnnotations = new HashMap<String, Collection<String>>();
531+
532+
private final List<String> matchedNames = new ArrayList<String>();
533+
534+
private final Map<String, Collection<String>> matchedTypes = new HashMap<String, Collection<String>>();
535+
536+
private final List<String> unmatchedAnnotations = new ArrayList<String>();
537+
538+
private final List<String> unmatchedNames = new ArrayList<String>();
539+
540+
private final List<String> unmatchedTypes = new ArrayList<String>();
541+
542+
private final Set<String> namesOfAllMatches = new HashSet<String>();
543+
544+
private void recordMatchedName(String name) {
545+
this.matchedNames.add(name);
546+
this.namesOfAllMatches.add(name);
547+
}
548+
549+
private void recordUnmatchedName(String name) {
550+
this.unmatchedNames.add(name);
551+
}
552+
553+
private void recordMatchedAnnotation(String annotation,
554+
Collection<String> matchingNames) {
555+
this.matchedAnnotations.put(annotation, matchingNames);
556+
this.namesOfAllMatches.addAll(matchingNames);
557+
}
558+
559+
private void recordUnmatchedAnnotation(String annotation) {
560+
this.unmatchedAnnotations.add(annotation);
561+
}
562+
563+
private void recordMatchedType(String type, Collection<String> matchingNames) {
564+
this.matchedTypes.put(type, matchingNames);
565+
this.namesOfAllMatches.addAll(matchingNames);
566+
}
567+
568+
private void recordUnmatchedType(String type) {
569+
this.unmatchedTypes.add(type);
570+
}
571+
572+
private boolean isAllMatched() {
573+
return this.unmatchedAnnotations.isEmpty() && this.unmatchedNames.isEmpty()
574+
&& this.unmatchedTypes.isEmpty();
575+
}
576+
577+
private boolean isAnyMatched() {
578+
return (!this.matchedAnnotations.isEmpty()) || (!this.matchedNames.isEmpty())
579+
|| (!this.matchedTypes.isEmpty());
580+
}
581+
582+
private Set<String> getNamesOfAllMatches() {
583+
return this.namesOfAllMatches;
584+
}
585+
586+
}
587+
442588
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2016 the original author or authors.
2+
* Copyright 2012-2017 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.
@@ -57,7 +57,7 @@ public void testNameAndTypeOnBeanCondition() {
5757
this.context.register(FooConfiguration.class,
5858
OnBeanNameAndTypeConfiguration.class);
5959
this.context.refresh();
60-
assertThat(this.context.containsBean("bar")).isTrue();
60+
assertThat(this.context.containsBean("bar")).isFalse();
6161
}
6262

6363
@Test

0 commit comments

Comments
 (0)