Spring Configuration & Profiles

Component Scanning Deep Dive

18 min Lesson 3 of 13

Component Scanning Deep Dive

In the previous lessons you saw how to declare beans explicitly with @Bean methods. Component scanning is the complementary mechanism: Spring walks a set of packages, finds classes annotated with @Component (or any meta-annotation that is itself annotated with it), and registers them as beans automatically. Understanding how scanning works — and how to control its scope precisely — is essential once your application grows beyond a handful of classes.

How Component Scanning Works Under the Hood

When the application context starts, Spring's ClassPathScanningCandidateComponentProvider iterates every .class file on the classpath under the configured base packages. It reads the bytecode metadata (without fully loading the class) and checks for the presence of stereotype annotations. Any matching class is instantiated and registered as a bean.

The stereotype annotations that trigger scanning by default are:

  • @Component — the generic stereotype; the parent of all others.
  • @Service — signals a service-layer bean (no extra Spring behavior, but documents intent).
  • @Repository — signals a data-access bean; also enables Spring's exception translation for persistence exceptions.
  • @Controller / @RestController — marks a Spring MVC handler.
All stereotype annotations are meta-annotated with @Component. That is why scanning picks them up. You can create your own stereotype by annotating your annotation with @Component (or any existing stereotype), and Spring will discover it too.

Declaring Base Packages with @ComponentScan

In a plain Spring 6 application you add @ComponentScan to a @Configuration class. In Spring Boot the @SpringBootApplication annotation already includes it, defaulting to the package of the annotated class. Knowing the explicit form lets you override or augment that default.

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackages = { "com.example.orders", "com.example.payments" }) public class AppConfig { }

Alternatively, use basePackageClasses to supply type-safe markers instead of string literals. This is the recommended style because refactoring tools will track the class rename automatically:

@ComponentScan(basePackageClasses = { OrdersMarker.class, // a package-private marker interface in com.example.orders PaymentsMarker.class // a package-private marker interface in com.example.payments })
Prefer basePackageClasses over string package names. A string survives a package rename silently; the marker class does not compile if moved without updating the reference. One empty package-info.java or a dedicated package/Marker.java interface per root package is all you need.

Include Filters: Expanding What Gets Scanned

By default, scanning picks up any class annotated with a stereotype annotation. You can broaden that with includeFilters. Each filter is a @ComponentScan.Filter that specifies a filter type and the target.

Common filter types (from FilterType):

  • ANNOTATION — include all classes carrying a given annotation.
  • ASSIGNABLE_TYPE — include all classes that are assignable to a given type (class or interface).
  • REGEX — include classes whose fully-qualified name matches a regular expression.
  • ASPECTJ — include classes matching an AspectJ type pattern.
  • CUSTOM — include classes accepted by a custom TypeFilter implementation.

Example: register every class that implements EventHandler, even if it carries no stereotype annotation:

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Configuration; import com.example.events.EventHandler; @Configuration @ComponentScan( basePackages = "com.example", includeFilters = @ComponentScan.Filter( type = FilterType.ASSIGNABLE_TYPE, classes = EventHandler.class ) ) public class AppConfig { }
When you add includeFilters, you often need to disable the default annotation filter. If useDefaultFilters is still true (the default), Spring scans for both stereotype annotations AND your include filter. Set useDefaultFilters = false for an entirely custom ruleset — but then remember to add back any stereotype filter you still want.

Exclude Filters: Narrowing the Scan

Exclude filters are more commonly useful in production code. The most frequent use-cases are isolating a test configuration, avoiding double-registration when multiple @ComponentScan declarations overlap, and skipping generated or third-party classes that live under your package prefix.

import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan( basePackages = "com.example", excludeFilters = { // exclude all @Controller beans from this non-web context @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = org.springframework.stereotype.Controller.class ), // exclude any class whose name ends with "Legacy" @ComponentScan.Filter( type = FilterType.REGEX, pattern = ".*Legacy" ) } ) public class ServiceLayerConfig { }

Writing a Custom TypeFilter

When the built-in filter types are not expressive enough, implement org.springframework.core.type.filter.TypeFilter. It receives metadata about every candidate class without needing to load it fully (classloading is expensive).

import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.TypeFilter; import java.io.IOException; public class InternalClassFilter implements TypeFilter { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { ClassMetadata meta = metadataReader.getClassMetadata(); // exclude any class whose simple name starts with "Internal" String simpleName = meta.getClassName(); int dot = simpleName.lastIndexOf('.'); return !simpleName.substring(dot + 1).startsWith("Internal"); } }

Then reference it with FilterType.CUSTOM:

@ComponentScan( basePackages = "com.example", includeFilters = @ComponentScan.Filter( type = FilterType.CUSTOM, classes = InternalClassFilter.class ), useDefaultFilters = false )

Lazy Scanning and @Lazy

By default, Spring instantiates all singleton beans eagerly at startup. For large component scans this can noticeably slow boot time. Annotate a component with @Lazy to defer its instantiation until it is first requested:

import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @Service @Lazy public class HeavyReportService { public HeavyReportService() { // expensive initialization } }

You can also set @ComponentScan(lazyInit = true) to make every scanned bean lazy by default — useful in developer-mode to speed up local restarts while keeping production eager.

Component Scanning in Spring Boot

Spring Boot's @SpringBootApplication is a composed annotation that includes @ComponentScan with no explicit packages, which means it scans the package of the main class and all sub-packages. This "scan from root" convention is why you should keep your main class at the top-level of your base package:

// com/example/demo/DemoApplication.java <-- scans com.example.demo.* package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

If you need to scan additional packages (for example, a shared library that lives under a different root), add @ComponentScan alongside @SpringBootApplication — or, better, use scanBasePackages directly on @SpringBootApplication:

@SpringBootApplication(scanBasePackages = { "com.example.demo", "com.shared.components" }) public class DemoApplication { ... }

Practical Trade-offs and Guidelines

  • Keep your base package tight. Scanning every class under com or org would be catastrophic for startup time. Always specify the narrowest packages that cover your own code.
  • Avoid overlapping scans. If two @Configuration classes scan the same package, beans will be registered twice (Spring deduplicates by bean name, but duplicate scanning wastes time and can cause subtle ordering bugs).
  • Separate web and non-web contexts. Classic Spring MVC applications have two contexts — root and servlet. Use excludeFilters to keep @Controller beans out of the root context and service/repository beans out of the servlet context.
  • Use explicit @Bean for third-party classes. You cannot add @Component to a class you do not own; declare it as a @Bean in a @Configuration class instead.

Summary

@ComponentScan automates bean discovery by reading classpath bytecode. basePackages / basePackageClasses define the search territory. includeFilters and excludeFilters — backed by annotation, type, regex, or custom TypeFilter logic — let you describe exactly which classes qualify. Mastering these controls means you can design clean context boundaries, speed up startup, and avoid accidental bean registrations as your codebase scales.