Spring Boot
Component Scanning
Component scanning is the mechanism Spring uses to automatically discover and register beans. Instead of declaring every bean manually, you annotate classes with stereotypes like @Component, @Service, @Repository, or @Controller, and Spring finds and registers them automatically. Understanding how scanning works — its rules, filters, and performance implications — lets you control exactly what Spring discovers.
How Component Scanning Works
Component scanning is triggered by @ComponentScan — included automatically via @SpringBootApplication. Spring walks the specified base packages, examines every class file, and registers any class annotated with a component stereotype as a Spring bean.
The scanning process:
1. Spring loads class metadata (using ASM bytecode reader — classes are NOT loaded by the JVM yet)
2. It checks each class for @Component or any annotation meta-annotated with @Component
3. If found, a BeanDefinition is created and registered with the BeanFactory
4. After all definitions are registered, beans are instantiated and wired
This two-phase approach (scan then instantiate) means Spring can resolve all dependencies before creating any bean — allowing it to detect circular dependencies and determine creation order.
Stereotype Annotations
Spring provides four stereotype annotations that trigger component scanning registration. Each is meta-annotated with @Component — they all register beans the same way but communicate different roles and may carry additional behavior.
Java
// @Component — generic Spring-managed component:
// Use when no more specific stereotype applies
@Component
public class EmailTemplateRenderer {
public String render(String template, Map<String, Object> variables) {
// render template
return "";
}
}
// @Service — business logic / service layer:
// Semantically identical to @Component
// Communicates intent: this class contains business logic
// May receive additional Spring features in future versions
@Service
public class UserService {
public User createUser(CreateUserRequest request) { return null; }
}
// @Repository — data access / persistence layer:
// Adds exception translation: database-specific exceptions
// (SQLException, HibernateException) are translated to
// Spring's DataAccessException hierarchy automatically
// This makes your service layer independent of the persistence technology
@Repository
public class JdbcUserRepository implements UserRepository {
public User save(User user) { return null; }
}
// @Controller — web MVC controller (returns view names):
@Controller
public class HomeController {
@GetMapping("/")
public String home(Model model) {
return "home"; // resolves to templates/home.html
}
}
// @RestController — REST API controller:
// Meta-annotated with @Controller + @ResponseBody
// Return values serialized to JSON/XML automatically
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) { return null; }
}
// @Configuration — configuration class:
// Also detected by component scanning
// Methods annotated with @Bean register additional beans
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// ALL of these are ultimately @Component:
// Spring treats them identically for bean registration purposes
// The stereotype name communicates intent and enables tooling support@ComponentScan — Controlling What's Scanned
@ComponentScan configures which packages to scan, what to include, and what to exclude. @SpringBootApplication includes @ComponentScan for the root package — usually all you need.
Java
// @SpringBootApplication includes @ComponentScan for the annotated class's package:
@SpringBootApplication // scans com.example.myapp and all sub-packages
public class MyappApplication {
public static void main(String[] args) {
SpringApplication.run(MyappApplication.class, args);
}
}
// Explicit @ComponentScan — customize the scan:
@Configuration
@ComponentScan(basePackages = {
"com.example.myapp", // your application
"com.example.shared" // shared library
})
public class AppConfig { }
// @ComponentScan with type-safe base package classes:
// Specify a class IN the package you want to scan — refactoring-safe:
@ComponentScan(basePackageClasses = {
MyappApplication.class, // scans com.example.myapp
SharedLibraryMarker.class // scans com.example.shared
})
public class AppConfig { }
// Multiple @ComponentScan (Java 8+ repeatable annotations):
@ComponentScan("com.example.myapp")
@ComponentScan("com.example.shared")
public class AppConfig { }
// Customize via @SpringBootApplication:
@SpringBootApplication(
scanBasePackages = {"com.example.myapp", "com.example.shared"},
scanBasePackageClasses = {MyappApplication.class}
)
public class MyappApplication { }
// CRITICAL: The @SpringBootApplication class must be in the ROOT package:
// com/example/myapp/MyappApplication.java → scans com.example.myapp.*
// If placed in a sub-package, sibling packages are missed:
// com/example/myapp/config/MyappApplication.java → only scans config package!Include and Exclude Filters
Component scanning filters let you include or exclude specific classes from scanning — by annotation, type, regex pattern, or custom logic. Filters give precise control over what Spring discovers.
Java
// includeFilters — scan additional classes beyond the default:
@ComponentScan(
basePackages = "com.example",
includeFilters = {
// Include classes annotated with @MyCustomAnnotation:
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = MyCustomAnnotation.class
),
// Include classes that are subtypes of a specific type:
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = BaseRepository.class
)
},
useDefaultFilters = false // disable default @Component detection
// Now ONLY classes matching includeFilters are scanned
)
public class CustomScanConfig { }
// excludeFilters — skip specific classes during scanning:
@ComponentScan(
basePackages = "com.example",
excludeFilters = {
// Exclude classes annotated with @Generated:
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Generated.class
),
// Exclude a specific class:
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = LegacyService.class
),
// Exclude by regex pattern on class name:
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = ".*Legacy.*"
),
// Exclude by AspectJ type pattern:
@ComponentScan.Filter(
type = FilterType.ASPECTJ,
pattern = "com.example..*Legacy*"
)
}
)
public class AppConfig { }
// Custom filter — exclude classes in a specific sub-package:
public class ExcludeTestPackageFilter implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader,
MetadataReaderFactory factory) throws IOException {
String className = metadataReader.getClassMetadata().getClassName();
return className.contains(".test."); // true = exclude this class
}
}
@ComponentScan(
basePackages = "com.example",
excludeFilters = @ComponentScan.Filter(
type = FilterType.CUSTOM,
classes = ExcludeTestPackageFilter.class
)
)
public class AppConfig { }
// In Spring Boot tests — limit what's scanned:
@SpringBootTest
@ComponentScan(
basePackages = "com.example.myapp",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {ScheduledTasks.class, KafkaConsumer.class}
)
)
class ServiceLayerTest { }Custom Stereotype Annotations
You can create your own stereotype annotations by meta-annotating with @Component. Custom stereotypes carry additional metadata, enable consistent configuration, and communicate domain-specific roles.
Java
// Custom stereotype — @DomainService:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service // meta-annotated with @Service — detected by component scan
@Transactional // all DomainService beans get transactions by default
public @interface DomainService {
String value() default "";
}
// Use the custom stereotype:
@DomainService
public class OrderDomainService {
// Registered as a Spring bean automatically
// Has @Transactional applied automatically
// Communicates: this is a domain service, not an application service
public Order placeOrder(PlaceOrderCommand command) { return null; }
}
// Custom stereotype — @UseCase (hexagonal architecture):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
@Transactional
public @interface UseCase { }
@UseCase
public class RegisterUserUseCase {
public void execute(RegisterUserCommand command) { }
}
// Custom stereotype — @Adapter (hexagonal architecture):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Adapter { }
@Adapter
public class PostgresUserRepository implements UserRepository {
public User save(User user) { return null; }
}
// Custom stereotype — @EventHandler with ordering:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface EventHandler {
int order() default 0;
}
@EventHandler(order = 1)
public class AuditEventHandler {
@EventListener
public void handle(UserRegisteredEvent event) { }
}
// Custom stereotype — @WebAdapter for REST controllers:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@RequestMapping
public @interface WebAdapter {
String value() default "";
}
@WebAdapter("/api/users")
public class UserWebAdapter {
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) { return null; }
}Scanning Performance and Lazy Scanning
Component scanning reads every class file in the specified packages. On large classpaths, this can slow startup. Spring Boot 2.2+ introduced lazy initialization and scanning optimizations.
Java
// Performance impact of scanning:
// Spring uses ASM bytecode reader — classes are NOT loaded by the JVM during scanning
// Only class metadata is read from .class files
// Still: reading thousands of .class files takes time
// Measure scanning cost:
// application.properties:
// logging.level.org.springframework.context.annotation=DEBUG
// Prints each discovered component — helps identify unexpected inclusions
// Optimize scanning — be specific about packages:
// BAD: scan the entire classpath root
@ComponentScan("com") // scans EVERY class in every com.* package
// GOOD: scan only your application packages
@ComponentScan("com.example.myapp") // only your code
// Spring Boot index — pre-compute component scan results at compile time:
// Add to pom.xml:
// groupId: org.springframework
// artifactId: spring-context-indexer
// optional: true
// Generates META-INF/spring.components at compile time
// Spring uses the index at startup instead of scanning — much faster for large apps
// Lazy initialization — defer ALL bean creation until first use:
// application.properties:
// spring.main.lazy-initialization=true
// Reduces startup time dramatically
// First request to any endpoint will be slower (beans created on demand)
// Selective lazy initialization — keep critical beans eager:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setLazyInitialization(true); // all beans lazy by default
app.run(args);
}
}
// Override lazy for specific beans that MUST be eager:
@Component
@Lazy(false) // eager even when global lazy is enabled
public class DatabaseMigrationRunner {
@PostConstruct
public void migrate() {
// Must run at startup, before any requests
}
}
// Spring Boot 2.4+ — startup actuator endpoint shows scanning time:
// GET /actuator/startup
// Shows timeline of each startup step including component scan durationScanning in Multi-Module Projects
Multi-module Maven/Gradle projects require careful attention to package structure and scan configuration. Each module's components must be reachable from the root scan package.
Java
// Multi-module project structure:
// my-app/
// myapp-core/ → com.example.core
// myapp-web/ → com.example.web
// myapp-data/ → com.example.data
// myapp-boot/ → com.example.boot (entry point module)
// Option 1 — Shared root package (simplest):
// All modules use sub-packages of com.example:
// com.example.core.service.UserService
// com.example.data.repository.UserRepository
// com.example.web.controller.UserController
// @SpringBootApplication in com.example scans all sub-packages:
package com.example.boot;
@SpringBootApplication(scanBasePackages = "com.example")
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
// Option 2 — Separate packages, explicit scan:
@SpringBootApplication(scanBasePackages = {
"com.example.boot",
"com.example.core",
"com.example.web",
"com.example.data"
})
public class MyAppApplication { }
// Option 3 — spring.factories / AutoConfiguration.imports in each module:
// Each module provides its own auto-configuration
// Main app picks it up via @EnableAutoConfiguration
// Scales well — adding a module is adding a dependency, no scan changes
// Option 4 — @Import in the main @Configuration:
@SpringBootApplication
@Import({
CoreModuleConfig.class, // imports all beans from core module
DataModuleConfig.class, // imports all beans from data module
})
public class MyAppApplication { }
// @Configuration in each module:
@Configuration
@ComponentScan("com.example.core")
public class CoreModuleConfig { }
@Configuration
@ComponentScan("com.example.data")
public class DataModuleConfig { }
// Testing individual modules without the full app:
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = CoreModuleConfig.class)
class CoreModuleTest {
@Autowired UserService userService; // only core beans loaded
}Conditional Component Registration
Components can be conditionally registered during scanning using @Conditional annotations. This lets you include or exclude beans based on the environment, classpath, properties, or any custom condition.
Java
// @ConditionalOnProperty — register only when property is set:
@Service
@ConditionalOnProperty(
name = "app.feature.payments",
havingValue = "true",
matchIfMissing = false
)
public class PaymentProcessingService {
// Only registered when app.feature.payments=true in application.properties
}
// @ConditionalOnClass — register only when a class is on the classpath:
@Service
@ConditionalOnClass(name = "com.stripe.Stripe")
public class StripeIntegrationService {
// Only registered when the Stripe SDK is on the classpath
}
// @ConditionalOnMissingBean — register only when no other bean of this type exists:
@Service
@ConditionalOnMissingBean(PaymentService.class)
public class DefaultPaymentService implements PaymentService {
// Default implementation — replaced if a custom PaymentService is defined
}
// @Profile — register only for specific profiles:
@Service
@Profile("dev")
public class MockEmailService implements EmailService {
public void send(String to, String subject, String body) {
System.out.println("MOCK EMAIL to: " + to + " | " + subject);
}
}
@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
public void send(String to, String subject, String body) {
smtpClient.sendEmail(to, subject, body);
}
}
// Custom @Conditional — register based on any logic:
public class OnLinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
}
@Service
@Conditional(OnLinuxCondition.class)
public class LinuxSpecificService {
// Only registered when running on Linux
}
// Combining conditions:
@Service
@Profile("prod")
@ConditionalOnProperty(name = "feature.analytics", havingValue = "true")
@ConditionalOnClass(name = "com.example.analytics.AnalyticsClient")
public class ProductionAnalyticsService {
// Registered ONLY when: prod profile AND property set AND class available
}