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

TAKETODAY / today-infrastructure / 19225086188

10 Nov 2025 08:15AM UTC coverage: 84.13%. Remained the same
19225086188

push

github

TAKETODAY
:white_check_mark:

61359 of 78029 branches covered (78.64%)

Branch coverage included in aggregate %.

145112 of 167391 relevant lines covered (86.69%)

3.69 hits per line

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

91.47
today-context/src/main/java/infra/context/annotation/ClassPathScanningCandidateComponentProvider.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.annotation;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.io.IOException;
23
import java.lang.annotation.Annotation;
24
import java.util.ArrayList;
25
import java.util.HashSet;
26
import java.util.LinkedHashSet;
27
import java.util.Set;
28
import java.util.function.Predicate;
29

30
import infra.beans.factory.BeanDefinitionStoreException;
31
import infra.beans.factory.annotation.AnnotatedBeanDefinition;
32
import infra.beans.factory.annotation.Lookup;
33
import infra.beans.factory.support.BeanDefinitionRegistry;
34
import infra.bytecode.ClassReader;
35
import infra.context.index.CandidateComponentsIndex;
36
import infra.context.index.CandidateComponentsIndexLoader;
37
import infra.core.annotation.AnnotationUtils;
38
import infra.core.env.Environment;
39
import infra.core.env.EnvironmentCapable;
40
import infra.core.env.StandardEnvironment;
41
import infra.core.io.PathMatchingPatternResourceLoader;
42
import infra.core.io.PatternResourceLoader;
43
import infra.core.io.ResourceLoader;
44
import infra.core.type.AnnotationMetadata;
45
import infra.core.type.classreading.MetadataReader;
46
import infra.core.type.classreading.MetadataReaderFactory;
47
import infra.core.type.filter.AnnotationTypeFilter;
48
import infra.core.type.filter.AssignableTypeFilter;
49
import infra.core.type.filter.TypeFilter;
50
import infra.lang.Assert;
51
import infra.stereotype.Component;
52
import infra.stereotype.Controller;
53
import infra.stereotype.Indexed;
54
import infra.stereotype.Repository;
55
import infra.stereotype.Service;
56
import infra.util.ClassUtils;
57

58
/**
59
 * A component provider that scans for candidate components starting from a
60
 * specified base package. Can use the {@linkplain CandidateComponentsIndex component
61
 * index}, if it is available, and scans the classpath otherwise.
62
 *
63
 * <p>Candidate components are identified by applying exclude and include filters.
64
 * {@link AnnotationTypeFilter} and {@link AssignableTypeFilter} include filters
65
 * for an annotation/target-type that is annotated with {@link Indexed} are
66
 * supported: if any other include filter is specified, the index is ignored and
67
 * classpath scanning is used instead.
68
 *
69
 * <p>This implementation is based on framework 's
70
 * {@link MetadataReader MetadataReader}
71
 * facility, backed by an ASM {@link ClassReader ClassReader}.
72
 *
73
 * @author Mark Fisher
74
 * @author Juergen Hoeller
75
 * @author Ramnivas Laddad
76
 * @author Chris Beams
77
 * @author Stephane Nicoll
78
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
79
 * @see MetadataReaderFactory
80
 * @see AnnotationMetadata
81
 * @see ScannedGenericBeanDefinition
82
 * @see CandidateComponentsIndex
83
 * @since 4.0 2021/12/9 21:33
84
 */
85
public class ClassPathScanningCandidateComponentProvider extends ClassPathScanningComponentProvider implements EnvironmentCapable {
86

87
  private final ArrayList<TypeFilter> includeFilters = new ArrayList<>();
10✔
88

89
  private final ArrayList<TypeFilter> excludeFilters = new ArrayList<>();
10✔
90

91
  @Nullable
92
  private Environment environment;
93

94
  @Nullable
95
  private ConditionEvaluator conditionEvaluator;
96

97
  @Nullable
98
  private CandidateComponentsIndex componentsIndex;
99

100
  private Predicate<AnnotationMetadata> candidateComponentPredicate = this::isCandidateComponent;
8✔
101

102
  public ClassPathScanningCandidateComponentProvider() { }
3✔
103

104
  /**
105
   * Create a ClassPathScanningCandidateComponentProvider with a {@link StandardEnvironment}.
106
   *
107
   * @param useDefaultFilters whether to register the default filters for the
108
   * {@link Component @Component}, {@link Repository @Repository},
109
   * {@link Service @Service}, and {@link Controller @Controller}
110
   * stereotype annotations
111
   * @see #registerDefaultFilters()
112
   */
113
  public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) {
114
    this(useDefaultFilters, new StandardEnvironment());
6✔
115
  }
1✔
116

117
  /**
118
   * Create a ClassPathScanningCandidateComponentProvider with the given {@link Environment}.
119
   *
120
   * @param useDefaultFilters whether to register the default filters for the
121
   * {@link Component @Component}, {@link Repository @Repository},
122
   * {@link Service @Service}, and {@link Controller @Controller}
123
   * stereotype annotations
124
   * @param environment the Environment to use
125
   * @see #registerDefaultFilters()
126
   */
127
  public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) {
2✔
128
    if (useDefaultFilters) {
2✔
129
      registerDefaultFilters();
2✔
130
    }
131
    setEnvironment(environment);
3✔
132
    setResourceLoader(null);
3✔
133
  }
1✔
134

135
  /**
136
   * Add an include type filter to the <i>end</i> of the inclusion list.
137
   */
138
  public void addIncludeFilter(TypeFilter includeFilter) {
139
    this.includeFilters.add(includeFilter);
5✔
140
  }
1✔
141

142
  /**
143
   * Add an exclude type filter to the <i>front</i> of the exclusion list.
144
   */
145
  public void addExcludeFilter(TypeFilter excludeFilter) {
146
    this.excludeFilters.add(0, excludeFilter);
5✔
147
  }
1✔
148

149
  /**
150
   * Reset the configured type filters.
151
   *
152
   * @param useDefaultFilters whether to re-register the default filters for
153
   * the {@link Component @Component}, {@link Repository @Repository},
154
   * {@link Service @Service}, and {@link Controller @Controller}
155
   * stereotype annotations
156
   * @see #registerDefaultFilters()
157
   */
158
  public void resetFilters(boolean useDefaultFilters) {
159
    this.includeFilters.clear();
3✔
160
    this.excludeFilters.clear();
3✔
161
    if (useDefaultFilters) {
2!
162
      registerDefaultFilters();
2✔
163
    }
164
  }
1✔
165

166
  /**
167
   * Register the default filter for {@link Component @Component}.
168
   * <p>This will implicitly register all annotations that have the
169
   * {@link Component @Component} meta-annotation including the
170
   * {@link Repository @Repository}, {@link Service @Service}, and
171
   * {@link Controller @Controller} stereotype annotations.
172
   * <p>Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and
173
   * JSR-330's {@link jakarta.inject.Named} annotations (as well as their
174
   * pre-Jakarta {@code javax.annotation.ManagedBean} and {@code javax.inject.Named}
175
   * equivalents), if available.
176
   */
177
  protected void registerDefaultFilters() {
178
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
8✔
179
    ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
3✔
180
    try {
181
      this.includeFilters.add(new AnnotationTypeFilter(
6✔
182
              ClassUtils.forName("jakarta.annotation.ManagedBean", cl), false));
×
183
      logger.trace("JSR-250 'jakarta.annotation.ManagedBean' found and supported for component scanning");
×
184
    }
185
    catch (ClassNotFoundException ex) {
1✔
186
      // JSR-250 1.1 API (as included in Jakarta EE) not available - simply skip.
187
    }
×
188
    try {
189
      this.includeFilters.add(new AnnotationTypeFilter(
8✔
190
              ClassUtils.forName("javax.annotation.ManagedBean", cl), false));
3✔
191
      logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
4✔
192
    }
193
    catch (ClassNotFoundException ex) {
1✔
194
      // JSR-250 1.1 API not available - simply skip.
195
    }
1✔
196
    try {
197
      this.includeFilters.add(new AnnotationTypeFilter(
8✔
198
              ClassUtils.forName("jakarta.inject.Named", cl), false));
3✔
199
      logger.trace("JSR-330 'jakarta.inject.Named' annotation found and supported for component scanning");
4✔
200
    }
201
    catch (ClassNotFoundException ex) {
1✔
202
      // JSR-330 API (as included in Jakarta EE) not available - simply skip.
203
    }
1✔
204
    try {
205
      this.includeFilters.add(new AnnotationTypeFilter(
8✔
206
              ClassUtils.forName("javax.inject.Named", cl), false));
3✔
207
      logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
4✔
208
    }
209
    catch (ClassNotFoundException ex) {
1✔
210
      // JSR-330 API not available - simply skip.
211
    }
1✔
212
  }
1✔
213

214
  /**
215
   * Set the Environment to use when resolving placeholders and evaluating
216
   * {@link Conditional @Conditional}-annotated component classes.
217
   * <p>The default is a {@link StandardEnvironment}.
218
   *
219
   * @param environment the Environment to use
220
   */
221
  public void setEnvironment(Environment environment) {
222
    Assert.notNull(environment, "Environment is required");
3✔
223
    this.environment = environment;
3✔
224
    this.conditionEvaluator = null;
3✔
225
  }
1✔
226

227
  @Override
228
  public final Environment getEnvironment() {
229
    if (this.environment == null) {
3!
230
      this.environment = new StandardEnvironment();
×
231
    }
232
    return this.environment;
3✔
233
  }
234

235
  /**
236
   * Return the {@link BeanDefinitionRegistry} used by this scanner, if any.
237
   */
238
  @Nullable
239
  protected BeanDefinitionRegistry getRegistry() {
240
    return null;
2✔
241
  }
242

243
  /**
244
   * Set the {@link ResourceLoader} to use for resource locations.
245
   * This will typically be a {@link PatternResourceLoader} implementation.
246
   * <p>Default is a {@code PathMatchingPatternResourceLoader}, also capable of
247
   * resource pattern resolving through the {@code PatternResourceLoader} interface.
248
   *
249
   * @see PatternResourceLoader
250
   * @see PathMatchingPatternResourceLoader
251
   */
252
  @Override
253
  public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
254
    super.setResourceLoader(resourceLoader);
3✔
255
    this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(getResourceLoader().getClassLoader());
6✔
256
  }
1✔
257

258
  /**
259
   * Set the {@link Predicate} to use for candidate component testing.
260
   */
261
  public void setCandidateComponentPredicate(@Nullable Predicate<AnnotationMetadata> candidateComponentPredicate) {
262
    this.candidateComponentPredicate = candidateComponentPredicate == null ? this::isCandidateComponent : candidateComponentPredicate;
5!
263
  }
1✔
264

265
  /**
266
   * Scan the component index or class path for candidate components.
267
   *
268
   * @param basePackage the package to check for annotated classes
269
   * @return a corresponding Set of autodetected bean definitions
270
   */
271
  public Set<AnnotatedBeanDefinition> findCandidateComponents(String basePackage) {
272
    try {
273
      LinkedHashSet<AnnotatedBeanDefinition> candidates = new LinkedHashSet<>();
4✔
274
      scanCandidateComponents(basePackage, (reader, factory) -> {
5✔
275
        ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(reader);
5✔
276
        sbd.setSource(reader.getResource());
4✔
277
        candidates.add(sbd);
4✔
278
      });
1✔
279
      return candidates;
2✔
280
    }
281
    catch (IOException ex) {
×
282
      throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
×
283
    }
284
  }
285

286
  /**
287
   * Scan the class path for candidate components.
288
   *
289
   * @param basePackage the package to check for annotated classes
290
   */
291
  public void scanCandidateComponents(String basePackage, MetadataReaderConsumer metadataReaderConsumer) throws IOException {
292
    if (componentsIndex != null && indexSupportsIncludeFilters()) {
6✔
293
      if (componentsIndex.hasScannedPackage(basePackage)) {
5✔
294
        scanCandidateComponentsFromIndex(
10✔
295
                componentsIndex, basePackage, new FilteredMetadataReaderConsumer(metadataReaderConsumer));
296
        return;
1✔
297
      }
298
      else {
299
        componentsIndex.registerScan(basePackage);
9✔
300
      }
301
    }
302

303
    scan(basePackage, new FilteredMetadataReaderConsumer(metadataReaderConsumer));
8✔
304
  }
1✔
305

306
  /**
307
   * Determine if the component index can be used by this instance.
308
   *
309
   * @return {@code true} if the index is available and the configuration of this
310
   * instance is supported by it, {@code false} otherwise
311
   */
312
  private boolean indexSupportsIncludeFilters() {
313
    for (TypeFilter includeFilter : this.includeFilters) {
11✔
314
      if (!indexSupportsIncludeFilter(includeFilter)) {
4✔
315
        return false;
2✔
316
      }
317
    }
1✔
318
    return true;
2✔
319
  }
320

321
  /**
322
   * Determine if the specified include {@link TypeFilter} is supported by the index.
323
   *
324
   * @param filter the filter to check
325
   * @return whether the index supports this include filter
326
   * @see #extractStereotype(TypeFilter)
327
   */
328
  private boolean indexSupportsIncludeFilter(TypeFilter filter) {
329
    if (filter instanceof AnnotationTypeFilter) {
3✔
330
      return isStereotypeAnnotationForIndex(((AnnotationTypeFilter) filter).getAnnotationType());
6✔
331
    }
332
    else if (filter instanceof AssignableTypeFilter atf) {
6!
333
      Class<?> target = atf.getTargetType();
3✔
334
      return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target);
4✔
335
    }
336
    return false;
×
337
  }
338

339
  /**
340
   * Register the given class as a candidate type with the runtime-populated index, if any.
341
   *
342
   * @param className the fully-qualified class name of the candidate type
343
   * @param filter the include filter to introspect for the associated stereotype
344
   */
345
  private void registerCandidateTypeForIncludeFilter(String className, TypeFilter filter) {
346
    if (this.componentsIndex != null) {
3✔
347
      if (filter instanceof AnnotationTypeFilter atf) {
6✔
348
        Class<? extends Annotation> annotationType = atf.getAnnotationType();
3✔
349
        if (isStereotypeAnnotationForIndex(annotationType)) {
4✔
350
          this.componentsIndex.registerCandidateType(className, annotationType.getName());
11✔
351
        }
352
      }
1✔
353
      else if (filter instanceof AssignableTypeFilter atf) {
6!
354
        Class<?> target = atf.getTargetType();
3✔
355
        if (AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target)) {
4✔
356
          this.componentsIndex.registerCandidateType(className, target.getName());
11✔
357
        }
358
      }
359
    }
360
  }
1✔
361

362
  /**
363
   * Extract the stereotype to use for the specified compatible filter.
364
   *
365
   * @param filter the filter to handle
366
   * @return the stereotype in the index matching this filter
367
   * @see #indexSupportsIncludeFilter(TypeFilter)
368
   */
369
  @Nullable
370
  private String extractStereotype(TypeFilter filter) {
371
    if (filter instanceof AnnotationTypeFilter) {
3✔
372
      return ((AnnotationTypeFilter) filter).getAnnotationType().getName();
5✔
373
    }
374
    if (filter instanceof AssignableTypeFilter) {
3!
375
      return ((AssignableTypeFilter) filter).getTargetType().getName();
5✔
376
    }
377
    return null;
×
378
  }
379

380
  private boolean isStereotypeAnnotationForIndex(Class<? extends Annotation> annotationType) {
381
    return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotationType)
6✔
382
            || annotationType.getName().startsWith("jakarta.")
5✔
383
            || annotationType.getName().startsWith("javax.");
7✔
384
  }
385

386
  private void scanCandidateComponentsFromIndex(CandidateComponentsIndex index,
387
          String basePackage, MetadataReaderConsumer metadataReaderConsumer) throws IOException {
388
    HashSet<String> types = new HashSet<>();
4✔
389
    for (TypeFilter filter : this.includeFilters) {
11✔
390
      String stereotype = extractStereotype(filter);
4✔
391
      if (stereotype == null) {
2!
392
        throw new IllegalArgumentException("Failed to extract stereotype from " + filter);
×
393
      }
394
      types.addAll(index.getCandidateTypes(basePackage, stereotype));
7✔
395
    }
1✔
396

397
    MetadataReaderFactory metadataReaderFactory = getMetadataReaderFactory();
3✔
398
    for (String type : types) {
10✔
399
      MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(type);
4✔
400
      metadataReaderConsumer.accept(metadataReader, metadataReaderFactory);
4✔
401
    }
1✔
402
  }
1✔
403

404
  /**
405
   * Resolve the specified base package into a pattern specification for
406
   * the package search path.
407
   * <p>The default implementation resolves placeholders against system properties,
408
   * and converts a "."-based package path to a "/"-based resource path.
409
   *
410
   * @param basePackage the base package as specified by the user
411
   * @return the pattern specification to be used for package searching
412
   */
413
  @Override
414
  protected String resolveBasePackage(String basePackage) {
415
    return ClassUtils.convertClassNameToResourcePath(
3✔
416
            getEnvironment().resolveRequiredPlaceholders(basePackage));
3✔
417
  }
418

419
  /**
420
   * Determine whether the given class does not match any exclude filter
421
   * and does match at least one include filter.
422
   *
423
   * @param metadataReader the ASM ClassReader for the class
424
   * @param factory a factory for obtaining metadata readers
425
   * for other classes (such as superclasses and interfaces)
426
   * @return whether the class qualifies as a candidate component
427
   */
428
  protected boolean isCandidateComponent(MetadataReader metadataReader, MetadataReaderFactory factory) throws IOException {
429
    for (TypeFilter tf : excludeFilters) {
11✔
430
      if (tf.match(metadataReader, factory)) {
5✔
431
        return false;
2✔
432
      }
433
    }
1✔
434
    for (TypeFilter tf : includeFilters) {
11✔
435
      if (tf.match(metadataReader, factory)) {
5✔
436
        registerCandidateTypeForIncludeFilter(metadataReader.getClassMetadata().getClassName(), tf);
6✔
437
        return isConditionMatch(metadataReader);
4✔
438
      }
439
    }
1✔
440
    return false;
2✔
441
  }
442

443
  /**
444
   * Determine whether the given class is a candidate component based on any
445
   * {@code @Conditional} annotations.
446
   *
447
   * @param metadataReader the ASM ClassReader for the class
448
   * @return whether the class qualifies as a candidate component
449
   */
450
  private boolean isConditionMatch(MetadataReader metadataReader) {
451
    if (conditionEvaluator == null) {
3✔
452
      this.conditionEvaluator = new ConditionEvaluator(
6✔
453
              environment, getResourceLoader(), getRegistry());
5✔
454
    }
455
    return conditionEvaluator.passCondition(metadataReader.getAnnotationMetadata());
6✔
456
  }
457

458
  /**
459
   * Determine whether the given bean definition qualifies as a candidate component.
460
   * <p>The default implementation checks whether the class is not dependent on an
461
   * enclosing class as well as whether the class is either concrete (and therefore
462
   * not an interface) or has {@link Lookup @Lookup} methods.
463
   * <p>Can be overridden in subclasses.
464
   *
465
   * @param metadata the metadata to check
466
   * @return whether the bean definition qualifies as a candidate component
467
   */
468
  protected boolean isCandidateComponent(AnnotationMetadata metadata) {
469
    return metadata.isIndependent() && (
5!
470
            metadata.isConcrete() || (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))
13!
471
    );
472
  }
473

474
  // includeFilters excludeFilters Consumer
475

476
  private final class FilteredMetadataReaderConsumer implements MetadataReaderConsumer {
477

478
    final MetadataReaderConsumer delegate;
479

480
    FilteredMetadataReaderConsumer(MetadataReaderConsumer delegate) {
5✔
481
      this.delegate = delegate;
3✔
482
    }
1✔
483

484
    @Override
485
    public void accept(MetadataReader metadataReader, MetadataReaderFactory factory) throws IOException {
486
      if (isCandidateComponent(metadataReader, factory)
10✔
487
              && candidateComponentPredicate.test(metadataReader.getAnnotationMetadata())) {
3✔
488
        delegate.accept(metadataReader, factory);
5✔
489
      }
490
    }
1✔
491
  }
492
}
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