• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18901940452

29 Oct 2025 08:40AM UTC coverage: 83.385% (-0.01%) from 83.397%
18901940452

push

github

TAKETODAY
:art:

60820 of 78019 branches covered (77.96%)

Branch coverage included in aggregate %.

143823 of 167400 relevant lines covered (85.92%)

3.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.75
today-context/src/main/java/infra/context/condition/OnBeanCondition.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.context.condition;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.io.Serial;
23
import java.lang.annotation.Annotation;
24
import java.lang.reflect.Method;
25
import java.util.ArrayList;
26
import java.util.Arrays;
27
import java.util.Collection;
28
import java.util.Collections;
29
import java.util.HashMap;
30
import java.util.HashSet;
31
import java.util.LinkedHashMap;
32
import java.util.LinkedHashSet;
33
import java.util.List;
34
import java.util.Locale;
35
import java.util.Map;
36
import java.util.Set;
37
import java.util.function.BiPredicate;
38
import java.util.function.Predicate;
39

40
import infra.aop.scope.ScopedProxyUtils;
41
import infra.beans.factory.BeanFactory;
42
import infra.beans.factory.BeanFactoryUtils;
43
import infra.beans.factory.HierarchicalBeanFactory;
44
import infra.beans.factory.NoSuchBeanDefinitionException;
45
import infra.beans.factory.config.BeanDefinition;
46
import infra.beans.factory.config.ConfigurableBeanFactory;
47
import infra.beans.factory.config.SingletonBeanRegistry;
48
import infra.beans.factory.support.AbstractBeanDefinition;
49
import infra.context.annotation.Condition;
50
import infra.context.annotation.ConditionContext;
51
import infra.context.annotation.ConfigurationCondition;
52
import infra.context.annotation.config.AutoConfigurationMetadata;
53
import infra.core.Ordered;
54
import infra.core.ResolvableType;
55
import infra.core.annotation.MergedAnnotation;
56
import infra.core.annotation.MergedAnnotation.Adapt;
57
import infra.core.annotation.MergedAnnotationCollectors;
58
import infra.core.annotation.MergedAnnotationPredicates;
59
import infra.core.annotation.MergedAnnotations;
60
import infra.core.type.AnnotatedTypeMetadata;
61
import infra.core.type.MethodMetadata;
62
import infra.lang.Assert;
63
import infra.lang.Contract;
64
import infra.stereotype.Component;
65
import infra.util.ClassUtils;
66
import infra.util.CollectionUtils;
67
import infra.util.MultiValueMap;
68
import infra.util.ObjectUtils;
69
import infra.util.ReflectionUtils;
70
import infra.util.StringUtils;
71

72
/**
73
 * {@link Condition} that checks for the presence or absence of specific beans.
74
 *
75
 * @author Phillip Webb
76
 * @author Dave Syer
77
 * @author Jakub Kubrynski
78
 * @author Stephane Nicoll
79
 * @author Andy Wilkinson
80
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
81
 * @see ConditionalOnBean
82
 * @see ConditionalOnMissingBean
83
 * @see ConditionalOnSingleCandidate
84
 */
85
class OnBeanCondition extends FilteringInfraCondition implements ConfigurationCondition, Ordered {
3✔
86

87
  @Override
88
  public ConfigurationPhase getConfigurationPhase() {
89
    return ConfigurationPhase.REGISTER_BEAN;
2✔
90
  }
91

92
  @Override
93
  public int getOrder() {
94
    return LOWEST_PRECEDENCE;
2✔
95
  }
96

97
  @Override
98
  @SuppressWarnings("NullAway")
99
  protected final @Nullable ConditionOutcome[] getOutcomes(String[] configClasses, AutoConfigurationMetadata configMetadata) {
100
    @Nullable ConditionOutcome[] outcomes = new ConditionOutcome[configClasses.length];
4✔
101
    for (int i = 0; i < outcomes.length; i++) {
8✔
102
      String autoConfigurationClass = configClasses[i];
4✔
103
      Set<String> onBeanTypes = configMetadata.getSet(autoConfigurationClass, "ConditionalOnBean");
5✔
104
      ConditionOutcome outcome = getOutcome(onBeanTypes, ConditionalOnBean.class);
5✔
105
      if (outcome == null) {
2!
106
        Set<String> onSingleCandidateTypes = configMetadata.getSet(autoConfigurationClass, "ConditionalOnSingleCandidate");
5✔
107
        outcome = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class);
5✔
108
      }
109
      outcomes[i] = outcome;
4✔
110
    }
111
    return outcomes;
2✔
112
  }
113

114
  @Nullable
115
  private ConditionOutcome getOutcome(@Nullable Set<String> requiredBeanTypes, Class<? extends Annotation> annotation) {
116
    List<String> missing = filter(requiredBeanTypes, ClassNameFilter.MISSING, getBeanClassLoader());
7✔
117
    if (!missing.isEmpty()) {
3!
118
      ConditionMessage message = ConditionMessage.forCondition(annotation)
×
119
              .didNotFind("required type", "required types")
×
120
              .items(ConditionMessage.Style.QUOTE, missing);
×
121
      return ConditionOutcome.noMatch(message);
×
122
    }
123
    return null;
2✔
124
  }
125

126
  @Override
127
  public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
128
    ConditionOutcome matchOutcome = ConditionOutcome.match();
2✔
129
    MergedAnnotations annotations = metadata.getAnnotations();
3✔
130
    if (annotations.isPresent(ConditionalOnBean.class)) {
4✔
131
      var spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
8✔
132
      matchOutcome = evaluateConditionalOnBean(spec, matchOutcome.getConditionMessage());
6✔
133
      if (!matchOutcome.isMatch()) {
3✔
134
        return matchOutcome;
2✔
135
      }
136
    }
137
    if (metadata.isAnnotated(ConditionalOnSingleCandidate.class)) {
4✔
138
      var spec = new SingleCandidateSpec(context, metadata, metadata.getAnnotations());
8✔
139
      matchOutcome = evaluateConditionalOnSingleCandidate(spec, matchOutcome.getConditionMessage());
6✔
140
      if (!matchOutcome.isMatch()) {
3✔
141
        return matchOutcome;
2✔
142
      }
143
    }
144
    if (metadata.isAnnotated(ConditionalOnMissingBean.class)) {
4✔
145
      var spec = new Spec<>(context, metadata, annotations, ConditionalOnMissingBean.class);
8✔
146
      matchOutcome = evaluateConditionalOnMissingBean(spec, matchOutcome.getConditionMessage());
6✔
147
      if (!matchOutcome.isMatch()) {
3✔
148
        return matchOutcome;
2✔
149
      }
150
    }
151
    return matchOutcome;
2✔
152
  }
153

154
  private ConditionOutcome evaluateConditionalOnBean(Spec<ConditionalOnBean> spec, ConditionMessage matchMessage) {
155
    MatchResult matchResult = getMatchingBeans(spec);
4✔
156
    if (matchResult.isNoneMatch()) {
3✔
157
      String reason = createOnBeanNoMatchReason(matchResult);
4✔
158
      return ConditionOutcome.noMatch(spec.message().because(reason));
6✔
159
    }
160
    return ConditionOutcome.match(spec.message(matchMessage)
7✔
161
            .found("bean", "beans")
4✔
162
            .items(ConditionMessage.Style.QUOTE, matchResult.namesOfAllMatches));
1✔
163
  }
164

165
  private ConditionOutcome evaluateConditionalOnSingleCandidate(Spec<ConditionalOnSingleCandidate> spec, ConditionMessage matchMessage) {
166
    MatchResult matchResult = getMatchingBeans(spec);
4✔
167
    if (matchResult.isNoneMatch()) {
3✔
168
      return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
7✔
169
    }
170
    Set<String> allBeans = matchResult.namesOfAllMatches;
3✔
171
    if (allBeans.size() == 1) {
4✔
172
      return ConditionOutcome
3✔
173
              .match(spec.message(matchMessage).found("a single bean").items(ConditionMessage.Style.QUOTE, allBeans));
7✔
174
    }
175
    var beanDefinitions = getBeanDefinitions(spec.context.getRequiredBeanFactory(), allBeans, spec.getStrategy() == SearchStrategy.ALL);
13!
176
    List<String> primaryBeans = getPrimaryBeans(beanDefinitions);
4✔
177
    if (primaryBeans.size() == 1) {
4✔
178
      return ConditionOutcome.match(spec.message(matchMessage)
12✔
179
              .found("a single primary bean '%s' from beans".formatted(primaryBeans.get(0)))
6✔
180
              .items(ConditionMessage.Style.QUOTE, allBeans));
1✔
181
    }
182
    if (primaryBeans.size() > 1) {
4✔
183
      return ConditionOutcome
2✔
184
              .noMatch(spec.message().found("multiple primary beans").items(ConditionMessage.Style.QUOTE, primaryBeans));
7✔
185
    }
186
    List<String> nonFallbackBeans = getNonFallbackBeans(beanDefinitions);
4✔
187
    if (nonFallbackBeans.size() == 1) {
4✔
188
      return ConditionOutcome.match(spec.message(matchMessage)
12✔
189
              .found("a single non-fallback bean '%s' from beans".formatted(nonFallbackBeans.get(0)))
6✔
190
              .items(ConditionMessage.Style.QUOTE, allBeans));
1✔
191
    }
192
    return ConditionOutcome.noMatch(spec.message().found("multiple beans").items(ConditionMessage.Style.QUOTE, allBeans));
9✔
193
  }
194

195
  private ConditionOutcome evaluateConditionalOnMissingBean(Spec<ConditionalOnMissingBean> spec, ConditionMessage matchMessage) {
196
    MatchResult matchResult = getMatchingBeans(spec);
4✔
197
    if (matchResult.isAnyMatched()) {
3✔
198
      String reason = createOnMissingBeanNoMatchReason(matchResult);
4✔
199
      return ConditionOutcome.noMatch(spec.message().because(reason));
6✔
200
    }
201
    return ConditionOutcome.match(spec.message(matchMessage).didNotFind("any beans").atAll());
8✔
202
  }
203

204
  protected final MatchResult getMatchingBeans(Spec<?> spec) {
205
    ConfigurableBeanFactory beanFactory = getSearchBeanFactory(spec);
4✔
206
    ClassLoader classLoader = spec.context.getClassLoader();
4✔
207
    boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
8✔
208
    Set<ResolvableType> parameterizedContainers = spec.parameterizedContainers;
3✔
209
    MatchResult result = new MatchResult();
4✔
210
    Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(beanFactory, considerHierarchy, spec.ignoredTypes, parameterizedContainers);
8✔
211
    for (var type : spec.types) {
11✔
212
      Map<String, BeanDefinition> typeMatchedDefinitions = getBeanDefinitionsForType(beanFactory,
7✔
213
              considerHierarchy, type, parameterizedContainers);
214
      Set<String> typeMatchedNames = matchedNamesFrom(typeMatchedDefinitions,
8✔
215
              (name, definition) -> !ScopedProxyUtils.isScopedTarget(name)
9✔
216
                      && isCandidate(beanFactory, name, definition, beansIgnoredByType));
5✔
217
      if (typeMatchedNames.isEmpty()) {
3✔
218
        result.recordUnmatchedType(type);
4✔
219
      }
220
      else {
221
        result.recordMatchedType(type, typeMatchedNames);
4✔
222
      }
223
    }
1✔
224
    for (String annotation : spec.annotations) {
11✔
225
      var annotationMatchedDefinitions = getBeanDefinitionsForAnnotation(classLoader, beanFactory, annotation, considerHierarchy);
7✔
226
      Set<String> annotationMatchedNames = matchedNamesFrom(annotationMatchedDefinitions,
8✔
227
              (name, definition) -> isCandidate(beanFactory, name, definition, beansIgnoredByType));
7✔
228
      if (annotationMatchedNames.isEmpty()) {
3✔
229
        result.recordUnmatchedAnnotation(annotation);
4✔
230
      }
231
      else {
232
        result.recordMatchedAnnotation(annotation, annotationMatchedNames);
4✔
233
      }
234
    }
1✔
235

236
    for (String beanName : spec.names) {
11✔
237
      if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {
10!
238
        result.recordMatchedName(beanName);
4✔
239
      }
240
      else {
241
        result.recordUnmatchedName(beanName);
3✔
242
      }
243
    }
1✔
244
    return result;
2✔
245
  }
246

247
  @SuppressWarnings("NullAway")
248
  private ConfigurableBeanFactory getSearchBeanFactory(Spec<?> spec) {
249
    ConfigurableBeanFactory beanFactory = spec.context.getBeanFactory();
4✔
250
    if (spec.getStrategy() == SearchStrategy.ANCESTORS) {
4✔
251
      BeanFactory parent = beanFactory.getParentBeanFactory();
3✔
252
      Assert.isInstanceOf(ConfigurableBeanFactory.class, parent,
4✔
253
              "Unable to use SearchStrategy.ANCESTORS");
254
      beanFactory = (ConfigurableBeanFactory) parent;
3✔
255
    }
256
    return beanFactory;
2✔
257
  }
258

259
  private Set<String> matchedNamesFrom(Map<String, BeanDefinition> namedDefinitions, BiPredicate<String, BeanDefinition> filter) {
260
    LinkedHashSet<String> matchedNames = new LinkedHashSet<>(namedDefinitions.size());
6✔
261
    for (Map.Entry<String, BeanDefinition> namedDefinition : namedDefinitions.entrySet()) {
11✔
262
      if (filter.test(namedDefinition.getKey(), namedDefinition.getValue())) {
9✔
263
        matchedNames.add(namedDefinition.getKey());
6✔
264
      }
265
    }
1✔
266
    return matchedNames;
2✔
267
  }
268

269
  private boolean isCandidate(ConfigurableBeanFactory beanFactory, String name, @Nullable BeanDefinition definition, Set<String> ignoredBeans) {
270
    if (ignoredBeans.contains(name)) {
4✔
271
      return false;
2✔
272
    }
273
    if (definition == null || (definition.isAutowireCandidate() && isDefaultCandidate(definition))) {
9✔
274
      return true;
2✔
275
    }
276
    if (ScopedProxyUtils.isScopedTarget(name)) {
3✔
277
      try {
278
        var originalDefinition = beanFactory.getBeanDefinition(ScopedProxyUtils.getOriginalBeanName(name));
5✔
279
        if (originalDefinition.isAutowireCandidate() && isDefaultCandidate(originalDefinition)) {
7!
280
          return true;
2✔
281
        }
282
      }
283
      catch (NoSuchBeanDefinitionException ignored) {
×
284
      }
×
285
    }
286
    return false;
2✔
287
  }
288

289
  private boolean isDefaultCandidate(BeanDefinition definition) {
290
    if (definition instanceof AbstractBeanDefinition abd) {
6!
291
      return abd.isDefaultCandidate();
3✔
292
    }
293
    return true;
×
294
  }
295

296
  private Set<String> getNamesOfBeansIgnoredByType(BeanFactory beanFactory, boolean considerHierarchy,
297
          Set<ResolvableType> ignoredTypes, Set<ResolvableType> parameterizedContainers) {
298
    Set<String> result = null;
2✔
299
    for (ResolvableType ignoredType : ignoredTypes) {
10✔
300
      Collection<String> ignoredNames = getBeanDefinitionsForType(beanFactory, considerHierarchy, ignoredType, parameterizedContainers)
6✔
301
              .keySet();
2✔
302
      result = addAll(result, ignoredNames);
4✔
303
    }
1✔
304
    return (result != null) ? result : Collections.emptySet();
6✔
305
  }
306

307
  private Map<String, BeanDefinition> getBeanDefinitionsForType(BeanFactory beanFactory,
308
          boolean considerHierarchy, ResolvableType type, Set<ResolvableType> parameterizedContainers) {
309
    Map<String, BeanDefinition> result = collectBeanDefinitionsForType(beanFactory, considerHierarchy, type,
8✔
310
            parameterizedContainers, null);
311
    return result != null ? result : Collections.emptyMap();
6✔
312
  }
313

314
  @Nullable
315
  @SuppressWarnings("NullAway")
316
  private Map<String, BeanDefinition> collectBeanDefinitionsForType(BeanFactory beanFactory, boolean considerHierarchy,
317
          ResolvableType type, Set<ResolvableType> parameterizedContainers, @Nullable Map<String, BeanDefinition> result) {
318
    result = putAll(result, beanFactory.getBeanNamesForType(type, true, false), beanFactory);
9✔
319
    for (ResolvableType parameterizedContainer : parameterizedContainers) {
10✔
320
      ResolvableType generic = ResolvableType.forClassWithGenerics(parameterizedContainer.resolve(), type);
10✔
321
      result = putAll(result, beanFactory.getBeanNamesForType(generic, true, false), beanFactory);
9✔
322
    }
1✔
323

324
    if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory) {
8!
325
      BeanFactory parent = hierarchicalBeanFactory.getParentBeanFactory();
3✔
326
      if (parent != null) {
2✔
327
        result = collectBeanDefinitionsForType(parent, considerHierarchy, type, parameterizedContainers, result);
8✔
328
      }
329
    }
330
    return result;
2✔
331
  }
332

333
  private Map<String, BeanDefinition> getBeanDefinitionsForAnnotation(@Nullable ClassLoader classLoader,
334
          ConfigurableBeanFactory beanFactory, String type, boolean considerHierarchy) throws LinkageError {
335
    Map<String, BeanDefinition> result = null;
2✔
336
    try {
337
      result = collectBeanDefinitionsForAnnotation(beanFactory,
7✔
338
              resolveAnnotationType(classLoader, type), considerHierarchy, result);
3✔
339
    }
340
    catch (ClassNotFoundException ex) {
×
341
      // Continue
342
    }
1✔
343
    return result != null ? result : Collections.emptyMap();
6✔
344
  }
345

346
  @SuppressWarnings("unchecked")
347
  private Class<? extends Annotation> resolveAnnotationType(@Nullable ClassLoader classLoader, String type) throws ClassNotFoundException {
348
    return (Class<? extends Annotation>) resolve(type, classLoader);
4✔
349
  }
350

351
  @Nullable
352
  private Map<String, BeanDefinition> collectBeanDefinitionsForAnnotation(BeanFactory beanFactory,
353
          Class<? extends Annotation> annotationType, boolean considerHierarchy, @Nullable Map<String, BeanDefinition> result) {
354
    result = putAll(result, getBeanNamesForAnnotation(beanFactory, annotationType), beanFactory);
8✔
355
    if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory hierarchical) {
8!
356
      BeanFactory parent = hierarchical.getParentBeanFactory();
3✔
357
      if (parent != null) {
2!
358
        result = collectBeanDefinitionsForAnnotation(parent, annotationType, considerHierarchy, result);
×
359
      }
360
    }
361
    return result;
2✔
362
  }
363

364
  private String[] getBeanNamesForAnnotation(BeanFactory beanFactory, Class<? extends Annotation> annotationType) {
365
    LinkedHashSet<String> foundBeanNames = new LinkedHashSet<>();
4✔
366
    for (String beanName : beanFactory.getBeanDefinitionNames()) {
17✔
367
      if (beanFactory instanceof ConfigurableBeanFactory cbf) {
6!
368
        BeanDefinition beanDefinition = cbf.getBeanDefinition(beanName);
4✔
369
        if (beanDefinition != null && beanDefinition.isAbstract()) {
5!
370
          continue;
×
371
        }
372
      }
373
      if (beanFactory.findAnnotationOnBean(beanName, annotationType, false).isPresent()) {
7✔
374
        foundBeanNames.add(beanName);
4✔
375
      }
376
    }
377
    if (beanFactory instanceof SingletonBeanRegistry singletonBeanRegistry) {
6!
378
      for (String beanName : singletonBeanRegistry.getSingletonNames()) {
17✔
379
        if (beanFactory.findAnnotationOnBean(beanName, annotationType).isPresent()) {
6!
380
          foundBeanNames.add(beanName);
×
381
        }
382
      }
383
    }
384
    return StringUtils.toStringArray(foundBeanNames);
3✔
385
  }
386

387
  private boolean containsBean(ConfigurableBeanFactory beanFactory, String beanName, boolean considerHierarchy) {
388
    if (considerHierarchy) {
2✔
389
      return beanFactory.containsBean(beanName);
4✔
390
    }
391
    return beanFactory.containsLocalBean(beanName);
4✔
392
  }
393

394
  private String createOnBeanNoMatchReason(MatchResult matchResult) {
395
    StringBuilder reason = new StringBuilder();
4✔
396
    appendMessageForNoMatches(reason, matchResult.unmatchedAnnotations, "annotated with");
6✔
397
    appendMessageForNoMatches(reason, matchResult.unmatchedTypes, "of type");
6✔
398
    appendMessageForNoMatches(reason, matchResult.unmatchedNames, "named");
6✔
399
    return reason.toString();
3✔
400
  }
401

402
  private void appendMessageForNoMatches(StringBuilder reason, Collection<String> unmatched, String description) {
403
    if (!unmatched.isEmpty()) {
3✔
404
      if (!reason.isEmpty()) {
3!
405
        reason.append(" and ");
×
406
      }
407
      reason.append("did not find any beans ");
4✔
408
      reason.append(description);
4✔
409
      reason.append(" ");
4✔
410
      reason.append(StringUtils.collectionToDelimitedString(unmatched, ", "));
6✔
411
    }
412
  }
1✔
413

414
  private String createOnMissingBeanNoMatchReason(MatchResult matchResult) {
415
    StringBuilder reason = new StringBuilder();
4✔
416
    appendMessageForMatches(reason, matchResult.matchedAnnotations, "annotated with");
6✔
417
    appendMessageForMatches(reason, matchResult.matchedTypes, "of type");
6✔
418
    if (!matchResult.matchedNames.isEmpty()) {
4✔
419
      if (!reason.isEmpty()) {
3!
420
        reason.append(" and ");
×
421
      }
422
      reason.append("found beans named ");
4✔
423
      reason.append(StringUtils.collectionToDelimitedString(matchResult.matchedNames, ", "));
7✔
424
    }
425
    return reason.toString();
3✔
426
  }
427

428
  private void appendMessageForMatches(StringBuilder reason, Map<String, Collection<String>> matches, String description) {
429
    if (!matches.isEmpty()) {
3✔
430
      for (Map.Entry<String, Collection<String>> entry : matches.entrySet()) {
11✔
431
        if (!reason.isEmpty()) {
3!
432
          reason.append(" and ");
×
433
        }
434
        reason.append("found beans ");
4✔
435
        reason.append(description);
4✔
436
        reason.append(" '");
4✔
437
        reason.append(entry.getKey());
6✔
438
        reason.append("' ");
4✔
439
        reason.append(StringUtils.collectionToDelimitedString(entry.getValue(), ", "));
8✔
440
      }
1✔
441
    }
442
  }
1✔
443

444
  private Map<String, BeanDefinition> getBeanDefinitions(ConfigurableBeanFactory beanFactory, Set<String> beanNames, boolean considerHierarchy) {
445
    HashMap<String, BeanDefinition> definitions = new HashMap<>(beanNames.size());
6✔
446
    for (String beanName : beanNames) {
10✔
447
      BeanDefinition beanDefinition = findBeanDefinition(beanFactory, beanName, considerHierarchy);
6✔
448
      definitions.put(beanName, beanDefinition);
5✔
449
    }
1✔
450
    return definitions;
2✔
451
  }
452

453
  private List<String> getPrimaryBeans(Map<String, BeanDefinition> beanDefinitions) {
454
    return getMatchingBeans(beanDefinitions, BeanDefinition::isPrimary);
5✔
455
  }
456

457
  private List<String> getNonFallbackBeans(Map<String, BeanDefinition> beanDefinitions) {
458
    return getMatchingBeans(beanDefinitions, Predicate.not(BeanDefinition::isFallback));
6✔
459
  }
460

461
  private List<String> getMatchingBeans(Map<String, BeanDefinition> beanDefinitions, Predicate<BeanDefinition> test) {
462
    ArrayList<String> matches = new ArrayList<>();
4✔
463
    for (Map.Entry<String, BeanDefinition> namedBeanDefinition : beanDefinitions.entrySet()) {
11✔
464
      if (test.test(namedBeanDefinition.getValue())) {
6✔
465
        matches.add(namedBeanDefinition.getKey());
6✔
466
      }
467
    }
1✔
468
    return matches;
2✔
469
  }
470

471
  @Nullable
472
  private BeanDefinition findBeanDefinition(ConfigurableBeanFactory beanFactory, String beanName, boolean considerHierarchy) {
473
    if (beanFactory.containsBeanDefinition(beanName)) {
4✔
474
      return beanFactory.getBeanDefinition(beanName);
4✔
475
    }
476
    if (considerHierarchy && beanFactory.getParentBeanFactory() instanceof ConfigurableBeanFactory configurable) {
11!
477
      return findBeanDefinition(configurable, beanName, true);
6✔
478
    }
479
    return null;
×
480
  }
481

482
  @Nullable
483
  private static Set<String> addAll(@Nullable Set<String> result, Collection<String> additional) {
484
    if (CollectionUtils.isEmpty(additional)) {
3!
485
      return result;
×
486
    }
487
    result = (result != null) ? result : new LinkedHashSet<>();
6!
488
    result.addAll(additional);
4✔
489
    return result;
2✔
490
  }
491

492
  @Nullable
493
  private static Map<String, BeanDefinition> putAll(@Nullable Map<String, BeanDefinition> result, String[] beanNames, BeanFactory beanFactory) {
494
    if (ObjectUtils.isEmpty(beanNames)) {
3✔
495
      return result;
2✔
496
    }
497
    if (result == null) {
2!
498
      result = new LinkedHashMap<>();
4✔
499
    }
500
    for (String beanName : beanNames) {
16✔
501
      if (beanFactory instanceof ConfigurableBeanFactory clbf) {
6!
502
        result.put(beanName, getBeanDefinition(beanName, clbf));
8✔
503
      }
504
      else {
505
        result.put(beanName, null);
×
506
      }
507
    }
508
    return result;
2✔
509
  }
510

511
  @Nullable
512
  private static BeanDefinition getBeanDefinition(String beanName, ConfigurableBeanFactory beanFactory) {
513
    try {
514
      return beanFactory.getBeanDefinition(beanName);
4✔
515
    }
516
    catch (NoSuchBeanDefinitionException ex) {
1✔
517
      if (BeanFactoryUtils.isFactoryDereference(beanName)) {
3✔
518
        return getBeanDefinition(BeanFactoryUtils.transformedBeanName(beanName), beanFactory);
5✔
519
      }
520
    }
521
    return null;
2✔
522
  }
523

524
  /**
525
   * A search specification extracted from the underlying annotation.
526
   *
527
   * @param <A> Annotation type
528
   */
529
  static class Spec<A extends Annotation> {
530

531
    public final ConditionContext context;
532

533
    public final Class<? extends Annotation> annotationType;
534

535
    public final Set<String> names;
536

537
    public final Set<ResolvableType> types;
538

539
    public final Set<String> annotations;
540

541
    public final Set<ResolvableType> ignoredTypes;
542

543
    @Nullable
544
    private final SearchStrategy strategy;
545

546
    public final Set<ResolvableType> parameterizedContainers;
547

548
    Spec(ConditionContext context, AnnotatedTypeMetadata metadata, MergedAnnotations annotations, Class<A> annotationType) {
2✔
549
      MultiValueMap<String, Object> attributes = annotations.stream(annotationType)
4✔
550
              .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
8✔
551
              .collect(MergedAnnotationCollectors.toMultiValueMap(Adapt.CLASS_TO_STRING));
4✔
552
      this.annotationType = annotationType;
3✔
553
      this.context = context;
3✔
554
      this.names = extract(attributes, "name");
11✔
555
      this.annotations = extract(attributes, "annotation");
11✔
556
      this.ignoredTypes = resolveWhenPossible(extract(attributes, "ignored", "ignoredType"));
17✔
557
      this.parameterizedContainers = resolveWhenPossible(extract(attributes, "parameterizedContainer"));
13✔
558
      this.strategy = annotations.get(annotationType).getValue("search", SearchStrategy.class);
9✔
559
      Set<ResolvableType> types = resolveWhenPossible(extractTypes(attributes));
6✔
560
      BeanTypeDeductionException deductionException = null;
2✔
561
      if (types.isEmpty() && this.names.isEmpty() && this.annotations.isEmpty()) {
11✔
562
        try {
563
          types = deducedBeanType(context, metadata);
5✔
564
        }
565
        catch (BeanTypeDeductionException ex) {
1✔
566
          deductionException = ex;
2✔
567
        }
1✔
568
      }
569
      this.types = types;
3✔
570
      validate(deductionException);
3✔
571
    }
1✔
572

573
    protected Set<String> extractTypes(MultiValueMap<String, Object> attributes) {
574
      return extract(attributes, "value", "type");
14✔
575
    }
576

577
    private Set<String> extract(MultiValueMap<String, Object> attributes, String... attributeNames) {
578
      if (attributes.isEmpty()) {
3!
579
        return Collections.emptySet();
×
580
      }
581
      var result = new LinkedHashSet<String>();
4✔
582
      for (String attributeName : attributeNames) {
16✔
583
        List<Object> values = attributes.getOrDefault(attributeName, Collections.emptyList());
6✔
584
        for (Object value : values) {
9✔
585
          if (value instanceof String[] stringArray) {
6✔
586
            merge(result, stringArray);
5✔
587
          }
588
          else if (value instanceof String string) {
6!
589
            merge(result, string);
9✔
590
          }
591
        }
1✔
592
      }
593
      return result.isEmpty() ? Collections.emptySet() : result;
7✔
594
    }
595

596
    private void merge(Set<String> result, String... additional) {
597
      Collections.addAll(result, additional);
4✔
598
    }
1✔
599

600
    private Set<ResolvableType> resolveWhenPossible(Set<String> classNames) {
601
      if (classNames.isEmpty()) {
3✔
602
        return Collections.emptySet();
2✔
603
      }
604
      Set<ResolvableType> resolved = new LinkedHashSet<>(classNames.size());
6✔
605
      for (String className : classNames) {
10✔
606
        try {
607
          Class<?> type = resolve(className, this.context.getClassLoader());
6✔
608
          resolved.add(ResolvableType.forRawClass(type));
5✔
609
        }
610
        catch (ClassNotFoundException | NoClassDefFoundError ex) {
1✔
611
          resolved.add(ResolvableType.NONE);
4✔
612
        }
1✔
613
      }
1✔
614
      return resolved;
2✔
615
    }
616

617
    protected void validate(@Nullable BeanTypeDeductionException ex) {
618
      if (!hasAtLeastOneElement(types, names, annotations)) {
20✔
619
        String message = getAnnotationName() + " did not specify a bean using type, name or annotation";
4✔
620
        if (ex == null) {
2!
621
          throw new IllegalStateException(message);
×
622
        }
623
        throw new IllegalStateException(message + " and the attempt to deduce the bean's type failed", ex);
7✔
624
      }
625
    }
1✔
626

627
    private boolean hasAtLeastOneElement(Set<?>... sets) {
628
      for (Set<?> set : sets) {
16✔
629
        if (!set.isEmpty()) {
3✔
630
          return true;
2✔
631
        }
632
      }
633
      return false;
2✔
634
    }
635

636
    protected final String getAnnotationName() {
637
      return "@" + ClassUtils.getShortName(this.annotationType);
5✔
638
    }
639

640
    private Set<ResolvableType> deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) {
641
      if (metadata instanceof MethodMetadata && metadata.isAnnotated(Component.class)) {
7!
642
        return deducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata);
6✔
643
      }
644
      return Collections.emptySet();
2✔
645
    }
646

647
    private Set<ResolvableType> deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) {
648
      try {
649
        return Set.of(getReturnType(context, metadata));
6✔
650
      }
651
      catch (Throwable ex) {
1✔
652
        throw new BeanTypeDeductionException(metadata.getDeclaringClassName(), metadata.getMethodName(), ex);
9✔
653
      }
654
    }
655

656
    private ResolvableType getReturnType(ConditionContext context, MethodMetadata metadata)
657
            throws ClassNotFoundException, LinkageError {
658
      // Safe to load at this point since we are in the REGISTER_BEAN phase
659
      ClassLoader classLoader = context.getClassLoader();
3✔
660
      ResolvableType returnType = getMethodReturnType(metadata, classLoader);
5✔
661
      if (isParameterizedContainer(returnType.resolve())) {
5✔
662
        returnType = returnType.getGeneric();
5✔
663
      }
664
      return returnType;
2✔
665
    }
666

667
    private boolean isParameterizedContainer(@Nullable Class<?> type) {
668
      return (type != null) && this.parameterizedContainers.stream()
7!
669
              .map(ResolvableType::resolve)
3✔
670
              .anyMatch((container) -> container != null && container.isAssignableFrom(type));
15!
671
    }
672

673
    private ResolvableType getMethodReturnType(MethodMetadata metadata, @Nullable ClassLoader classLoader) throws ClassNotFoundException, LinkageError {
674
      Class<?> declaringClass = resolve(metadata.getDeclaringClassName(), classLoader);
5✔
675
      Method beanMethod = findBeanMethod(declaringClass, metadata.getMethodName());
6✔
676
      return ResolvableType.forReturnType(beanMethod);
3✔
677
    }
678

679
    private Method findBeanMethod(Class<?> declaringClass, String methodName) {
680
      Method method = ReflectionUtils.findMethod(declaringClass, methodName);
4✔
681
      if (isBeanMethod(method)) {
4!
682
        return method;
2✔
683
      }
684
      Method[] candidates = ReflectionUtils.getAllDeclaredMethods(declaringClass);
×
685
      for (Method candidate : candidates) {
×
686
        if (candidate.getName().equals(methodName) && isBeanMethod(candidate)) {
×
687
          return candidate;
×
688
        }
689
      }
690
      throw new IllegalStateException("Unable to find bean method " + methodName);
×
691
    }
692

693
    @Contract("null -> false")
694
    private boolean isBeanMethod(@Nullable Method method) {
695
      return method != null && MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
7!
696
              .isPresent(Component.class);
4!
697
    }
698

699
    private SearchStrategy getStrategy() {
700
      return (this.strategy != null) ? this.strategy : SearchStrategy.ALL;
7!
701
    }
702

703
    private ConditionMessage.Builder message() {
704
      return ConditionMessage.forCondition(this.annotationType, this);
10✔
705
    }
706

707
    private ConditionMessage.Builder message(ConditionMessage message) {
708
      return message.andCondition(this.annotationType, this);
11✔
709
    }
710

711
    @Override
712
    public String toString() {
713
      boolean hasNames = !this.names.isEmpty();
8✔
714
      boolean hasTypes = !this.types.isEmpty();
8✔
715
      boolean hasIgnoredTypes = !this.ignoredTypes.isEmpty();
8✔
716
      StringBuilder string = new StringBuilder();
4✔
717
      string.append("(");
4✔
718
      if (hasNames) {
2✔
719
        string.append("names: ");
4✔
720
        string.append(StringUtils.collectionToCommaDelimitedString(this.names));
6✔
721
        string.append(hasTypes ? " " : "; ");
8✔
722
      }
723
      if (hasTypes) {
2✔
724
        string.append("types: ");
4✔
725
        string.append(StringUtils.collectionToCommaDelimitedString(this.types));
6✔
726
        string.append(hasIgnoredTypes ? " " : "; ");
8✔
727
      }
728
      if (hasIgnoredTypes) {
2✔
729
        string.append("ignored: ");
4✔
730
        string.append(StringUtils.collectionToCommaDelimitedString(this.ignoredTypes));
6✔
731
        string.append("; ");
4✔
732
      }
733

734
      if (strategy != null) {
3!
735
        string.append("SearchStrategy: ");
4✔
736
        string.append(strategy.toString().toLowerCase(Locale.ROOT));
8✔
737
        string.append(")");
4✔
738
      }
739
      return string.toString();
3✔
740
    }
741

742
  }
743

744
  /**
745
   * Specialized {@link Spec specification} for
746
   * {@link ConditionalOnSingleCandidate @ConditionalOnSingleCandidate}.
747
   */
748
  private static class SingleCandidateSpec extends Spec<ConditionalOnSingleCandidate> {
749

750
    private static final Collection<String> FILTERED_TYPES = Arrays.asList("", Object.class.getName());
14✔
751

752
    SingleCandidateSpec(ConditionContext context, AnnotatedTypeMetadata metadata, MergedAnnotations annotations) {
753
      super(context, metadata, annotations, ConditionalOnSingleCandidate.class);
6✔
754
    }
1✔
755

756
    @Override
757
    protected Set<String> extractTypes(MultiValueMap<String, Object> attributes) {
758
      Set<String> types = super.extractTypes(attributes);
4✔
759
      types.removeAll(FILTERED_TYPES);
4✔
760
      return types;
2✔
761
    }
762

763
    @Override
764
    protected void validate(@Nullable BeanTypeDeductionException ex) {
765
      if (types.size() != 1) {
5✔
766
        throw new IllegalArgumentException("%s annotations must specify only one type (got %s)"
8✔
767
                .formatted(getAnnotationName(), StringUtils.collectionToCommaDelimitedString(types)));
11✔
768
      }
769
    }
1✔
770

771
  }
772

773
  /**
774
   * Results collected during the condition evaluation.
775
   */
776
  static final class MatchResult {
2✔
777

778
    private final HashSet<String> namesOfAllMatches = new HashSet<>();
5✔
779
    private final HashMap<String, Collection<String>> matchedTypes = new HashMap<>();
5✔
780
    private final HashMap<String, Collection<String>> matchedAnnotations = new HashMap<>();
5✔
781

782
    private final ArrayList<String> matchedNames = new ArrayList<>();
5✔
783
    private final ArrayList<String> unmatchedNames = new ArrayList<>();
5✔
784
    private final ArrayList<String> unmatchedTypes = new ArrayList<>();
5✔
785
    private final ArrayList<String> unmatchedAnnotations = new ArrayList<>();
6✔
786

787
    private void recordMatchedName(String name) {
788
      this.matchedNames.add(name);
5✔
789
      this.namesOfAllMatches.add(name);
5✔
790
    }
1✔
791

792
    private void recordUnmatchedName(String name) {
793
      this.unmatchedNames.add(name);
5✔
794
    }
1✔
795

796
    private void recordMatchedAnnotation(String annotation, Collection<String> matchingNames) {
797
      this.matchedAnnotations.put(annotation, matchingNames);
6✔
798
      this.namesOfAllMatches.addAll(matchingNames);
5✔
799
    }
1✔
800

801
    private void recordUnmatchedAnnotation(String annotation) {
802
      this.unmatchedAnnotations.add(annotation);
5✔
803
    }
1✔
804

805
    private void recordMatchedType(ResolvableType type, Collection<String> matchingNames) {
806
      this.matchedTypes.put(type.toString(), matchingNames);
7✔
807
      this.namesOfAllMatches.addAll(matchingNames);
5✔
808
    }
1✔
809

810
    private void recordUnmatchedType(ResolvableType type) {
811
      this.unmatchedTypes.add(type.toString());
6✔
812
    }
1✔
813

814
    boolean isNoneMatch() {
815
      return !this.unmatchedAnnotations.isEmpty()
7✔
816
              || !this.unmatchedNames.isEmpty()
4✔
817
              || !this.unmatchedTypes.isEmpty();
5✔
818
    }
819

820
    boolean isAnyMatched() {
821
      return (!this.matchedAnnotations.isEmpty())
7✔
822
              || (!this.matchedNames.isEmpty())
4✔
823
              || (!this.matchedTypes.isEmpty());
5✔
824
    }
825

826
  }
827

828
  /**
829
   * Exception thrown when the bean type cannot be deduced.
830
   */
831
  static final class BeanTypeDeductionException extends RuntimeException {
832

833
    @Serial
834
    private static final long serialVersionUID = 1L;
835

836
    private BeanTypeDeductionException(String className, String beanMethodName, Throwable cause) {
837
      super("Failed to deduce bean type for %s.%s".formatted(className, beanMethodName), cause);
15✔
838
    }
1✔
839

840
  }
841

842
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc