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

TAKETODAY / today-infrastructure / 20171237335

12 Dec 2025 03:17PM UTC coverage: 84.404% (+0.003%) from 84.401%
20171237335

push

github

TAKETODAY
:recycle: 重构资源处理器配置属性

- 将静态资源路径模式属性从 WebMvcProperties 移动到 WebProperties
- 将 WebJars 路径模式属性从 WebMvcProperties 移动到 WebProperties
- 更新 WebMvcAutoConfiguration 中的资源配置逻辑以使用新的属性位置
- 修改测试类中的属性路径引用以匹配新的配置结构
- 确保默认资源映射功能正常工作并正确加载静态资源和 WebJars
- 更新测试用例中的属性键名以反映新的命名空间变更

61765 of 78231 branches covered (78.95%)

Branch coverage included in aggregate %.

145774 of 167657 relevant lines covered (86.95%)

3.71 hits per line

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

90.21
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, @Nullable BeanDefinition> getBeanDefinitions(ConfigurableBeanFactory beanFactory,
445
          Set<String> beanNames, boolean considerHierarchy) {
446
    Map<String, @Nullable BeanDefinition> definitions = new HashMap<>(beanNames.size());
6✔
447
    for (String beanName : beanNames) {
10✔
448
      BeanDefinition beanDefinition = findBeanDefinition(beanFactory, beanName, considerHierarchy);
6✔
449
      definitions.put(beanName, beanDefinition);
5✔
450
    }
1✔
451
    return definitions;
2✔
452
  }
453

454
  private List<String> getPrimaryBeans(Map<String, @Nullable BeanDefinition> beanDefinitions) {
455
    return getMatchingBeans(beanDefinitions, this::isPrimary);
6✔
456
  }
457

458
  private boolean isPrimary(@Nullable BeanDefinition beanDefinition) {
459
    return beanDefinition != null && beanDefinition.isPrimary();
9✔
460
  }
461

462
  private List<String> getNonFallbackBeans(Map<String, @Nullable BeanDefinition> beanDefinitions) {
463
    return getMatchingBeans(beanDefinitions, this::isNotFallback);
6✔
464
  }
465

466
  private boolean isNotFallback(@Nullable BeanDefinition beanDefinition) {
467
    return beanDefinition == null || !beanDefinition.isFallback();
9✔
468
  }
469

470
  private List<String> getMatchingBeans(Map<String, @Nullable BeanDefinition> beanDefinitions, Predicate<@Nullable BeanDefinition> test) {
471
    ArrayList<String> matches = new ArrayList<>();
4✔
472
    for (Map.Entry<String, @Nullable BeanDefinition> namedBeanDefinition : beanDefinitions.entrySet()) {
11✔
473
      if (test.test(namedBeanDefinition.getValue())) {
6✔
474
        matches.add(namedBeanDefinition.getKey());
6✔
475
      }
476
    }
1✔
477
    return matches;
2✔
478
  }
479

480
  @Nullable
481
  private BeanDefinition findBeanDefinition(ConfigurableBeanFactory beanFactory, String beanName, boolean considerHierarchy) {
482
    if (beanFactory.containsBeanDefinition(beanName)) {
4✔
483
      return beanFactory.getBeanDefinition(beanName);
4✔
484
    }
485
    if (considerHierarchy && beanFactory.getParentBeanFactory() instanceof ConfigurableBeanFactory configurable) {
11!
486
      return findBeanDefinition(configurable, beanName, true);
6✔
487
    }
488
    return null;
2✔
489
  }
490

491
  @Nullable
492
  private static Set<String> addAll(@Nullable Set<String> result, Collection<String> additional) {
493
    if (CollectionUtils.isEmpty(additional)) {
3!
494
      return result;
×
495
    }
496
    result = (result != null) ? result : new LinkedHashSet<>();
6!
497
    result.addAll(additional);
4✔
498
    return result;
2✔
499
  }
500

501
  @Nullable
502
  private static Map<String, BeanDefinition> putAll(@Nullable Map<String, BeanDefinition> result, String[] beanNames, BeanFactory beanFactory) {
503
    if (ObjectUtils.isEmpty(beanNames)) {
3✔
504
      return result;
2✔
505
    }
506
    if (result == null) {
2!
507
      result = new LinkedHashMap<>();
4✔
508
    }
509
    for (String beanName : beanNames) {
16✔
510
      if (beanFactory instanceof ConfigurableBeanFactory clbf) {
6!
511
        result.put(beanName, getBeanDefinition(beanName, clbf));
8✔
512
      }
513
      else {
514
        result.put(beanName, null);
×
515
      }
516
    }
517
    return result;
2✔
518
  }
519

520
  @Nullable
521
  private static BeanDefinition getBeanDefinition(String beanName, ConfigurableBeanFactory beanFactory) {
522
    try {
523
      return beanFactory.getBeanDefinition(beanName);
4✔
524
    }
525
    catch (NoSuchBeanDefinitionException ex) {
1✔
526
      if (BeanFactoryUtils.isFactoryDereference(beanName)) {
3✔
527
        return getBeanDefinition(BeanFactoryUtils.transformedBeanName(beanName), beanFactory);
5✔
528
      }
529
    }
530
    return null;
2✔
531
  }
532

533
  /**
534
   * A search specification extracted from the underlying annotation.
535
   *
536
   * @param <A> Annotation type
537
   */
538
  static class Spec<A extends Annotation> {
539

540
    public final ConditionContext context;
541

542
    public final Class<? extends Annotation> annotationType;
543

544
    public final Set<String> names;
545

546
    public final Set<ResolvableType> types;
547

548
    public final Set<String> annotations;
549

550
    public final Set<ResolvableType> ignoredTypes;
551

552
    @Nullable
553
    private final SearchStrategy strategy;
554

555
    public final Set<ResolvableType> parameterizedContainers;
556

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

582
    protected Set<String> extractTypes(@Nullable MultiValueMap<String, @Nullable Object> attributes) {
583
      return extract(attributes, "value", "type");
14✔
584
    }
585

586
    private Set<String> extract(@Nullable MultiValueMap<String, @Nullable Object> attributes, String... attributeNames) {
587
      if (CollectionUtils.isEmpty(attributes)) {
3!
588
        return Collections.emptySet();
×
589
      }
590
      var result = new LinkedHashSet<String>();
4✔
591
      for (String attributeName : attributeNames) {
16✔
592
        List<@Nullable Object> values = attributes.getOrDefault(attributeName, Collections.emptyList());
6✔
593
        for (Object value : values) {
9✔
594
          if (value instanceof String[] stringArray) {
6✔
595
            merge(result, stringArray);
5✔
596
          }
597
          else if (value instanceof String string) {
6!
598
            merge(result, string);
9✔
599
          }
600
        }
1✔
601
      }
602
      return result.isEmpty() ? Collections.emptySet() : result;
7✔
603
    }
604

605
    private void merge(Set<String> result, String... additional) {
606
      Collections.addAll(result, additional);
4✔
607
    }
1✔
608

609
    private Set<ResolvableType> resolveWhenPossible(Set<String> classNames) {
610
      if (classNames.isEmpty()) {
3✔
611
        return Collections.emptySet();
2✔
612
      }
613
      Set<ResolvableType> resolved = new LinkedHashSet<>(classNames.size());
6✔
614
      for (String className : classNames) {
10✔
615
        try {
616
          Class<?> type = resolve(className, this.context.getClassLoader());
6✔
617
          resolved.add(ResolvableType.forRawClass(type));
5✔
618
        }
619
        catch (ClassNotFoundException | NoClassDefFoundError ex) {
1✔
620
          resolved.add(ResolvableType.NONE);
4✔
621
        }
1✔
622
      }
1✔
623
      return resolved;
2✔
624
    }
625

626
    protected void validate(@Nullable BeanTypeDeductionException ex) {
627
      if (!hasAtLeastOneElement(types, names, annotations)) {
20✔
628
        String message = getAnnotationName() + " did not specify a bean using type, name or annotation";
4✔
629
        if (ex == null) {
2!
630
          throw new IllegalStateException(message);
×
631
        }
632
        throw new IllegalStateException(message + " and the attempt to deduce the bean's type failed", ex);
7✔
633
      }
634
    }
1✔
635

636
    private boolean hasAtLeastOneElement(Set<?>... sets) {
637
      for (Set<?> set : sets) {
16✔
638
        if (!set.isEmpty()) {
3✔
639
          return true;
2✔
640
        }
641
      }
642
      return false;
2✔
643
    }
644

645
    protected final String getAnnotationName() {
646
      return "@" + ClassUtils.getShortName(this.annotationType);
5✔
647
    }
648

649
    private Set<ResolvableType> deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) {
650
      if (metadata instanceof MethodMetadata && metadata.isAnnotated(Component.class)) {
7!
651
        return deducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata);
6✔
652
      }
653
      return Collections.emptySet();
2✔
654
    }
655

656
    private Set<ResolvableType> deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) {
657
      try {
658
        return Set.of(getReturnType(context, metadata));
6✔
659
      }
660
      catch (Throwable ex) {
1✔
661
        throw new BeanTypeDeductionException(metadata.getDeclaringClassName(), metadata.getMethodName(), ex);
9✔
662
      }
663
    }
664

665
    private ResolvableType getReturnType(ConditionContext context, MethodMetadata metadata)
666
            throws ClassNotFoundException, LinkageError {
667
      // Safe to load at this point since we are in the REGISTER_BEAN phase
668
      ClassLoader classLoader = context.getClassLoader();
3✔
669
      ResolvableType returnType = getMethodReturnType(metadata, classLoader);
5✔
670
      if (isParameterizedContainer(returnType.resolve())) {
5✔
671
        returnType = returnType.getGeneric();
5✔
672
      }
673
      return returnType;
2✔
674
    }
675

676
    private boolean isParameterizedContainer(@Nullable Class<?> type) {
677
      return (type != null) && this.parameterizedContainers.stream()
7!
678
              .map(ResolvableType::resolve)
3✔
679
              .anyMatch((container) -> container != null && container.isAssignableFrom(type));
15!
680
    }
681

682
    private ResolvableType getMethodReturnType(MethodMetadata metadata, @Nullable ClassLoader classLoader) throws ClassNotFoundException, LinkageError {
683
      Class<?> declaringClass = resolve(metadata.getDeclaringClassName(), classLoader);
5✔
684
      Method beanMethod = findBeanMethod(declaringClass, metadata.getMethodName());
6✔
685
      return ResolvableType.forReturnType(beanMethod);
3✔
686
    }
687

688
    private Method findBeanMethod(Class<?> declaringClass, String methodName) {
689
      Method method = ReflectionUtils.findMethod(declaringClass, methodName);
4✔
690
      if (isBeanMethod(method)) {
4!
691
        return method;
2✔
692
      }
693
      Method[] candidates = ReflectionUtils.getAllDeclaredMethods(declaringClass);
×
694
      for (Method candidate : candidates) {
×
695
        if (candidate.getName().equals(methodName) && isBeanMethod(candidate)) {
×
696
          return candidate;
×
697
        }
698
      }
699
      throw new IllegalStateException("Unable to find bean method " + methodName);
×
700
    }
701

702
    @Contract("null -> false")
703
    private boolean isBeanMethod(@Nullable Method method) {
704
      return method != null && MergedAnnotations.from(method, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
7!
705
              .isPresent(Component.class);
4!
706
    }
707

708
    private SearchStrategy getStrategy() {
709
      return (this.strategy != null) ? this.strategy : SearchStrategy.ALL;
7!
710
    }
711

712
    private ConditionMessage.Builder message() {
713
      return ConditionMessage.forCondition(this.annotationType, this);
10✔
714
    }
715

716
    private ConditionMessage.Builder message(ConditionMessage message) {
717
      return message.andCondition(this.annotationType, this);
11✔
718
    }
719

720
    @Override
721
    public String toString() {
722
      boolean hasNames = !this.names.isEmpty();
8✔
723
      boolean hasTypes = !this.types.isEmpty();
8✔
724
      boolean hasIgnoredTypes = !this.ignoredTypes.isEmpty();
8✔
725
      StringBuilder string = new StringBuilder();
4✔
726
      string.append("(");
4✔
727
      if (hasNames) {
2✔
728
        string.append("names: ");
4✔
729
        string.append(StringUtils.collectionToCommaDelimitedString(this.names));
6✔
730
        string.append(hasTypes ? " " : "; ");
8✔
731
      }
732
      if (hasTypes) {
2✔
733
        string.append("types: ");
4✔
734
        string.append(StringUtils.collectionToCommaDelimitedString(this.types));
6✔
735
        string.append(hasIgnoredTypes ? " " : "; ");
8✔
736
      }
737
      if (hasIgnoredTypes) {
2✔
738
        string.append("ignored: ");
4✔
739
        string.append(StringUtils.collectionToCommaDelimitedString(this.ignoredTypes));
6✔
740
        string.append("; ");
4✔
741
      }
742

743
      if (strategy != null) {
3!
744
        string.append("SearchStrategy: ");
4✔
745
        string.append(strategy.toString().toLowerCase(Locale.ROOT));
8✔
746
        string.append(")");
4✔
747
      }
748
      return string.toString();
3✔
749
    }
750

751
  }
752

753
  /**
754
   * Specialized {@link Spec specification} for
755
   * {@link ConditionalOnSingleCandidate @ConditionalOnSingleCandidate}.
756
   */
757
  private static class SingleCandidateSpec extends Spec<ConditionalOnSingleCandidate> {
758

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

761
    SingleCandidateSpec(ConditionContext context, AnnotatedTypeMetadata metadata, MergedAnnotations annotations) {
762
      super(context, metadata, annotations, ConditionalOnSingleCandidate.class);
6✔
763
    }
1✔
764

765
    @Override
766
    protected Set<String> extractTypes(@Nullable MultiValueMap<String, @Nullable Object> attributes) {
767
      Set<String> types = super.extractTypes(attributes);
4✔
768
      types.removeAll(FILTERED_TYPES);
4✔
769
      return types;
2✔
770
    }
771

772
    @Override
773
    protected void validate(@Nullable BeanTypeDeductionException ex) {
774
      if (types.size() != 1) {
5✔
775
        throw new IllegalArgumentException("%s annotations must specify only one type (got %s)"
8✔
776
                .formatted(getAnnotationName(), StringUtils.collectionToCommaDelimitedString(types)));
11✔
777
      }
778
    }
1✔
779

780
  }
781

782
  /**
783
   * Results collected during the condition evaluation.
784
   */
785
  static final class MatchResult {
2✔
786

787
    private final HashSet<String> namesOfAllMatches = new HashSet<>();
5✔
788
    private final HashMap<String, Collection<String>> matchedTypes = new HashMap<>();
5✔
789
    private final HashMap<String, Collection<String>> matchedAnnotations = new HashMap<>();
5✔
790

791
    private final ArrayList<String> matchedNames = new ArrayList<>();
5✔
792
    private final ArrayList<String> unmatchedNames = new ArrayList<>();
5✔
793
    private final ArrayList<String> unmatchedTypes = new ArrayList<>();
5✔
794
    private final ArrayList<String> unmatchedAnnotations = new ArrayList<>();
6✔
795

796
    private void recordMatchedName(String name) {
797
      this.matchedNames.add(name);
5✔
798
      this.namesOfAllMatches.add(name);
5✔
799
    }
1✔
800

801
    private void recordUnmatchedName(String name) {
802
      this.unmatchedNames.add(name);
5✔
803
    }
1✔
804

805
    private void recordMatchedAnnotation(String annotation, Collection<String> matchingNames) {
806
      this.matchedAnnotations.put(annotation, matchingNames);
6✔
807
      this.namesOfAllMatches.addAll(matchingNames);
5✔
808
    }
1✔
809

810
    private void recordUnmatchedAnnotation(String annotation) {
811
      this.unmatchedAnnotations.add(annotation);
5✔
812
    }
1✔
813

814
    private void recordMatchedType(ResolvableType type, Collection<String> matchingNames) {
815
      this.matchedTypes.put(type.toString(), matchingNames);
7✔
816
      this.namesOfAllMatches.addAll(matchingNames);
5✔
817
    }
1✔
818

819
    private void recordUnmatchedType(ResolvableType type) {
820
      this.unmatchedTypes.add(type.toString());
6✔
821
    }
1✔
822

823
    boolean isNoneMatch() {
824
      return !this.unmatchedAnnotations.isEmpty()
7✔
825
              || !this.unmatchedNames.isEmpty()
4✔
826
              || !this.unmatchedTypes.isEmpty();
5✔
827
    }
828

829
    boolean isAnyMatched() {
830
      return (!this.matchedAnnotations.isEmpty())
7✔
831
              || (!this.matchedNames.isEmpty())
4✔
832
              || (!this.matchedTypes.isEmpty());
5✔
833
    }
834

835
  }
836

837
  /**
838
   * Exception thrown when the bean type cannot be deduced.
839
   */
840
  static final class BeanTypeDeductionException extends RuntimeException {
841

842
    @Serial
843
    private static final long serialVersionUID = 1L;
844

845
    private BeanTypeDeductionException(String className, String beanMethodName, Throwable cause) {
846
      super("Failed to deduce bean type for %s.%s".formatted(className, beanMethodName), cause);
15✔
847
    }
1✔
848

849
  }
850

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

© 2026 Coveralls, Inc