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

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

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

92.39
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(
8✔
182
              ClassUtils.forName("jakarta.annotation.ManagedBean", cl), false));
3✔
183
      logger.trace("JSR-250 'jakarta.annotation.ManagedBean' found and supported for component scanning");
4✔
184
    }
185
    catch (ClassNotFoundException ex) {
1✔
186
      // JSR-250 1.1 API (as included in Jakarta EE) not available - simply skip.
187
    }
1✔
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
      scanCandidateComponentsFromIndex(
11✔
294
              componentsIndex, basePackage, new FilteredMetadataReaderConsumer(metadataReaderConsumer));
295
    }
296
    else {
297
      scan(basePackage, new FilteredMetadataReaderConsumer(metadataReaderConsumer));
8✔
298
    }
299
  }
1✔
300

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

316
  /**
317
   * Determine if the specified include {@link TypeFilter} is supported by the index.
318
   *
319
   * @param filter the filter to check
320
   * @return whether the index supports this include filter
321
   * @see #extractStereotype(TypeFilter)
322
   */
323
  private boolean indexSupportsIncludeFilter(TypeFilter filter) {
324
    if (filter instanceof AnnotationTypeFilter) {
3✔
325
      Class<? extends Annotation> annotation = ((AnnotationTypeFilter) filter).getAnnotationType();
4✔
326
      return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, annotation)
6✔
327
              || annotation.getName().startsWith("jakarta.")
5✔
328
              || annotation.getName().startsWith("javax.");
7✔
329
    }
330
    else if (filter instanceof AssignableTypeFilter atf) {
6!
331
      Class<?> target = atf.getTargetType();
3✔
332
      return AnnotationUtils.isAnnotationDeclaredLocally(Indexed.class, target);
4✔
333
    }
334
    return false;
×
335
  }
336

337
  /**
338
   * Extract the stereotype to use for the specified compatible filter.
339
   *
340
   * @param filter the filter to handle
341
   * @return the stereotype in the index matching this filter
342
   * @see #indexSupportsIncludeFilter(TypeFilter)
343
   */
344
  @Nullable
345
  private String extractStereotype(TypeFilter filter) {
346
    if (filter instanceof AnnotationTypeFilter) {
3✔
347
      return ((AnnotationTypeFilter) filter).getAnnotationType().getName();
5✔
348
    }
349
    if (filter instanceof AssignableTypeFilter) {
3!
350
      return ((AssignableTypeFilter) filter).getTargetType().getName();
5✔
351
    }
352
    return null;
×
353
  }
354

355
  private void scanCandidateComponentsFromIndex(CandidateComponentsIndex index,
356
          String basePackage, MetadataReaderConsumer metadataReaderConsumer) throws IOException {
357
    HashSet<String> types = new HashSet<>();
4✔
358
    for (TypeFilter filter : this.includeFilters) {
11✔
359
      String stereotype = extractStereotype(filter);
4✔
360
      if (stereotype == null) {
2!
361
        throw new IllegalArgumentException("Failed to extract stereotype from " + filter);
×
362
      }
363
      types.addAll(index.getCandidateTypes(basePackage, stereotype));
7✔
364
    }
1✔
365

366
    MetadataReaderFactory metadataReaderFactory = getMetadataReaderFactory();
3✔
367
    for (String type : types) {
10✔
368
      MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(type);
4✔
369
      metadataReaderConsumer.accept(metadataReader, metadataReaderFactory);
4✔
370
    }
1✔
371
  }
1✔
372

373
  /**
374
   * Resolve the specified base package into a pattern specification for
375
   * the package search path.
376
   * <p>The default implementation resolves placeholders against system properties,
377
   * and converts a "."-based package path to a "/"-based resource path.
378
   *
379
   * @param basePackage the base package as specified by the user
380
   * @return the pattern specification to be used for package searching
381
   */
382
  @Override
383
  protected String resolveBasePackage(String basePackage) {
384
    return ClassUtils.convertClassNameToResourcePath(
3✔
385
            getEnvironment().resolveRequiredPlaceholders(basePackage));
3✔
386
  }
387

388
  /**
389
   * Determine whether the given class does not match any exclude filter
390
   * and does match at least one include filter.
391
   *
392
   * @param metadataReader the ASM ClassReader for the class
393
   * @param factory a factory for obtaining metadata readers
394
   * for other classes (such as superclasses and interfaces)
395
   * @return whether the class qualifies as a candidate component
396
   */
397
  protected boolean isCandidateComponent(MetadataReader metadataReader, MetadataReaderFactory factory) throws IOException {
398
    for (TypeFilter tf : excludeFilters) {
11✔
399
      if (tf.match(metadataReader, factory)) {
5✔
400
        return false;
2✔
401
      }
402
    }
1✔
403
    for (TypeFilter tf : includeFilters) {
11✔
404
      if (tf.match(metadataReader, factory)) {
5✔
405
        return isConditionMatch(metadataReader);
4✔
406
      }
407
    }
1✔
408
    return false;
2✔
409
  }
410

411
  /**
412
   * Determine whether the given class is a candidate component based on any
413
   * {@code @Conditional} annotations.
414
   *
415
   * @param metadataReader the ASM ClassReader for the class
416
   * @return whether the class qualifies as a candidate component
417
   */
418
  private boolean isConditionMatch(MetadataReader metadataReader) {
419
    if (conditionEvaluator == null) {
3✔
420
      this.conditionEvaluator = new ConditionEvaluator(
6✔
421
              environment, getResourceLoader(), getRegistry());
5✔
422
    }
423
    return conditionEvaluator.passCondition(metadataReader.getAnnotationMetadata());
6✔
424
  }
425

426
  /**
427
   * Determine whether the given bean definition qualifies as a candidate component.
428
   * <p>The default implementation checks whether the class is not dependent on an
429
   * enclosing class as well as whether the class is either concrete (and therefore
430
   * not an interface) or has {@link Lookup @Lookup} methods.
431
   * <p>Can be overridden in subclasses.
432
   *
433
   * @param metadata the metadata to check
434
   * @return whether the bean definition qualifies as a candidate component
435
   */
436
  protected boolean isCandidateComponent(AnnotationMetadata metadata) {
437
    return metadata.isIndependent() && (
5!
438
            metadata.isConcrete() || (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))
13!
439
    );
440
  }
441

442
  // includeFilters excludeFilters Consumer
443

444
  private final class FilteredMetadataReaderConsumer implements MetadataReaderConsumer {
445

446
    final MetadataReaderConsumer delegate;
447

448
    FilteredMetadataReaderConsumer(MetadataReaderConsumer delegate) {
5✔
449
      this.delegate = delegate;
3✔
450
    }
1✔
451

452
    @Override
453
    public void accept(MetadataReader metadataReader, MetadataReaderFactory factory) throws IOException {
454
      if (isCandidateComponent(metadataReader, factory)
10✔
455
              && candidateComponentPredicate.test(metadataReader.getAnnotationMetadata())) {
3✔
456
        delegate.accept(metadataReader, factory);
5✔
457
      }
458
    }
1✔
459
  }
460
}
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