skip to content

Under the hood, how does Spring discover stereotype-annotated classes and assign bean names, and how can you customize both?

level: principalimportance: nice to knowfreq 25%

answer

  1. ClassPathScanningCandidateComponentProvider + ASM MetadataReader (no class load)
  2. default AnnotationTypeFilter(@Component) matches meta-annotations
  3. AnnotationBeanNameGenerator = uncapitalized class name or value
  4. FullyQualifiedAnnotationBeanNameGenerator avoids clashes
  5. @ComponentScan include/exclude filters, useDefaultFilters, nameGenerator

basics

~20 s

Component scanning uses ClassPathScanningCandidateComponentProvider with an AnnotationTypeFilter for @Component to find candidates in the base packages, reading bytecode via ASM (no class loading). AnnotationBeanNameGenerator names each bean (uncapitalized class name, or the stereotype's value). You customize with @ComponentScan filters, nameGenerator, and scopeResolver.

solid answer

~40 s

At startup, @ComponentScan (or Boot's default) drives a ClassPathScanningCandidateComponentProvider that scans base packages, reading class metadata via ASM MetadataReader without loading classes. Its default include filter is an AnnotationTypeFilter for @Component, which matches meta-annotated stereotypes too. Matching classes become BeanDefinitions. Bean names come from AnnotationBeanNameGenerator: if the stereotype declares a non-empty value (e.g., @Service("x")), that's the name; otherwise it's the uncapitalized simple class name. You can customize scanning via @ComponentScan attributes: includeFilters/excludeFilters (by ANNOTATION, ASSIGNABLE_TYPE, REGEX, ASPECTJ, or CUSTOM TypeFilter), useDefaultFilters=false to drop the @Component filter, lazyInit, nameGenerator (a custom BeanNameGenerator, e.g. FullyQualifiedAnnotationBeanNameGenerator to avoid name clashes), and scopeResolver/scopedProxy. Filters and a custom generator let you build alternative discovery/naming conventions.

code

java · 18 lines
java
import org.springframework.context.annotation.*;
import org.springframework.context.annotation.FilterType;

// Scan only @UseCase-annotated classes, name beans by FQN to avoid clashes
@Configuration
@ComponentScan(
    basePackages = "com.example.app",
    useDefaultFilters = false, // drop the built-in @Component filter
    includeFilters = @ComponentScan.Filter(
        type = FilterType.ANNOTATION, classes = UseCase.class),
    nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class
)
class ScanConfig { }

// Programmatic alternative:
// var ctx = new AnnotationConfigApplicationContext();
// ctx.scan("com.example.app");
// ctx.refresh();

go deeper

for a junior

Not expected; may just know scanning finds annotated classes.

for a middle

Knows @ComponentScan filters and default naming.

for a senior

Explains the filter/metadata pipeline and FullyQualifiedAnnotationBeanNameGenerator for clashes.

for a principal

Details ASM bytecode metadata, meta-annotation matching, custom TypeFilters/BeanNameGenerator, and startup/name-collision trade-offs.

**Discovery pipeline:** 1. `@ComponentScan` (directly, or transitively via `@SpringBootApplication`) is processed by `ConfigurationClassPostProcessor` → `ComponentScanAnnotationParser`, which configures a **`ClassPathScanningCandidateComponentProvider`**. 2. The provider resolves the **base packages** (explicit `basePackages`/`basePackageClasses`, else the annotated class's own package) into a classpath resource pattern and enumerates `.class` resources. 3. For each candidate it uses an ASM-based **`MetadataReader`** to read annotations from **bytecode without loading the class** (fast, avoids side effects and premature static init). Metadata is exposed via `AnnotationMetadata`. 4. **Include filters** decide candidacy. By default `useDefaultFilters=true` registers an `AnnotationTypeFilter(Component.class)`, plus filters for JSR-250 `@ManagedBean` / JSR-330 `@Named` if present. The `@Component` filter matches **meta-annotated** stereotypes because Spring's metadata records meta-annotations — that's why `@Service`/`@Repository`/`@Controller` and custom stereotypes match. 5. Candidates that pass filters and are **concrete** (not interface/abstract, unless a `@Lookup` case) become `ScannedGenericBeanDefinition`s registered in the `BeanDefinitionRegistry`. **Bean naming:** the **`BeanNameGenerator`** — default `AnnotationBeanNameGenerator` — computes the name: if the stereotype (or any meta-annotation like `@ManagedBean`/`@Named`) has an explicit `value`, that string wins; otherwise it **uncapitalizes the short class name** (`OrderService` → `orderService`; special-cased so an all-caps prefix like `URLParser` isn't lowercased incorrectly per Java Beans `Introspector.decapitalize`). To avoid collisions across packages (two `com.a.Foo` and `com.b.Foo`), use **`FullyQualifiedAnnotationBeanNameGenerator`**, which uses the FQN as the name; wire it via `@ComponentScan(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)`. **Scope:** a `ScopeMetadataResolver` (default `AnnotationScopeMetadataResolver`) reads `@Scope`; `scopedProxy` controls creating scoped proxies for shorter-lived beans injected into singletons. **Customization levers (`@ComponentScan` attributes):** - `includeFilters` / `excludeFilters` with `FilterType.ANNOTATION`, `ASSIGNABLE_TYPE`, `REGEX`, `ASPECTJ`, or `CUSTOM` (your own `TypeFilter`). Example: scan only classes annotated `@UseCase`, or exclude a package. - `useDefaultFilters=false` to disable the built-in `@Component` filter entirely (then only your include filters apply) — handy to build a fully custom stereotype set. - `lazyInit=true` to make all scanned beans lazy. - `nameGenerator`, `scopeResolver`, `scopedProxy`, `resourcePattern`. **Edge cases & gotchas:** - Because scanning reads bytecode, an annotation with `@Retention(CLASS)` or `SOURCE` won't be seen — stereotypes must be `RUNTIME`. - Two beans resolving to the same default name across packages cause an override/conflict — the FQN generator or explicit names fix it. - `excludeFilters` are evaluated too; Spring Boot's `@SpringBootApplication` uses exclude filters (`TypeExcludeFilter`, `AutoConfigurationExcludeFilter`) to keep auto-config and test slices tidy. - Meta-annotation matching means an `excludeFilter` on `@Component` would exclude everything; filter on the specific stereotype instead. - Programmatic scanning is possible via `AnnotationConfigApplicationContext#scan` or `ClassPathBeanDefinitionScanner`.

  • Why does component scanning read bytecode via ASM instead of loading classes and using reflection?
    To inspect annotation metadata cheaply without triggering class loading/static initialization or resolving dependencies for classes that may be filtered out — faster startup and no side effects.
  • Two classes named Foo in different packages are both @Service. What breaks and how do you fix it?
    Both default to bean name "foo", causing a name conflict/override. Fix with explicit @Service("...") names or FullyQualifiedAnnotationBeanNameGenerator.
  • How would you scan for ONLY your custom stereotype and ignore @Service/@Component?
    Set useDefaultFilters=false and add an includeFilter of FilterType.ANNOTATION for your annotation, so only those classes are registered.

saying these in an interview costs you the question

  • Claiming scanning loads every class via reflection
  • Thinking bean names include the package by default
  • Saying you cannot change the naming strategy
  • Believing @Retention(SOURCE) annotations can be stereotypes

context