We've reached the point where the BeanDefinitionLoader (Part 3) has registered the BeanDefinition for your main class. But that's where the magic of @SpringBootApplication only begins — the class itself is marked with this annotation, and it kicks off three independent mechanisms. Let's break down each one.
4.1. @SpringBootApplication — a Composed Annotation
Let's look at its definition (Spring Boot 3.x):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
public @interface SpringBootApplication {
// aliases for @ComponentScan
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
// aliases for @EnableAutoConfiguration
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
}
Key point: @SpringBootApplication is not "one annotation that does everything" — it's three separate annotations, each kicking off its own mechanism. And each of them is processed at a different time, by different classes.
4.2. The Three Mechanisms and When They Fire
| Annotation | What it does | When it fires | Processed by |
|---|---|---|---|
@SpringBootConfiguration |
Marks the class as @Configuration
|
ConfigurationClassPostProcessor |
ConfigurationClassParser |
@ComponentScan |
Looks for @Components in the package |
ConfigurationClassPostProcessor |
ComponentScanAnnotationParser |
@EnableAutoConfiguration |
Loads auto-configurations | ConfigurationClassPostProcessor |
AutoConfigurationImportSelector |
All three are processed inside ConfigurationClassPostProcessor — a BeanFactoryPostProcessor invoked by refreshContext() during invokeBeanFactoryPostProcessors(). That is, before regular beans are created.
The diagram:
refreshContext()
└─ AbstractApplicationContext.refresh()
└─ invokeBeanFactoryPostProcessors()
└─ ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()
└─ ConfigurationClassParser.parse()
├─ @SpringBootConfiguration → registerAsConfigClass()
├─ @ComponentScan → doProcessConfigurationClass() → scan()
└─ @EnableAutoConfiguration → AutoConfigurationImportSelector
4.3. @SpringBootConfiguration — "I'm a Configuration"
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
It's just @Configuration with a label saying "I'm Spring Boot's main configuration class." Why a separate annotation? To:
- Distinguish the main class from other
@Configurationclasses (used in tests:@SpringBootTestlooks specifically for@SpringBootConfiguration). - Add
@Indexed(more on that below).
proxyBeanMethods = true (the default) means the class will be wrapped in a CGLIB proxy, and calls to @Bean methods within the class will be intercepted — this guarantees that a bean is created only once (singleton semantics).
4.4. @ComponentScan — How Scanning Works
4.4.1. Where the Base Package Comes From
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
// ...
}
If neither basePackages nor basePackageClasses is specified, scanning starts from the package of the class the annotation is on.
package com.example.myapp; // ← the root package
@SpringBootApplication
public class MyApplication { }
Everything under com.example.myapp and its sub-packages (com.example.myapp.service, com.example.myapp.web, etc.) will be found.
4.4.2. Why the "Root Package" Matters
If you put your main class in com.example.myapp and a service in com.other.service, the service won't be found. This isn't a Spring bug — it's standard scanning behavior.
Correct:
com.example.myapp
├─ MyApplication.java ← @SpringBootApplication
├─ service/
│ └─ UserService.java ← @Service ✅ will be found
└─ web/
└─ UserController.java ← @Controller ✅ will be found
Incorrect:
com.example.myapp
└─ MyApplication.java
com.other.service ← outside the root package
└─ UserService.java ← @Service ❌ NOT found
The exception is if you specify scanBasePackages explicitly:
@SpringBootApplication(scanBasePackages = {"com.example.myapp", "com.other.service"})
4.4.3. basePackageClasses — a Type-Safe Alternative
Instead of strings, you can pass marker classes:
@SpringBootApplication(
scanBasePackageClasses = {MyApplication.class, AnotherMarker.class}
)
The packages are derived automatically from the packages of the given classes. This is safer: if you refactor (rename a package), the compiler will catch the mistake.
4.4.4. What Exactly the Scanner Looks For
ClassPathBeanDefinitionScanner looks for classes marked with stereotype annotations:
| Annotation | Semantics |
|---|---|
@Component |
the base stereotype |
@Service |
business logic |
@Repository |
data access (+ exception translation) |
@Controller |
MVC controller |
@RestController |
@Controller + @ResponseBody
|
@Configuration |
configuration class (also a component) |
Important: all of them are meta-annotated with @Component. The scanner looks for @Component following the chain of meta-annotations.
4.4.5. The Mechanics of Scanning — ClassPathScanningCandidateComponentProvider
This class does the heavy lifting:
public class ClassPathScanningCandidateComponentProvider {
private final List<TypeFilter> includeFilters = new ArrayList<>();
private final List<TypeFilter> excludeFilters = new ArrayList<>();
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
// 1. Collect all .class files in the package
// 2. For each, read the metadata (via ASM, not reflection!)
// 3. Apply the include/exclude filters
// 4. Return the matching BeanDefinitions
}
}
Key point: the metadata is read via ASM (SimpleMetadataReader), not via Class.forName(). This means the scanner doesn't load classes during scanning — it only reads the headers of .class files. Reflection comes into play later, at bean creation time.
4.5. Filters: includeFilters and excludeFilters
4.5.1. FilterType — Five Strategies
public enum FilterType {
ANNOTATION, // by annotation
ASSIGNABLE_TYPE, // by type (class/interface)
ASPECTJ, // by AspectJ expression
REGEX, // by regex on the class name
CUSTOM // your own TypeFilter
}
Examples:
// ANNOTATION: include everything annotated with @MyService
@ComponentScan(includeFilters = @Filter(type = FilterType.ANNOTATION,
classes = MyService.class))
// ASSIGNABLE_TYPE: include everything that implements Animal
@ComponentScan(includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE,
classes = Animal.class))
// CUSTOM: your own logic
@ComponentScan(includeFilters = @Filter(type = FilterType.CUSTOM,
classes = MyCustomFilter.class))
When multiple classes are listed in a single @Filter, OR logic applies: "the type is annotated with @foo OR @bar".
4.5.2. useDefaultFilters — an Important Flag
@ComponentScan(
useDefaultFilters = false,
includeFilters = @Filter(type = FilterType.ANNOTATION,
classes = MyService.class)
)
By default, useDefaultFilters = true, meaning the scanner automatically includes @Component, @Service, @Repository, and @Controller. If you want to match only your own annotations, set it to false.
4.5.3. TypeExcludeFilter — Spring Boot's Extensible Filter
Recall that @SpringBootApplication contains:
@ComponentScan(excludeFilters = {
@Filter(type = CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = CUSTOM, classes = AutoConfigurationExcludeFilter.class)
})
TypeExcludeFilter is an extension point. It doesn't filter anything on its own. Instead, at startup it fetches all beans implementing TypeExcludeFilter from the BeanFactory and applies their match() methods:
public class TypeExcludeFilter implements TypeFilter, BeanFactoryAware {
@Override
public boolean match(MetadataReader metadataReader,
MetadataReaderFactory metadataReaderFactory) {
if (this.beanFactory instanceof ListableBeanFactory lbf) {
Collection<TypeExcludeFilter> filters = lbf.getBeansOfType(
TypeExcludeFilter.class).values();
return filters.stream().anyMatch(filter ->
filter.match(metadataReader, metadataReaderFactory));
}
return false;
}
}
This lets you plug in your own filters via @Bean:
@Bean
MyExcludeFilter myExcludeFilter() {
return new MyExcludeFilter();
}
Your filter will be applied automatically during scanning. It's a powerful but little-known mechanism.
4.5.4. AutoConfigurationExcludeFilter — the Second Built-in Filter
This filter excludes auto-configurations from scanning. The logic:
@Override
public boolean match(MetadataReader metadataReader,
MetadataReaderFactory factory) {
return isConfiguration(metadataReader) && isAutoConfiguration(metadataReader);
}
In other words, if a class is a @Configuration and is an auto-configuration (registered in .imports / spring.factories), it is not picked up by component scanning. Auto-configurations are loaded separately, via @EnableAutoConfiguration. Without this filter, they would be registered twice.
4.6. @AutoConfigurationPackage — the "Hidden" Annotation
Let's look at the definition of @EnableAutoConfiguration once more:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// ...
}
@AutoConfigurationPackage is what remembers the package of your main class:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage { }
AutoConfigurationPackages.Registrar registers an AutoConfigurationPackages.BasePackages bean holding the list of base packages. Why? So that other auto-configurations know where to look for your entities:
-
Spring Data JPA (
@EntityScan) — uses the base package to find@Entityclasses. - Spring Data MongoDB — the same.
- Spring Boot DevTools — for restarts.
If you put an @Entity outside the root package, JPA won't find it — because @AutoConfigurationPackage only remembered com.example.myapp.
4.7. @Indexed and META-INF/spring.components — Speeding Up Scanning
4.7.1. The Problem
At startup, Spring Boot scans the classpath. In large projects this can take seconds — especially when there are many JARs on the classpath.
4.7.2. The Solution: @Indexed
Spring Framework 5.0 introduced @Indexed:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Indexed { }
All stereotype annotations (@Component, @Service, @Repository, @Controller, @Configuration, @SpringBootConfiguration) are meta-annotated with @Indexed.
4.7.3. How It Works
At compile time, an annotation processor (spring-context-indexer) generates the file:
META-INF/spring.components
Example content:
com.example.myapp.service.UserService=org.springframework.stereotype.Component
com.example.myapp.web.UserController=org.springframework.stereotype.Component
com.example.myapp.config.AppConfig=org.springframework.context.annotation.Configuration
At startup, CandidateComponentsIndexLoader reads this file and builds a CandidateComponentsIndex. Instead of scanning the classpath, ClassPathScanningCandidateComponentProvider simply reads the index.
4.7.4. How to Enable It
Option 1 — the annotation on the main class:
@SpringBootApplication
@Indexed
public class MyApplication { }
Option 2 — a dependency + annotation processor:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>
Important caveat:
@Indexedswitches on the index. If an index exists, scanning goes through it. If an index exists but you've added a new@Componentwithout recompiling, it won't be found. The index must be rebuilt on every build.
4.8. Diagram: the Full Path of @ComponentScan
ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry()
│
└─ ConfigurationClassParser.parse()
│
├─ processConfigurationClass(MyApplication.class)
│ │
│ ├─ @SpringBootConfiguration → registerAsConfigClass()
│ │
│ ├─ @ComponentScan → doProcessConfigurationClass()
│ │ │
│ │ ├─ ComponentScanAnnotationParser.parse()
│ │ │ ├─ basePackages = {pkg(MyApplication)}
│ │ │ ├─ excludeFilters = [TypeExcludeFilter, AutoConfigurationExcludeFilter]
│ │ │ └─ useDefaultFilters = true
│ │ │
│ │ └─ ClassPathBeanDefinitionScanner.doScan(basePackages)
│ │ │
│ │ ├─ CandidateComponentsIndexLoader.loadIndex() ← @Indexed
│ │ │ └─ if the index exists → read spring.components
│ │ │
│ │ └─ findCandidateComponents(basePackage)
│ │ ├─ for each .class:
│ │ │ ├─ SimpleMetadataReader (ASM)
│ │ │ ├─ isCandidateComponent() → include/exclude
│ │ │ └─ if it passes → BeanDefinition
│ │ │
│ │ └─ registerBeanDefinition()
│ │
│ └─ @EnableAutoConfiguration → AutoConfigurationImportSelector
│ └─ auto-configuration loading ← Part 5
│
└─ ...
4.9. Spring Boot 3 vs Spring Boot 4
| Aspect | Spring Boot 3.x | Spring Boot 4.x |
|---|---|---|
@ComponentScan |
Unchanged | Unchanged |
@SpringBootApplication |
Unchanged | Unchanged |
| New mechanism | — |
BeanRegistrar — programmatic bean registration as an alternative to scanning |
| Modularity |
spring-boot-autoconfigure — a monolith |
47 modular JARs, each with its own scanning |
| Starters | spring-boot-starter-web |
spring-boot-starter-webmvc |
@Indexed |
Works | Works, but less relevant due to AOT |
The main thing in Boot 4: the core behavior of @ComponentScan and @SpringBootApplication hasn't changed. But there's a new player — BeanRegistrar, an alternative way to register beans without scanning:
public class MyBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
if (env.acceptsProfiles(Profiles.of("dev"))) {
registry.registerBean("myService", MyService.class);
}
}
}
This is closer to the AOT approach: beans are registered programmatically rather than discovered by scanning.
4.10. Key Takeaways
-
@SpringBootApplication= three annotations, all processed byConfigurationClassPostProcessorduringrefresh(). - The base package for scanning = the main class's package. Anything outside it won't be found unless you specify
scanBasePackagesexplicitly. -
ClassPathBeanDefinitionScannerreads metadata via ASM without loading the classes. -
TypeExcludeFilteris an extensible point: you can add your own filter via a@Beanmethod. -
AutoConfigurationExcludeFilterkeeps auto-configurations out of component scanning so they aren't registered twice. -
@AutoConfigurationPackageremembers the base package for JPA/Spring Data and other auto-configurations. -
@Indexed+META-INF/spring.componentsspeeds up scanning in large projects. -
Boot 4: the core behavior is unchanged, but
BeanRegistrarhas appeared as an alternative to scanning.
Top comments (0)