When you create a project with Spring Initializr, the main class comes with @SpringBootApplication attached. You probably know that “adding this makes it work,” but explaining what it actually does under the hood is surprisingly hard.
To cut to the chase, @SpringBootApplication is a composed annotation that bundles three annotations: @SpringBootConfiguration, @EnableAutoConfiguration, and @ComponentScan. Once you understand these three, you will know which attribute to reach for in situations like “I moved the main class and now Beans can’t be found” or “I want to remove an unnecessary auto-configuration.”
If the relationship between Spring and Spring Boot is still a little fuzzy for you, reading the difference between Spring and Spring Boot first will make this article easier to follow.
The environment used for verification is as follows.
- Spring Boot 3.5.x
- Java 21
- Gradle (Kotlin DSL)
What Is @SpringBootApplication? A Composed Annotation Bundling Three Annotations
Let’s start with the main class generated by Initializr.
package com.example.app;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
The DemoApplication.class passed to SpringApplication.run is the starting point of the configuration. Let’s take a look at the definition of @SpringBootApplication attached to this class. The following is an excerpt of the key parts from the Spring Boot 3.x source (some attributes such as nameGenerator are omitted).
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
Notice that three more annotations sit on top of the annotation definition itself. An annotation built by combining other annotations like this is called a composed annotation (or meta-annotation). The attributes are simply forwarded to the original annotations via @AliasFor, so the actual substance is as shown in the table below.
| Annotation | Role | Attributes you can touch |
|---|---|---|
| @SpringBootConfiguration | Treats the main class as a configuration class | proxyBeanMethods |
| @EnableAutoConfiguration | Infers configuration from the classpath and applies it automatically | exclude / excludeName |
| @ComponentScan | Searches for Beans under the main class’s package | scanBasePackages / scanBasePackageClasses |
@SpringBootConfiguration Turns the Main Class into a Configuration Class
The source of @SpringBootConfiguration is simple: it is just @Configuration with one extra marker.
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
In other words, the main class is also an ordinary @Configuration class, so you can write @Bean methods directly in it. It works, but in practice it is more common to split them into separate @Configuration classes. The guidelines for choosing between the two are summarized in What Are @Configuration / @Bean?.
So why does a dedicated annotation exist at all? The answer is testing. @SpringBootTest searches upward from the test class’s package for a class annotated with @SpringBootConfiguration and uses it as the configuration class. The reason it works without specifying classes is precisely this marker. This is covered in more detail in Choosing the Right webEnvironment.
@EnableAutoConfiguration Infers Configuration from the Classpath
Just by adding spring-boot-starter-web as a dependency, you get an embedded Tomcat and a DispatcherServlet, right? @EnableAutoConfiguration is the entry point for that “it works even though I configured nothing” phenomenon. It looks at the libraries on the classpath and the Beans already registered, decides “in this situation, this configuration is probably needed,” and adds configuration classes automatically.
The division of labor is that Starters assemble the dependencies and auto-configuration configures them. For what is inside a Starter, see What Is a Spring Boot Starter?, and for the internal mechanism that decides “whether it is needed,” see How Auto-Configuration Works. This article focuses only on controlling it via exclude, which is described later.
@ComponentScan Searches for Beans Under the Main Class’s Package
Looking at the source above, you will notice that basePackages is not specified on @ComponentScan. In this case, Spring uses the package of the annotated class as the starting point and scans it along with its subpackages. In other words, where you place the main class directly determines the scope of Bean detection.
com.example.app
├── DemoApplication.java ← starting point
├── controller
│ └── GreetingController.java
├── service
│ └── GreetingService.java
└── repository
└── GreetingRepository.java
Since the main class sits directly under com.example.app, all of controller / service / repository are within scope. What gets detected is @Component and the annotations that inherit from it, such as @Service / @Repository / @Controller. For @Component itself, I will defer to What Is @Component?.
Incidentally, the two excludeFilters are: TypeExcludeFilter, a hook for excluding out-of-scope Beans in test slices (such as @WebMvcTest), and AutoConfigurationExcludeFilter, a filter that prevents auto-configuration classes from being picked up by regular scanning. These will come up briefly in the experiment later on.
Placing the Main Class Anywhere but the Root Makes Beans Undiscoverable
Let’s actually break it. First, prepare a Service and a Controller.
package com.example.app.service;
@Service
public class GreetingService {
public String greet(String name) {
return "Hello, " + name;
}
}
package com.example.app.controller;
@RestController
public class GreetingController {
private final GreetingService greetingService;
public GreetingController(GreetingService greetingService) {
this.greetingService = greetingService;
}
@GetMapping("/greet")
public String greet() {
return greetingService.greet("Spring");
}
}
With this in place, move the main class to com.example.app.config.
package com.example.app.config;
@SpringBootApplication
public class DemoApplication {
@Bean
CommandLineRunner runner(GreetingService greetingService) {
return args -> System.out.println(greetingService.greet("Spring"));
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
When you start it, it fails like this.
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of method runner in com.example.app.config.DemoApplication required a bean of type 'com.example.app.service.GreetingService' that could not be found.
Action:
Consider defining a bean of type 'com.example.app.service.GreetingService' in your configuration.
Because the starting point of the scan became com.example.app.config, both service and controller fell out of scope. The slightly scary case is when only the Controller falls out of scope: if nobody injects the Controller, startup succeeds and /greet simply returns a 404, which makes the cause hard to notice.
The fix is simple: just move the main class back to the root package com.example.app. Think of scanBasePackages, described next, as strictly an exceptional measure. Note that “APPLICATION FAILED TO START” also appears for port conflicts and duplicate Beans, so for how to narrow down the cause, refer to Troubleshooting Spring Boot Startup Failures.
Changing the Scan Scope with scanBasePackages / scanBasePackageClasses
If you really cannot move the main class, you can specify the scan scope explicitly.
// 文字列で指定
@SpringBootApplication(scanBasePackages = "com.example.app")
public class DemoApplication { ... }
// クラス参照で指定(こちらを推奨)
@SpringBootApplication(scanBasePackageClasses = AppRoot.class)
public class DemoApplication { ... }
For scanBasePackageClasses, you pass a class that lives in the package you want to use as the starting point. Typos in package names are caught at compile time, and it follows IDE refactoring, so if you are going to use one, prefer this.
One caveat: once you specify scanBasePackages, the default “main class’s package” is removed from the starting points. A Bean placed next to the main class can suddenly become undiscoverable.
The decision criteria are as follows.
- As a rule, place the main class in the root package. That alone makes the attributes unnecessary
- Use scanBasePackageClasses only when you need to include a package from another module in a multi-module project, or when you cannot move the existing structure
Multi-module setups and patterns using an explicit @ComponentScan are covered comprehensively in How to Fix Beans Not Found with @ComponentScan.
Removing Unnecessary Auto-Configuration with exclude / excludeName
Next are the attributes on the @EnableAutoConfiguration side. A common case is adding spring-boot-starter-data-jpa without having a DB ready yet, causing startup to fail with Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. If you want to remove it temporarily, specify the auto-configuration class with exclude.
// クラス参照で除外(通常はこちらで十分)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class DemoApplication { ... }
// FQCNの文字列で除外
@SpringBootApplication(excludeName =
"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration")
public class DemoApplication { ... }
With this, startup succeeds. However, this exclusion only works while you are not yet using @Entity or JpaRepository. If there is any place where a repository is injected, that Bean will now be the one that cannot be found and startup will fail, so treat this as a stopgap until the DB is ready.
exclude requires the class to exist at compile time. excludeName is for specifying auto-configurations that may not be on the classpath, and its use is limited to situations like shared libraries where “you don’t know which dependencies are present.”
Another option is specifying it via properties. If you want to switch per profile, this is easier to handle than the annotation.
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
# 次のセクションで使う自動構成レポートの出力
debug=true
Reading the Auto-Configuration Report with —debug
“Why was an auto-configuration enabled, and why was it disabled” is all printed when you start with --debug. The debug=true shown above does the same thing.
./gradlew bootRun --args='--debug'
# jarで起動する場合(jar名はプロジェクト名とバージョンに合わせて読み替えてください)
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --debug
A heading called “CONDITIONS EVALUATION REPORT” appears in the startup log, so look for that.
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
DispatcherServletAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
- found 'session' scope (OnWebApplicationCondition)
Negative matches:
-----------------
BatchAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'org.springframework.batch.core.launch.JobLauncher' (OnClassCondition)
Exclusions:
-----------
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
Unconditional classes:
----------------------
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
The report is divided into four sections, and the first three are the ones you will mainly look at.
- Positive matches are auto-configurations whose conditions were met and were enabled. The DispatcherServlet class was found, so the MVC auto-configuration is enabled
- Negative matches are auto-configurations that were disabled, along with the reasons. Spring Batch is not included, so BatchAutoConfiguration was dropped
- Exclusions are the ones explicitly removed via exclude or properties. The DataSourceAutoConfiguration from earlier shows up here
- Unconditional classes are auto-configurations that are always applied with no conditions
The trick to reading it is, when “the auto-configuration I expected isn’t taking effect,” first search for the class name in Negative matches and look at the did not find reason. The meaning of the conditional annotations is explained in How Auto-Configuration Works.
As an Experiment, Rewriting It as the Three Annotations
Now that we have come this far, let’s remove @SpringBootApplication and line up the three annotations directly. The main class has been moved back to the root package.
package com.example.app;
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class DemoApplication {
@Bean
CommandLineRunner printBeans(ApplicationContext context) {
return args -> Arrays.stream(context.getBeanDefinitionNames())
.filter(n -> n.contains("greeting") || n.contains("dispatcherServlet"))
.forEach(System.out::println);
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
When you start it, our own greetingController and greetingService are printed together with dispatcherServlet from auto-configuration. I also planted the same CommandLineRunner in the @SpringBootApplication version for comparison, and the output was identical.
greetingController
greetingService
dispatcherServlet
dispatcherServletRegistration
It is not completely identical, though, and the difference shows up in tests. @Configuration lacks the “marker” that @SpringBootTest looks for, so this main class is not auto-detected and you must specify @SpringBootTest(classes = DemoApplication.class) explicitly. Since the two excludeFilters filters are omitted, exclusion in test slices may also stop working.
This experiment is meant to help you understand what is inside the composed annotation. In production code, the right answer is to simply use @SpringBootApplication.
About the proxyBeanMethods Attribute
The proxyBeanMethods attribute that appeared in the table (default true) is forwarded as-is to the attribute of the same name on @Configuration, and controls whether a CGLIB proxy is applied to the main class. Setting it to false makes startup slightly lighter, but it has the side effect that calling @Bean methods from one another within the main class produces separate instances. Normally, leaving it at the default is fine. The behavior of CGLIB proxies is covered in detail in the @Configuration / @Bean article.
Summary
@SpringBootApplication turned out to be a single gateway for touching the attributes of three annotations.
| Component | Corresponding attributes | When to use |
|---|---|---|
| @SpringBootConfiguration | proxyBeanMethods | Basically leave it alone |
| @EnableAutoConfiguration | exclude / excludeName | You want to remove unnecessary auto-configuration |
| @ComponentScan | scanBasePackages / scanBasePackageClasses | You cannot move the main class |
When in doubt, here are the four decision criteria.
- Place the main class in the root package
- Use scanBasePackageClasses only when you cannot move the structure
- Exclude auto-configuration with exclude or
spring.autoconfigure.exclude - If you are not sure whether it is taking effect, check the report with
--debug
I hope the annotation that used to be a “magic incantation” has become a tool whose attributes you can now follow and understand. If you want to dig deeper into how auto-configuration evaluates its conditions, head to How Auto-Configuration Works, and if Beans cannot be found in your multi-module project, continue to How to Fix Beans Not Found with @ComponentScan.