Deep Dive into Spring’s @ComponentScan: Usage and Underlying Mechanics

This article explains why Spring Boot is popular, how @ComponentScan discovers @Component, @Controller, @Service, and @Repository beans across packages, demonstrates usage with code examples, and details the internal processing flow involving ConfigurationClassPostProcessor and related parsers.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Deep Dive into Spring’s @ComponentScan: Usage and Underlying Mechanics

1. Overview

Spring Boot became the mainstream framework because it follows the principle of "convention over configuration" and provides annotation‑based configuration, eliminating cumbersome XML files. In a typical Spring MVC three‑layer project, annotations such as @Controller, @Service, and @Repository are used to declare beans that can be injected into the Spring container regardless of their package locations.

The core question is how Spring injects classes annotated with @Component (and its derived annotations) into the container. The answer is the @ComponentScan annotation, which tells Spring which packages to scan for these annotations. @Controller: marks a presentation‑layer component. @Service: marks a business‑layer component. @Repository: marks a persistence‑layer component. @RestController: a specialization of @Controller for REST APIs.

2. Using @ComponentScan

The definition of @ComponentScan includes many attributes, most of which have sensible defaults. In practice, developers usually only set basePackages to specify the packages to scan.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
    String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;
    boolean useDefaultFilters() default true;
    Filter[] includeFilters() default {};
    Filter[] excludeFilters() default {};
    boolean lazyInit() default false;
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    @interface Filter {
        FilterType type() default FilterType.ANNOTATION;
        @AliasFor("classes")
        Class<?>[] value() default {};
        @AliasFor("value")
        Class<?>[] classes() default {};
        String[] pattern() default {};
    }
}

Example classes in package com.shepherd.common.bean:

@Component
public class Coo {}

@Repository
public class Doo {}

@Service
public class Eoo {}

@RestController
public class Foo {}

Another class in com.shepherd.common.bean1:

@Component
public class Goo {}

Scanning only com.shepherd.common.bean:

@ComponentScan("com.shepherd.common.bean")
public class MyConfig {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MyConfig.class);
        for (String name : ctx.getBeanDefinitionNames()) {
            System.out.println(name);
        }
    }
}

Output shows Goo is missing because its package was not scanned. Adding the second package resolves the issue:

@ComponentScan(basePackages = {"com.shepherd.common.bean", "com.shepherd.common.bean1"})
public class MyConfig { /* same main method */ }

When @ComponentScan is used multiple times on the same class, it is equivalent to using

@ComponentScans({@ComponentScan("..."), @ComponentScan("...")})

. However, without the @Configuration annotation, the class is not recognized as a configuration class, so the repeated scans are ignored.

3. Implementation Details of @ComponentScan

The scanning process is driven by ConfigurationClassPostProcessor. During parsing, ConfigurationClassParser.doProcessConfigurationClass() collects @ComponentScan attributes via AnnotationConfigUtils.attributesForRepeatable and invokes ComponentScanAnnotationParser.parse() for each scan.

protected final SourceClass doProcessConfigurationClass(
        ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) {
    // ... handle @Component, @PropertySource, etc.
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    if (!componentScans.isEmpty() && !conditionEvaluator.shouldSkip(...)) {
        for (AnnotationAttributes componentScan : componentScans) {
            Set<BeanDefinitionHolder> scanned = componentScanParser.parse(componentScan,
                    sourceClass.getMetadata().getClassName());
            for (BeanDefinitionHolder holder : scanned) {
                BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                if (bdCand == null) bdCand = holder.getBeanDefinition();
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, metadataReaderFactory)) {
                    parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        }
    }
    // ... process @Import, @ImportResource, @Bean methods, etc.
}
ComponentScanAnnotationParser.parse()

creates a ClassPathBeanDefinitionScanner, configures it according to the annotation attributes (name generator, scope resolver, resource pattern, include/exclude filters, lazy init), determines the base packages, adds an exclude filter for the declaring class itself, and finally calls scanner.doScan() to perform the actual classpath scan.

public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
    ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(
            this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
    // configure name generator, scoped proxy, scope resolver, resource pattern, filters, lazy init
    Set<String> basePackages = new LinkedHashSet<>();
    // resolve basePackages and basePackageClasses
    if (basePackages.isEmpty()) {
        basePackages.add(ClassUtils.getPackageName(declaringClass));
    }
    scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
        @Override
        protected boolean matchClassName(String className) {
            return declaringClass.equals(className);
        }
    });
    return scanner.doScan(StringUtils.toStringArray(basePackages));
}

The scanner then iterates over candidate components, assigns scopes, generates bean names, checks for name conflicts, and registers each BeanDefinition into the Spring container.

4. Summary

The article dissected the full lifecycle of @ComponentScan: from its declaration, through attribute parsing, to the creation of a ClassPathBeanDefinitionScanner that scans the specified packages, filters candidates, generates bean names, and registers them. Understanding this flow clarifies why beans annotated with @Component, @Controller, @Service, and @Repository become available in the Spring container, and why the presence of @Configuration (or its absence) affects multi‑scan scenarios.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ConfigurationSpringAnnotationDependency Injectioncomponent-scanspring-boot
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.