skip to content

What is ClassPathScanningCandidateComponentProvider and when would you use it directly?

level: seniorimportance: should knowfreq 30%

answer

  1. engine behind @ComponentScan / ClassPathBeanDefinitionScanner extends it
  2. ASM MetadataReader → no class loading
  3. constructor(false) = no default filters, add AnnotationTypeFilter/AssignableTypeFilter
  4. default rejects interfaces/abstract → override isCandidateComponent
  5. used for @Entity/mapper/plugin discovery pre-context

basics

~20 s

It's the low-level engine behind @ComponentScan: given base packages and filters, it scans the classpath and returns candidate BeanDefinitions. You use it directly to find annotated/typed classes yourself — outside the normal bean registration flow.

solid answer

~40 s

ClassPathScanningCandidateComponentProvider is the reusable scanning engine that @ComponentScan (via ClassPathBeanDefinitionScanner) builds on. It walks the classpath under given base packages, reads each class's metadata through ASM-based MetadataReader (no class loading), applies include/exclude TypeFilters, and returns a Set<BeanDefinition> of matching candidates. You instantiate it directly when you need scanning results outside a Spring context — e.g. discovering all classes annotated with a custom annotation, all implementations of an interface, or all JPA @Entity classes for a manual registration/tooling step. Pass useDefaultFilters=false to the constructor and add your own addIncludeFilter(new AnnotationTypeFilter(...)) or AssignableTypeFilter. A notable quirk: by default it only returns concrete, independent classes; to match interfaces/abstract types you override isCandidateComponent. It underpins things like entity scanning and mapper discovery.

code

java · 14 lines
java
// Find all classes annotated @DomainEvent, including nothing by default
var provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(DomainEvent.class));
for (BeanDefinition bd : provider.findCandidateComponents("com.app")) {
    System.out.println(bd.getBeanClassName());
}

// Scan for an INTERFACE type — must relax isCandidateComponent
var ifaceProvider = new ClassPathScanningCandidateComponentProvider(false) {
    @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition d) {
        return d.getMetadata().isIndependent(); // allow interfaces
    }
};
ifaceProvider.addIncludeFilter(new AssignableTypeFilter(ApiClient.class));

go deeper

for a junior

Likely unfamiliar; may only know @ComponentScan exists.

for a middle

Should recognize it as the underlying scanner and that filters map to TypeFilter classes.

for a senior

Should use it standalone with AnnotationTypeFilter/AssignableTypeFilter and know the interface-scanning override.

for a principal

Should discuss ASM metadata vs reflection tradeoffs, caching metadata readers, and building custom @Enable discovery/registrar mechanisms on top of it.

## The engine under @ComponentScan `@ComponentScan` is declarative sugar. At runtime, `ConfigurationClassPostProcessor` delegates to `ClassPathBeanDefinitionScanner`, which **extends** `ClassPathScanningCandidateComponentProvider`. The provider is the part that actually finds classes; the scanner subclass adds registering them into a `BeanDefinitionRegistry`, naming, scope resolution, etc. ### What the provider does Given base package(s), `findCandidateComponents(String basePackage)`: 1. Resolves a resource pattern (e.g. `classpath*:com/app/**/*.class`) via a `ResourcePatternResolver`. 2. For each `.class` resource, obtains a **`MetadataReader`** from a `MetadataReaderFactory` — this uses **ASM** to read bytecode metadata (annotations, superclass, interfaces) **without loading or initializing the class**. That's important: scanning thousands of classes without triggering static initializers or classloading side effects. 3. Applies **exclude** then **include** `TypeFilter`s. 4. For surviving matches, calls `isCandidateComponent(MetadataReader)` — by default requiring the class be **concrete (not abstract/interface) and independent** (not a non-static inner class), OR an abstract class with a lookup-method. 5. Returns a `Set<BeanDefinition>` (specifically `ScannedGenericBeanDefinition`). ### Using it standalone You can `new ClassPathScanningCandidateComponentProvider(false)` — the `false` means **don't register default filters**, so it won't match stereotypes unless you say so. Then add filters: - `provider.addIncludeFilter(new AnnotationTypeFilter(MyAnnotation.class))` - `provider.addIncludeFilter(new AssignableTypeFilter(MyInterface.class))` Then iterate `provider.findCandidateComponents("com.app")` and read `bd.getBeanClassName()`. ### Why bother directly? - **Framework/library code** that must discover classes by annotation or type before a context exists — e.g. JPA persistence-unit `@Entity` discovery, MyBatis mapper scanning, custom plugin registries, code generators. - Building your own `@Enable...` mechanism that programmatically registers beans from discovered classes. ### The interface-scanning gotcha The default `isCandidateComponent` **rejects interfaces and abstract classes**, so `AssignableTypeFilter(MyInterface.class)` alone returns nothing if your targets are interfaces. Override it: ```java new ClassPathScanningCandidateComponentProvider(false) { @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isIndependent(); } }; ``` This is a very common interview/real-world trap when someone tries to scan for interfaces (e.g. Feign-style clients or repository interfaces). ### Related APIs - `MetadataReaderFactory` / `CachingMetadataReaderFactory` — supply/cache metadata readers. - `AnnotationTypeFilter`, `AssignableTypeFilter`, `RegexPatternTypeFilter`, `AspectJTypeFilter` — the concrete `TypeFilter`s that back the declarative `FilterType`s. - `ClassPathBeanDefinitionScanner` — the registering subclass used by `@ComponentScan`. - `AnnotatedBeanDefinitionReader` — the counterpart for reading explicitly-passed classes (not scanning). ### Gotchas - Set the environment/resource loader if you use it very early; the default constructor sets up a `StandardEnvironment`. - ASM metadata means annotations must be **retained at CLASS/RUNTIME** retention to be visible. - It scans `.class` files — classes must be on the resolvable classpath (packaged jars via `classpath*:`).

  • Why does scanning use MetadataReader/ASM instead of reflection?
    ASM reads bytecode metadata without loading or initializing the class, avoiding classloading cost and static-initializer side effects across potentially thousands of classes, and letting Spring inspect classes it may never instantiate.
  • You scan for an interface with AssignableTypeFilter but get zero results — why?
    The default isCandidateComponent only accepts concrete, independent classes and rejects interfaces/abstract types. Override isCandidateComponent to return metadata.isIndependent() to include them.

saying these in an interview costs you the question

  • Claiming scanning uses reflection/Class.forName on every class (it uses ASM metadata)
  • Not knowing ClassPathBeanDefinitionScanner extends the provider
  • Assuming AssignableTypeFilter matches interfaces by default
  • Thinking the provider registers beans itself (it only returns candidate BeanDefinitions)

context