@SpringBootApplication: One Annotation, Three Meanings
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.
@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.

After this article you should be able to answer three questions:
- 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
excludeversusexcludeNameversusspring.autoconfigure.exclude?
You create a Spring Boot project, write a UserService annotated with @Service, inject it into a UserController, and startup fails outright:
Description:Field userService in com.example.demo.web.UserController required a bean of type'com.example.demo.service.UserService' that could not be found.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.
The best way to understand this is to pry apart that "three-in-one" annotation. In Spring Boot 3 it looks like this:
@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 {};}@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 / @Controllerinto the container- The four
@AliasForentries: forward this single annotation's attributes to the three inner annotations, so@SpringBootApplication(exclude = ...)is exactly the same as writingexcludeon the inner@EnableAutoConfiguration

@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.
Strip the outer layer and @SpringBootConfiguration is surprisingly short:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Configuration@Indexedpublic @interface SpringBootConfiguration { @AliasFor(annotation = Configuration.class) boolean proxyBeanMethods() default true;}- At its core it is
@Configuration. Your main class is therefore a configuration class by nature — you can write@Beanmethods on it directly @Indexedfeeds the "component index": the compiler writes aMETA-INF/spring.componentslist 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.@SpringBootTestrelies 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.
The third sibling is the auto-configuration switch, and its definition is just as short:
@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 {};}@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 readsMETA-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 namespring.boot.enableautoconfiguration=false: the global kill switch, used only in tests or when troubleshootingexclude/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.
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.
<?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>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:
The scan range is the package the main class lives in, plus all of its subpackages.
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:
| Main class location | Scans com.example.demo.service? | Why |
|---|---|---|
com.example.demo.DemoApplication | Yes | service is a subpackage of demo |
com.example.DemoApplication | Yes | service is still under demo |
com.example.demo.web.WebApplication | No | service is a sibling of web |
com.example.demo.web.admin.AdminApp | No | the parent demo package is never scanned upward |
Placing the main class in the wrong spot is an extremely common and painfully hard-to-find mistake:
// 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 injectionThere are two fixes, and the first always wins:
// ✅ 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 {}@ComponentScan also supports excludeFilters, used to pick out classes that are scanned but should not be registered:
@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 {}
That animation shows the scope; the figure below shows the price — the same project with the main class moved down exactly one package:

"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.
package com.example.demo.web; // you put the main class inside the web subpackage@SpringBootApplication // the @ComponentScan inside it declares no basePackagespublic class DemoApplication { /* main */ }// the container reads the annotation: default base package = the main class's package = com.example.demo.web// recurse over com.example.demo.web.** -> UserController matches, its BeanDefinition is registered// UserController's constructor needs UserService -- which lives in com.example.demo.service, outside that subtree| main class package | com.example.demo.web |
| UserService package | com.example.demo.service |
| relation between them | siblings; neither contains the other |
compile time: everything is fineAttention: `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".
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:
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- 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
@Configurationclasses live underconfigand are collected automatically, so no manual@Importis 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.
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:
| Approach | Syntax | Scope | Best for |
|---|---|---|---|
exclude / excludeName attribute | @SpringBootApplication(exclude = XxxAutoConfiguration.class) | this main class only | excluding one or two here and there |
spring.autoconfigure.exclude | fully-qualified class names in configuration, comma-separated | global | no code change / per-environment differences / tests |
| custom composite annotation | your own annotation wrapping @SpringBootApplication together with the exclusion list | team convention | many services sharing one exclusion policy |
The first, written right on the main class, is compile-time checked:
@SpringBootApplication( exclude = DataSourceAutoConfiguration.class, // type-based, refactorable, IDE-navigable excludeName = "org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration")public class DemoApplication {}The third packages that line into a single team-wide entry annotation:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class })@ComponentScanpublic @interface ServiceApplication {}// 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); }}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:
spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationThe 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.
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: trueNow back to the question from Section 1: why does @SpringBootApplication(scanBasePackages = "x") work at all, when scanBasePackages clearly belongs to @ComponentScan?
The answer is the @AliasFor meta-annotation. It does two things: declare aliases and forward across annotations. Here is how @SpringBootApplication uses it:
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")String[] scanBasePackages() default {};annotation = ComponentScan.class: tells Spring this attribute actually belongs to@ComponentScanattribute = "basePackages": specifies which attribute of the target it forwards to- So when you set
scanBasePackageson the outside, Spring readsbasePackageson the inside — two names, one value
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.
@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.
That forwarding takes six frames from the name you type to a scan range that really moved:

@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.
Placing the main class correctly is enough inside one module, but multi-module setups trip people up. A typical scene:
project├── user-module (com.example.user) ← contains UserService└── order-module (com.example.order) ← depends on user-module, main class lives hereThe 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.
Two fixes, pick by team convention:
| Option | How | Upside | Cost |
|---|---|---|---|
| Shared root package | all modules use com.example.xxx; main class raised to com.example | zero extra annotations, most natural | package names must be planned up front |
| Explicit scanning | @SpringBootApplication(scanBasePackages = {"com.example.order", "com.example.user"}) | no renames, fast to apply | update it for every new module, easy to miss |
| Make it an auto-configuration | dependent module ships AutoConfiguration.imports to expose its beans | truly self-contained and reusable | requires understanding the next chapter |
// Option two: explicitly list the sibling module packages to scan@SpringBootApplication(scanBasePackages = { "com.example.order", "com.example.user"})public class OrderApplication {}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.
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:
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.
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.
Walk it in this order and step 4 is where a name goes missing:
boot— build the container and read the wiring logbeans— is youruserServiceon that list?lab triple scan— keep only the scanning layer and see who is leftlab beanin miss— reproduce the not-found scene directlyconditions— which condition let or blocked each auto-configuration classdi— the injection graph, so you can point at the broken edge
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.
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.
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:
@SpringBootConfiguration(config): keep only "this is a configuration class". The@Beanmethods on your main class still register, while@Serviceclasses and auto-configuration do nothing — this layer only answers "may this card define workstations"@ComponentScan(scan): add scanning. Your ownUserService/HelloControllernow arrive, but there is noDispatcherServletand 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 andbeanDefinitionCountjumps 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@AliasForentries forwardscanBasePackages/excludeinward
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.
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:
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:
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:
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:
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:
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:

Pick a "surely fine" way to write it on the left; the right hands back the startup result and the exact line to change:
scanned: com.example.demo.** -> helloController, userServiceauto-config imported: DispatcherServlet / Tomcat / Jackson …GET /hello -> 200#beanDefinitionCount roughly 60+
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.
Every fragment below can be pasted verbatim into a search box:
| Error fragment | Real cause | 30-second fix | Dig 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 collide | The message already prints both fully-qualified names: keep one, name the other explicitly (@RestController("v2UserController")), or push one out with excludeFilters | This 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 service | Compare the class-name prefix in the error against the main class package; raising the main class is the cheapest fix | This article, Section 4 · Section 13 sandbox |
404 Not Found while the log happily prints Started XxxApplication | The controller sits outside the scan range, or you used @Controller without a view resolver | Walk "URL → package path → annotation"; print ctx.getBeanDefinitionCount() to check whether the controller ever arrived | Article #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 config | Search the codebase for the annotation and keep only the main class; revert others to @Configuration | This article, Section 2 |
No qualifying bean of type '...' available: expected single matching bean but found 2 | Two 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 other | Article #6 · Article #19 |
Beans of my own module vanished after adding scanBasePackages | Declaring base packages explicitly stops the automatic scan of the main class's own package, and the list omits yourself | Add your own module's package to the array; the durable fix is a shared ancestor package | This article, Section 8 |
Compile-time error annotation alias value must match attribute name style failures from @AliasFor | When hand-writing a composed annotation, @AliasFor(annotation = X.class, attribute = "y") names an attribute X does not have | Open the target annotation and check the attribute name exactly | This article, Section 7 |
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.
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.
The main class lives in com.example.demo.web, UserService lives in com.example.demo.service and carries @Service. It compiles; startup aborts immediately.
Goal: build a minimal project with "main class in the root plus layered subpackages", then deliberately break and repair "bean not found" three times.
pom.xml (one starter is enough):
<?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>Three files, watch the package levels (src/main/java/com/example/scanlab/):
// 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")); }}// 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"; }}// 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(); }}Run mvn spring-boot:run. Expected startup log (the three key lines):
:: 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@5c8eee2acurl http://localhost:8080/greet returns hello from scanned package.
Now break it three times on purpose, watching one symptom each time:
- Move
ScanlabApplicationtocom.example.scanlab.web→ startup fails immediately,Description:namingGreeter - Put it back in the root but add
scanBasePackages = "com.example.scanlab.controller"→ still failing, again missingGreeter(controller in, service out) - 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
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".
Each variant changes one thing; write down what you observed:
- Name collision: add
com.example.scanlab.v1.UserControllerandcom.example.scanlab.v2.UserController, neither named. You will observeConflictingBeanDefinitionExceptionprinting both fully-qualified names. Then name one@RestController("v2UserController")and you will observe startup recovering. - 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; tryFilterType.REGEXon a whole package next. - exclude an auto-configuration: add
exclude = HttpEncodingAutoConfiguration.classand 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. - Uniqueness: change one ordinary
@Configurationclass to also carry@SpringBootConfiguration, then run an empty@SpringBootTest. You will observeFound multiple @SpringBootConfiguration— the error points at the test while the cause sits in business code.
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.
- Option A: shared ancestor package (both under
com.example.xxx, main class atcom.example) - Option B:
scanBasePackageslisting both module packages explicitly - Option C:
common-moduleshipsMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsregistering its own configuration (formally introduced next article), with no scan configuration on the app side
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.
from memory, state the one-sentence job of each of the three annotations inside @SpringBootApplication, and the typical error each produces when it misfires.
does scanBasePackages replace or append to the default base package? Why does one rule explain both "bean not found" and "my whole module vanished"?
what separates @SpringBootConfiguration from @Configuration, and why may only one of the former exist per application?
when do you reach for exclude, excludeName, or spring.autoconfigure.exclude? How does compile-time visibility drive the choice?
why is "move the main class to another package" never an acceptable way to disable auto-configuration? What is the correct action?
Mantra: **main class in the root, a list replaces rather than appends, one primary config only, and exclude silences auto-configuration.**
@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.