@SpringBootApplication: One Annotation, Three Meanings

bee2026-10-0852 min read0 views
One annotation, three jobs: @SpringBootConfiguration, @EnableAutoConfiguration and @ComponentScan. Unpack each layer, plus scan boundaries, exclusions and where to put the main class.
1 / 143
Section
0. The 30-second version
2 / 143

Last article you got Hello World running, but you never actually pried open that one line @SpringBootApplication. This article does exactly that: split the single annotation into three responsibilities and make each boundary explicit. Why a whole article for it? Because the two most demoralising Boot errors — "bean not found" and "404" — are nine times out of ten not wrong business code but one of those three responsibilities failing to cover you. Once you know the boundaries, you hold a debugging map.

3 / 143
类比|Analogy

@SpringBootApplication is the company's three-in-one access card. The first gate (@SpringBootConfiguration) confirms "this card may define workstations"; the second (@ComponentScan) decides "people from your own department may enter this floor"; the third (@EnableAutoConfiguration) decides "whether the meeting room, water dispenser and printer the property team pre-installed get switched on". Carrying all three on one card is convenient, but when you end up on the wrong floor you must know which turnstile stopped you — that is the entire content of this article.

4 / 143
Diagram
Figure · Chapter map: the meta-annotation structure
Figure · Chapter map: the meta-annotation structure
5 / 143

After this article you should be able to answer three questions:

6 / 143
  • When a bean is missing, which annotation's scope failed to cover it, and how do I reverse-engineer that from the fully-qualified class name in the error?
  • Once I set scanBasePackages, does the main class's own package still get scanned?
  • What occasions call for exclude versus excludeName versus spring.autoconfigure.exclude?
7 / 143
Section
1. Starting from a "bean not found" error
8 / 143

You create a Spring Boot project, write a UserService annotated with @Service, inject it into a UserController, and startup fails outright:

9 / 143
text
Description:Field userService in com.example.demo.web.UserController required a bean of type'com.example.demo.service.UserService' that could not be found.
10 / 143

The class is fine, the annotation is fine, it compiles — so why is the bean missing? Nine times out of ten the problem is not UserService at all, but the fact that the @SpringBootApplication on your main class never scanned the package it should have.

11 / 143

The best way to understand this is to pry apart that "three-in-one" annotation. In Spring Boot 3 it looks like this:

12 / 143
Code
Codejava
@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 {    @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 {};}
Notes
  • @SpringBootConfiguration: declares "this is a configuration class", so the main class itself can define beans with @Bean
  • @EnableAutoConfiguration: flips the master switch for auto-configuration and imports the configuration classes shipped by your starters
  • @ComponentScan: scans the current package and its subpackages, collecting your own @Component / @Service / @Repository / @Controller into the container
  • The four @AliasFor entries: forward this single annotation's attributes to the three inner annotations, so @SpringBootApplication(exclude = ...) is exactly the same as writing exclude on the inner @EnableAutoConfiguration
13 / 143
Diagram
Figure 1 · One annotation, three layers
Figure 1 · One annotation, three layers
14 / 143
Note

@SpringBootApplication contains no business logic. It is syntax sugar that stacks three annotations together. What we call "the entry annotation" really just declares three things to the container: where to find configuration, where to scan beans, and whether to enable auto-configuration.

15 / 143
Section
2. @SpringBootConfiguration: it really is just @Configuration
16 / 143

Strip the outer layer and @SpringBootConfiguration is surprisingly short:

17 / 143
Code
Codejava
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Configuration@Indexedpublic @interface SpringBootConfiguration {    @AliasFor(annotation = Configuration.class)    boolean proxyBeanMethods() default true;}
Notes
  • At its core it is @Configuration. Your main class is therefore a configuration class by nature — you can write @Bean methods on it directly
  • @Indexed feeds the "component index": the compiler writes a META-INF/spring.components list so scanning does not have to open every class file, giving a slightly faster startup. Without it, nothing breaks
  • It carries one extra uniqueness constraint: a Spring Boot app must have exactly one @SpringBootConfiguration. @SpringBootTest relies on it to locate the primary configuration, and two of them fail with "Found multiple @SpringBootConfiguration"

Trap: never casually add @SpringBootConfiguration on top of your own Config class. To declare a configuration class, @Configuration is enough. Adding one extra makes @SpringBootTest fail with "found multiple @SpringBootConfiguration", and the error points at the test while the real cause sits in some business class — a very counter-intuitive hunt.

18 / 143
Section
3. @EnableAutoConfiguration: one @Import is the whole entry point
19 / 143

The third sibling is the auto-configuration switch, and its definition is just as short:

20 / 143
Code
Codejava
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration {    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";    Class<?>[] exclude() default {};    String[] excludeName() default {};}
Notes
  • @AutoConfigurationPackage: registers the main class's package as the "auto-configuration base package", giving JPA entity scanning, MyBatis mapper scanning and friends a shared anchor
  • @Import(AutoConfigurationImportSelector.class): the class that does the real work. It reads META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, filters each entry by conditions, and registers the matching configuration classes — that pipeline is the entire next article; for now just remember the name
  • spring.boot.enableautoconfiguration=false: the global kill switch, used only in tests or when troubleshooting
  • exclude / excludeName: turn off specific auto-configuration classes; Section 6 expands on this

Tip: @EnableAutoConfiguration = "switch" + "selector". The switch is the @Import itself; the selector is AutoConfigurationImportSelector. Burn both names into memory — you will meet them again constantly when reading auto-configuration source code.

21 / 143

That has a testable corollary: the ticket to auto-configuration is the classpath, not any switch. Every starter you tick hands a passport to a batch of auto-configuration classes; exclude can only turn away the ones that already got in — it cannot do anything about "the class is simply not there". So use the generator like this: tick Spring Web alone and note the single dependency in the output; add Data JPA plus the MySQL driver and count the new entries, then ask which of them is what lets Section 4's beans get past the condition check at all; finally tick Security — it is the one Section 8 is most often seen excluding, and the output shows you what it drags in.

22 / 143
Generator
GeneratorEvery starter you tick is a batch of auto-configuration ticketspom.xml1 / 9
Tick only what you actually need right now. The claim from Section 3 is verifiable here: more ticks mean more conditions evaluated, slower startup, and more things you may later want to exclude
Output
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Why each choice matters
parentInheriting 3.3.4 starter-parent means no spring-boot-starter-* needs a version; the moment someone adds an explicit version to one starter, that one wins — the most common source of dependency drift.
WebAnything that serves HTTP needs it: DispatcherServlet, embedded Tomcat and JSON mapping come inside this starter.
23 / 143
Section
4. @ComponentScan: the number-one cause of missing beans
24 / 143

Look again at the @ComponentScan inside @SpringBootApplication: it sets no basePackages at all. No value means the default, and the default is a single sentence:

25 / 143

The scan range is the package the main class lives in, plus all of its subpackages.

26 / 143

That sentence explains countless "bean not found" errors. The table below shows whether com.example.demo.service gets scanned, entirely depending on where the main class sits:

27 / 143
Table
Main class locationScans com.example.demo.service?Why
com.example.demo.DemoApplicationYesservice is a subpackage of demo
com.example.DemoApplicationYesservice is still under demo
com.example.demo.web.WebApplicationNoservice is a sibling of web
com.example.demo.web.admin.AdminAppNothe parent demo package is never scanned upward
28 / 143

Placing the main class in the wrong spot is an extremely common and painfully hard-to-find mistake:

29 / 143
java
// the real root package of the project is com.example.demo// ❌ Bad: the main class was dropped into the web subpackagepackage com.example.demo.web;@SpringBootApplicationpublic class DemoApplication {          // only scans com.example.demo.web.*    public static void main(String[] args) {        SpringApplication.run(DemoApplication.class, args);    }}// while com.example.demo.service.UserService is annotated with @Service// → it is not under web, so it is never scanned → NoSuchBeanDefinition on injection
30 / 143

There are two fixes, and the first always wins:

31 / 143
java
// ✅ Fix one: move the main class to the outermost root packagepackage com.example.demo;      // sibling of service / web / dao// ✅ Fix two: if it truly cannot move, widen the scan range explicitly@SpringBootApplication(scanBasePackages = "com.example.demo")public class WebApplication {}
32 / 143

@ComponentScan also supports excludeFilters, used to pick out classes that are scanned but should not be registered:

33 / 143
java
@SpringBootApplication(excludeFilters = {    // exclude by type    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = LegacyConfig.class),    // exclude a whole package by regex    @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.example\\.demo\\.legacy\\..*")})public class DemoApplication {}
34 / 143
Animation
Animation · The scan boundary
Animation · The scan boundary
35 / 143

That animation shows the scope; the figure below shows the price — the same project with the main class moved down exactly one package:

36 / 143
Diagram
Figure · Which package the main class lives in
Figure · Which package the main class lives in
37 / 143

"Downward only, never upward" sounds like common sense until the stack trace lands, and nobody remembers it then. Spread the mechanism over six stepped lines and let the right-hand pane refresh as you go: the base package in force, and who is on the candidate list. Press step repeatedly and watch beats 4 and 6 — the name that never appears is the whole story.

38 / 143
Stepper
StepperStep by step: the @Service is there, so why is the container empty?1 / 6
Six beats. Keep your eye on 'base package' and 'candidate list' — the base package fixed in step 4 decides whether step 6 has anybody to find
Code under debug
1package com.example.demo.web; // you put the main class inside the web subpackage
2@SpringBootApplication // the @ComponentScan inside it declares no basePackages
3public class DemoApplication { /* main */ }
4// the container reads the annotation: default base package = the main class's package = com.example.demo.web
5// recurse over com.example.demo.web.** -> UserController matches, its BeanDefinition is registered
6// UserController's constructor needs UserService -- which lives in com.example.demo.service, outside that subtree
Variables now
main class packagecom.example.demo.web
UserService packagecom.example.demo.service
relation between themsiblings; neither contains the other
Call stack
1compile time: everything is fine
1A package declaration is just a folder. Nothing has failed yet, and that is the first reason this bug hides: javac does not know the scanning rules, it only knows your imports.
39 / 143

Attention: `scanBasePackages` **replaces** the default base package; it does not append to it. Set it too narrowly and packages that used to be scanned vanish; set it to something huge like `com` and startup slows down while you may accidentally scan third-party components. The safest choice is always "keep the main class in the root package and set no property at all".

40 / 143
Section
5. Best practice: the main class always lives outermost
41 / 143

Since the rule is "its own package plus subpackages", the package layout and the main class location are bound by the same rule. The layout that causes the fewest surprises looks like this:

42 / 143
Code
Codetext
com.example.demo├── DemoApplication.java        ← main class outermost (direct child of the root)├── config/                     ← configuration classes│   ├── WebConfig.java│   └── SecurityConfig.java├── controller/                 ← web layer│   └── UserController.java├── service/                    ← business layer│   ├── UserService.java│   └── impl/UserServiceImpl.java├── mapper/                     ← data layer│   └── UserMapper.java├── domain/                     ← domain model│   └── User.java└── common/                     ← shared utilities, exceptions, constants    ├── Result.java    └── GlobalExceptionHandler.java
Notes
  • With the main class in the root package, every subpackage above (config / controller / service / mapper / common) falls inside the scan range — nothing is missed
  • Your own @Configuration classes live under config and are collected automatically, so no manual @Import is needed

Key point: you may organize packages by "technical layer" (controller/service/mapper) or by "business domain" (each of user/order/pay owning its controller, service, and so on). Both work. The one invariant is that the main class must sit outside every package that needs to be scanned.

43 / 143
Section
6. Excluding auto-configuration: three tools, three occasions
44 / 143

More auto-configuration is not automatically better. If you configured your own DataSource with @Bean, or pulled in a starter and do not want its security module just yet, you need to turn a few entries off. Three common approaches:

45 / 143
Table
ApproachSyntaxScopeBest for
exclude / excludeName attribute@SpringBootApplication(exclude = XxxAutoConfiguration.class)this main class onlyexcluding one or two here and there
spring.autoconfigure.excludefully-qualified class names in configuration, comma-separatedglobalno code change / per-environment differences / tests
custom composite annotationyour own annotation wrapping @SpringBootApplication together with the exclusion listteam conventionmany services sharing one exclusion policy
46 / 143

The first, written right on the main class, is compile-time checked:

47 / 143
java
@SpringBootApplication(    exclude = DataSourceAutoConfiguration.class,     // type-based, refactorable, IDE-navigable    excludeName = "org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration")public class DemoApplication {}
48 / 143

The third packages that line into a single team-wide entry annotation:

49 / 143
java
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class })@ComponentScanpublic @interface ServiceApplication {}
50 / 143
Code
Codejava
// every service's main class carries one line of the team convention;// the exclusion policy is maintained in exactly one placepackage com.example.order;@ServiceApplicationpublic class OrderApplication {    public static void main(String[] args) {        SpringApplication.run(OrderApplication.class, args);    }}
Notes

Tip: exclude and excludeName differ only in compile-time visibility — if the class is on the classpath, use exclude (the IDE can refactor it); if it is not (an optional dependency), fall back to the string form excludeName. The configuration approach looks like this in application.yml:

51 / 143
yaml
spring:  autoconfigure:    exclude:      - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
52 / 143

The second tool earns its keep somewhere other than "one less line of code": it can answer per environment, which an annotation never can, because an annotation is frozen at compile time. Excluding the data-source auto-configuration in development and keeping it in production is a one-file change here and impossible there. Use the generator like this: tick Profile first and watch how spring.profiles.active plus the --- document separator moves the two lines above into an environment-specific block; then add the datasource block and think about who wins when an exclusion list and connection settings appear together; finally add logging, because logging.level is the setting you will touch most often while diagnosing auto-configuration.

53 / 143
Generator
GeneratorWhat belongs in the config file rather than on the annotationapplication.yml1 / 4
Tick Profile to see the same file answer differently per environment; then add datasource and logging, and reread the Section 6 table: is this exclusion a code decision or an environment decision?
Output
server:
  port: 8080

spring:
  application:
    name: demo-service

---
spring:
  config:
    activate:
      on-profile: prod
logging:
  level: { root: WARN }
---
spring:
  config:
    activate:
      on-profile: dev
spring:
  jpa:
    show-sql: true
Why each choice matters
profiles + 分档配置Multi-document blocks are split by --- and activated with spring.config.activate.on-profile.
54 / 143
Section
7. @AliasFor: why one annotation equals another
55 / 143

Now back to the question from Section 1: why does @SpringBootApplication(scanBasePackages = "x") work at all, when scanBasePackages clearly belongs to @ComponentScan?

56 / 143

The answer is the @AliasFor meta-annotation. It does two things: declare aliases and forward across annotations. Here is how @SpringBootApplication uses it:

57 / 143
Code
Codejava
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")String[] scanBasePackages() default {};
Notes
  • annotation = ComponentScan.class: tells Spring this attribute actually belongs to @ComponentScan
  • attribute = "basePackages": specifies which attribute of the target it forwards to
  • So when you set scanBasePackages on the outside, Spring reads basePackages on the inside — two names, one value
58 / 143

Because of it, Spring Boot can collapse "three annotations" into one from the user's point of view. Interviews love to ask whether @SpringBootApplication really is three annotations; answering "yes, and it forwards attributes down through @AliasFor" earns you the point.

59 / 143
Key point

@AliasFor has only two forms — two attributes inside the same annotation aliasing each other (such as value ↔ path), or an explicit annotation = X, attribute = "y" for cross-annotation forwarding. It is the foundation on which the whole idea of "composed annotations" is built.

60 / 143

That forwarding takes six frames from the name you type to a scan range that really moved:

61 / 143
Animation
Animation · How one attribute travels into the inner annotation
Animation · How one attribute travels into the inner annotation
62 / 143

@SpringBootApplication carries four @AliasFor declarations, plus one more inside @SpringBootConfiguration — six names to keep straight, which nobody memorises from prose. Play a round instead: the left column is the attribute you write on the main class, the right column is the annotation and attribute it actually changes. A wrong pair explains itself on the spot, and the last two pairs are where people usually slip.

63 / 143
Match
MatchOuter attribute name to the inner annotation it really editsMatched 0/6 · Missed 0
Six hard mappings of name to ownership, both columns shuffled, so position will not save you
Pick a card on the left first
64 / 143
Section
8. Trap: multi-module projects cannot scan sibling modules
65 / 143

Placing the main class correctly is enough inside one module, but multi-module setups trip people up. A typical scene:

66 / 143
text
project├── user-module    (com.example.user)    ← contains UserService└── order-module   (com.example.order)   ← depends on user-module, main class lives here
67 / 143

The order module tries to inject UserService from user-module and gets "bean not found" at startup. The root cause is the same scan rule: order's main class is in com.example.order, so it only scans downward into com.example.order.*, while UserService is in com.example.user — siblings that can never see each other.

68 / 143

Two fixes, pick by team convention:

69 / 143
Table
OptionHowUpsideCost
Shared root packageall modules use com.example.xxx; main class raised to com.examplezero extra annotations, most naturalpackage names must be planned up front
Explicit scanning@SpringBootApplication(scanBasePackages = {"com.example.order", "com.example.user"})no renames, fast to applyupdate it for every new module, easy to miss
Make it an auto-configurationdependent module ships AutoConfiguration.imports to expose its beanstruly self-contained and reusablerequires understanding the next chapter
70 / 143
Code
Codejava
// Option two: explicitly list the sibling module packages to scan@SpringBootApplication(scanBasePackages = {    "com.example.order",    "com.example.user"})public class OrderApplication {}
Notes

Trap: once you set scanBasePackages explicitly, it no longer scans the main class's own package automatically. If you list only the sibling module and forget your own, your own beans vanish en masse — a "fix one, break a batch" situation that is very typical in multi-module projects.

71 / 143
Section
9. Hands-on: how conditional wiring decides a bean's fate
72 / 143

The configuration classes imported by @EnableAutoConfiguration do not take effect unconditionally. The demo below visualizes condition evaluation: every auto-configuration class is interrogated condition by condition and kept only if it passes. Try turning off the classpath condition and watch the auto-configuration be skipped wholesale:

73 / 143
Kernel lab
74 / 143

Step back one question first: how many doors does an object have into the container? Section 4 was about "can the scan reach it"; this one is about "how many roads exist at all". The beanin scenario lays all four side by side — component scanning, @Bean methods, @Import, and whatever auto-configuration registers for you. Its final mode, "when it is not found", reproduces Section 1's required a bean that could not be found verbatim, and you can watch that it is never about an annotation failing — just a package that was never opened.

75 / 143
Kernel lab
TeaVMThe four roads into the container, and the one that silently does not existidle
Look at when each road reads your class, then park on the last mode: the same @Service, moved one package over, and none of the four roads leads to it
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
76 / 143

Enough buttons — type at it instead. This console talks to the same in-browser Java kernel and computes every reply live, which is exactly what this article's question needs: beans counts who got in, conditions asks why the rest did not, di shows who is waiting on whom.

77 / 143

Walk it in this order and step 4 is where a name goes missing:

78 / 143
  1. boot — build the container and read the wiring log
  2. beans — is your userService on that list?
  3. lab triple scan — keep only the scanning layer and see who is left
  4. lab beanin miss — reproduce the not-found scene directly
  5. conditions — which condition let or blocked each auto-configuration class
  6. di — the injection graph, so you can point at the broken edge
79 / 143
Console
80 / 143
Note

run lab triple config and lab triple all back to back — the first leaves almost no framework beans in the container, the second floods in dozens. Whatever sits between those two counts is precisely what the four modes of Section 11 peel apart.

81 / 143
Section
10. Decision: how should a multi-module project organize its packages
82 / 143
Decision
Decisionyou are starting a 6-module backend (user / order / pay / notify / common / gateway) and want to lock the package layout down early so modules never fail to find each other's beans. Which approach should you choose?
83 / 143

Aside: if a module wants to be **fully self-contained and auto-effective for any consumer** (like shared components in common), make it an auto-configuration by shipping `AutoConfiguration.imports`. That is exactly the next chapter.

84 / 143
Section
11. Hands-on one: peel the three-in-one apart, layer by layer
85 / 143

So far we described what it is made of. The demo below lets you switch each layer off and see what each one owns — the triple scenario simulates the assembly of @SpringBootApplication, and its four buttons are four ways to take it apart:

86 / 143
  • @SpringBootConfiguration (config): keep only "this is a configuration class". The @Bean methods on your main class still register, while @Service classes and auto-configuration do nothing — this layer only answers "may this card define workstations"
  • @ComponentScan (scan): add scanning. Your own UserService / HelloController now arrive, but there is no DispatcherServlet and none of the embedded-Tomcat beans — proof that "scanning yours" and "wiring the framework" are separate jobs
  • @EnableAutoConfiguration (auto): add auto-configuration. A flood of framework beans lands and beanDefinitionCount jumps from a dozen to dozens — the answer to last article's "why does it run with nothing configured"
  • All three together (all): the full @SpringBootApplication, plus a demonstration of how the four @AliasFor entries forward scanBasePackages / exclude inward
87 / 143
Kernel lab
TeaVMPeel @SpringBootApplication apart: config, scan, auto, allidle
Click the four buttons in order and note how beanDefinitionCount changes each time — every jump is one layer's contribution
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
88 / 143
Key point

the biggest gap is between config and all, and almost all of it comes from auto-configuration. Memorise those numbers — the next article picks them straight up.

89 / 143

Four modes clicked through, now stack them in your head — this is the door-card analogy from the opening, one box at a time. Each box says plainly who it let in and who it turned away:

90 / 143
Diagram
LayersThree gates stacked make @SpringBootApplication (click each)1 / 5
Press ① through ⑤ top-down. The line between ② and ③ is the boundary between your beans and the framework's
→
→
→
→
① @SpringBootConfiguration — this card may define workstations
The thinnest layer: @Configuration plus an @Indexed marker. It guarantees one thing only — @Bean methods on the main class are read as configuration. It scans nothing and imports nothing, so with only this layer your @Service never arrives.
All clearIn one line: ① grants the right, ② collects yours, ③ imports the framework's, ④ passes your package to ③, ⑤ finally builds objects.
91 / 143
Section
12. Hands-on two: which gate stopped you, and when
92 / 143

Knowing "three annotations" is not enough; you need to know at which moment each takes effect. Do these three demos in order and you cover the whole chain:

93 / 143

The first returns to the timeline from article #16. In bootrun you can see @SpringBootApplication being read during "prepare the context" and "post-process the context", while bean instantiation happens later inside refresh(). Annotations decide the roster, not the moment objects are built — a distinction many beginners blur:

94 / 143
Kernel lab
TeaVMWhere the three-in-one is read along the eight stepsidle
Watch the two steps that prepare and post-process the context — that is where annotation processing happens
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
95 / 143

The second interrogates condition annotations one by one. @ConditionalOnMissingBean — the umbrella rule: the shop lends one only because you did not bring your own — plus its evaluation timing and outcome are printed line by line in cond; switch to report to see the passed / failed grouping, formatted exactly like the --debug report in article #18:

96 / 143
Kernel lab
TeaVMCondition annotations evaluated one by one: whose bean got stoppedidle
Go through onclass, onbean, onprop, missing, then read the report grouping
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
97 / 143

The third resolves the "sibling modules cannot scan each other" problem from Section 8 using option three. The starter scenario shows a depended-on module shipping its own AutoConfiguration.imports: whoever depends on it gets the beans automatically, regardless of package names, with no reliance on scan ranges at all:

98 / 143
Kernel lab
TeaVMTurn the shared module into auto-configuration: no scanning neededidle
Start with meta (the registration list), verify props and bean, then switch it off with off
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
99 / 143
Section
13. Sandbox: the scope of the three jobs, one knob at a time
100 / 143

Start with this animation — it turns Section 4's prose into a chain you can recite: how a class annotated @Service becomes an object you can actually @Autowired. Nine out of ten "bean not found" errors happen between step 2 and step 3:

101 / 143
Animation
Animation · How one @Service gets into the container
Animation · How one @Service gets into the container
102 / 143

Pick a "surely fine" way to write it on the left; the right hands back the startup result and the exact line to change:

103 / 143
Sandbox
SandboxThe scope of the three-in-one: change the wording, see who gets blocked
Result
scanned: com.example.demo.** -> helloController, userService
auto-config imported: DispatcherServlet / Tomcat / Jackson …
GET /hello -> 200
#beanDefinitionCount roughly 60+
The default is the best value: main class in the root, every business package covered, framework wired by auto-configuration.
104 / 143
Note

replay the fourth cell — what people exclude is usually an auto-configuration they assumed was useless, and here it governs the response charset. The methodology this article keeps repeating: never switch auto-configuration off by feel; run --debug first and see what it owns.

105 / 143
Section
14. Common errors, quick reference
106 / 143

Every fragment below can be pasted verbatim into a search box:

107 / 143
Table
Error fragmentReal cause30-second fixDig deeper in
org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userController' for bean class [com.example.demo.v2.UserController] conflicts with existing, non-compatible bean definition of same name and class [com.example.demo.v1.UserController]Two same-named classes in different packages both got scanned, and the default bean name is the decapitalised class name, so they collideThe message already prints both fully-qualified names: keep one, name the other explicitly (@RestController("v2UserController")), or push one out with excludeFiltersThis article, Section 4
Description: Field xxx in com.example.demo.web.UserController required a bean of type 'com.example.demo.service.UserService' that could not be found.The main class is not in the root package, so "own package plus subpackages" never covers serviceCompare the class-name prefix in the error against the main class package; raising the main class is the cheapest fixThis article, Section 4 · Section 13 sandbox
404 Not Found while the log happily prints Started XxxApplicationThe controller sits outside the scan range, or you used @Controller without a view resolverWalk "URL → package path → annotation"; print ctx.getBeanDefinitionCount() to check whether the controller ever arrivedArticle #16 Section 9 · Article #22
Found multiple @SpringBootConfiguration (usually inside @SpringBootTest)Someone put a second @SpringBootConfiguration on a business config class, destroying the uniqueness of the primary configSearch the codebase for the annotation and keep only the main class; revert others to @ConfigurationThis article, Section 2
No qualifying bean of type '...' available: expected single matching bean but found 2Two candidates of the same type exist and injection cannot pick (often you defined one while auto-configuration supplied another)Name it with @Qualifier("beanName") or mark one @Primary; also check whether auto-configuration contributed the otherArticle #6 · Article #19
Beans of my own module vanished after adding scanBasePackagesDeclaring base packages explicitly stops the automatic scan of the main class's own package, and the list omits yourselfAdd your own module's package to the array; the durable fix is a shared ancestor packageThis article, Section 8
Compile-time error annotation alias value must match attribute name style failures from @AliasForWhen hand-writing a composed annotation, @AliasFor(annotation = X.class, attribute = "y") names an attribute X does not haveOpen the target annotation and check the attribute name exactlyThis article, Section 7
108 / 143
Trap

ConflictingBeanDefinitionException and "bean not found" are errors in opposite directions — the first means you scanned too much (both same-named classes entered range), the second too little. On seeing conflicts with existing, non-compatible bean definition of same name, read out the two fully-qualified names and ask yourself why both packages are in scope.

109 / 143

Row two of that table is this article's home game, and here it is as a real stack. Do not read the analysis — click the frame you think is guilty. The difficulty of this one is that no frame in the stack points at the cause; the answer lives in two fully-qualified names inside the message text.

110 / 143
Triage
Error triageNoSuchBeanDefinitionException: No qualifying bean of type 'com.example.demo.service.UserService' available
The @Service is written, yet no frame in the stack mentions scanning

The main class lives in com.example.demo.web, UserService lives in com.example.demo.service and carries @Service. It compiles; startup aborts immediately.

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userController' defined in file [D:\demo\target\classes\com\example\demo\web\UserController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.demo.service.UserService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:801)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1375)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1212)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201)
at com.example.demo.web.DemoApplication.main(DemoApplication.java:23)
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
111 / 143
Section
15. Quick self-tests
112 / 143
Quiz
Check yourselfYou wrote `@SpringBootApplication(scanBasePackages = "com.example.order")` on a main class that itself lives in `com.example.order`, while shared beans live in `com.example.common`. None of the shared beans can be injected. What is the most accurate cause?
Pick one — you get feedback right away
113 / 143
Quiz
Check yourselfTo stop one auto-configuration from taking effect, a colleague moved the main class into `com.example.demo.web` and says "it starts fine now". What is the biggest danger?
Pick one — you get feedback right away
114 / 143
Section
16. Hands-on exercises
115 / 143
Section
Tier one · Follow along
116 / 143

Goal: build a minimal project with "main class in the root plus layered subpackages", then deliberately break and repair "bean not found" three times.

117 / 143

pom.xml (one starter is enough):

118 / 143
xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>3.2.5</version>        <relativePath/>    </parent>    <groupId>com.example</groupId>    <artifactId>scanlab</artifactId>    <version>0.0.1-SNAPSHOT</version>    <properties>        <java.version>17</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>
119 / 143

Three files, watch the package levels (src/main/java/com/example/scanlab/):

120 / 143
java
// 1) ScanlabApplication.java —— package com.example.scanlab (the root)package com.example.scanlab;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;@SpringBootApplicationpublic class ScanlabApplication {    public static void main(String[] args) {        ConfigurableApplicationContext ctx =                SpringApplication.run(ScanlabApplication.class, args);        System.out.println("[check] beanDefinitionCount = " + ctx.getBeanDefinitionCount());        System.out.println("[check] greeter = " + ctx.getBean("greeter"));    }}
121 / 143
java
// 2) service/Greeter.java —— package com.example.scanlab.service (subpackage)package com.example.scanlab.service;import org.springframework.stereotype.Service;@Servicepublic class Greeter {    public String greet() {        return "hello from scanned package";    }}
122 / 143
java
// 3) controller/GreetController.java —— package com.example.scanlab.controllerpackage com.example.scanlab.controller;import com.example.scanlab.service.Greeter;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class GreetController {    private final Greeter greeter;    public GreetController(Greeter greeter) {   // constructor injection        this.greeter = greeter;    }    @GetMapping("/greet")    public String greet() {        return greeter.greet();    }}
123 / 143

Run mvn spring-boot:run. Expected startup log (the three key lines):

124 / 143
text
 :: Spring Boot ::                (v3.2.5)... o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''... com.example.scanlab.ScanlabApplication  : Started ScanlabApplication in 1.4 seconds (process running for 1.7)[check] beanDefinitionCount = 63[check] greeter = com.example.scanlab.service.Greeter@5c8eee2a
125 / 143

curl http://localhost:8080/greet returns hello from scanned package.

126 / 143

Now break it three times on purpose, watching one symptom each time:

127 / 143
  1. Move ScanlabApplication to com.example.scanlab.web → startup fails immediately, Description: naming Greeter
  2. Put it back in the root but add scanBasePackages = "com.example.scanlab.controller" → still failing, again missing Greeter (controller in, service out)
  3. Keep step 2 and extend the list to {"com.example.scanlab.controller", "com.example.scanlab.service"} → works, but you are now hand-maintaining a scan list — feel why Section 8 calls that debt
128 / 143

Checklist: ① you can say which bean was missing in attempts one and two and why; ② you saved the exact error text of all three; ③ the final state is "main class in the root, no properties written".

129 / 143
Section
Tier two · Variants
130 / 143

Each variant changes one thing; write down what you observed:

131 / 143
  1. Name collision: add com.example.scanlab.v1.UserController and com.example.scanlab.v2.UserController, neither named. You will observe ConflictingBeanDefinitionException printing both fully-qualified names. Then name one @RestController("v2UserController") and you will observe startup recovering.
  2. excludeFilters: undo the explicit name and instead use @SpringBootApplication(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = V1UserController.class)). You will observe v1 pushed out without touching any class; try FilterType.REGEX on a whole package next.
  3. exclude an auto-configuration: add exclude = HttpEncodingAutoConfiguration.class and request /greet. You will observe it appear under Exclusions (use --debug) and notice the charset change on non-ASCII responses — the live version of the sandbox's fourth cell.
  4. Uniqueness: change one ordinary @Configuration class to also carry @SpringBootConfiguration, then run an empty @SpringBootTest. You will observe Found multiple @SpringBootConfiguration — the error points at the test while the cause sits in business code.
132 / 143
Section
Tier three · Build one
133 / 143

Build a two-module project multi-scan: a parent pom aggregating common-module and app-module, with the main class in app-module. Implement all three ways of letting app use common's beans, and document the comparison.

134 / 143
  • Option A: shared ancestor package (both under com.example.xxx, main class at com.example)
  • Option B: scanBasePackages listing both module packages explicitly
  • Option C: common-module ships META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports registering its own configuration (formally introduced next article), with no scan configuration on the app side
135 / 143

Checklist: ① all three start and serve the same endpoint; ② your README states, per option, "what must change when a new module appears"; ③ deliberately omit one package in option B, paste the error, and explain why B is riskier than A/C; ④ in option C, find your own auto-configuration's positive-match entry with --debug.

136 / 143
Section
17. Key-point self-check
137 / 143
Self-check

from memory, state the one-sentence job of each of the three annotations inside @SpringBootApplication, and the typical error each produces when it misfires.

138 / 143
Self-check

does scanBasePackages replace or append to the default base package? Why does one rule explain both "bean not found" and "my whole module vanished"?

139 / 143
Self-check

what separates @SpringBootConfiguration from @Configuration, and why may only one of the former exist per application?

140 / 143
Self-check

when do you reach for exclude, excludeName, or spring.autoconfigure.exclude? How does compile-time visibility drive the choice?

141 / 143
Self-check

why is "move the main class to another package" never an acceptable way to disable auto-configuration? What is the correct action?

142 / 143

Mantra: **main class in the root, a list replaces rather than appends, one primary config only, and exclude silences auto-configuration.**

143 / 143
Summary

@SpringBootApplication is three annotations stacked — @SpringBootConfiguration declares a configuration class (globally unique), @EnableAutoConfiguration imports auto-configuration, and @ComponentScan scans business beans, with four @AliasFor entries forwarding attributes down. Only two facts deserve muscle memory: the scan range is always "main class package plus subpackages", so the main class must live in the outermost root package; and for modules to see each other, give them a shared root. Get these two right and 90% of "bean not found" errors disappear on the spot.