本节目标:读懂上一节生成的
demo项目——每个目录、每一行启动类代码分别负责什么,为什么启动类必须放在根包,配置文件放哪里、谁覆盖谁;读完后你能自己判断一个 Spring Boot 项目「结构对不对」。
适用版本:Spring Boot 4.1.x(Java 21)
2.3 目录结构与启动类
上一节 2.2 用 start.spring.io 生成项目
生成了 demo 项目。它现在能构建、能启动,但里面每个文件为什么在那个位置,还没解释。本节把项目拆开:先看目录树,再逐行读启动类,然后重点讲清包扫描规则——这是初学者最常踩、报错信息又最不直白的坑。最后讲配置文件的位置与优先级,以及两种运行方式的差别。
标准 Maven 目录树
demo/ 展开后是这样(省略了 mvnw 等包装脚本的细节):
demo/
├── pom.xml
├── .gitignore
├── mvnw
├── mvnw.cmd
├── .mvn/
│ └── wrapper/
│ └── maven-wrapper.properties
└── src/
├── main/
│ ├── java/
│ │ └── com/example/demo/
│ │ └── DemoApplication.java
│ └── resources/
│ ├── application.properties
│ ├── static/
│ └── templates/
└── test/
└── java/
└── com/example/demo/
└── DemoApplicationTests.java
这套布局来自 Maven 的约定优于配置:src/main/java 放主代码,src/main/resources 放资源,src/test/java 放测试。你不需要在 pom.xml 里声明这些路径,Maven 默认就知道。逐个说明:
| 路径 | 放什么 | 会不会打进产物 |
|---|---|---|
src/main/java | 主程序源码(.java) | 编译成 .class 后打进去 |
src/main/resources | 配置、静态资源、模板 | 原样复制进 jar |
src/main/resources/static | 静态文件(CSS/JS/图片) | 是,直接映射为 Web 路径 |
src/main/resources/templates | 服务端模板(如 Thymeleaf) | 是,由模板引擎读取 |
src/test/java | 测试代码(.java) | 否,只在测试期存在 |
target/ | 构建产物(编译输出、jar) | 否,是生成目录,应忽略 |
mvnw / mvnw.cmd 是 Maven Wrapper:它们让你不用预装 Maven 也能构建,首次运行会自动下载 .mvn/wrapper/maven-wrapper.properties 里指定版本的 Maven。团队协作时把 wrapper 提交进仓库,能保证所有人用同一个 Maven 版本。static/ 与 templates/ 此刻是空目录(空目录不会被提交),第 11 章讲静态资源与模板时才会往里放东西。
启动类逐行解释
打开 src/main/java/com/example/demo/DemoApplication.java:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
短短十行,信息量不小。
package com.example.demo; 这行不只是给类分目录,它还决定了组件扫描的根——稍后重点讲。
@SpringBootApplication 是一个复合注解,等价于同时标注下面三个:
| 组合注解 | 作用 |
|---|---|
@SpringBootConfiguration | 声明这是一个配置类(本质是 @Configuration 的特化) |
@EnableAutoConfiguration | 开启自动配置:根据 classpath 上的依赖自动装配 Bean |
@ComponentScan | 从本类所在包开始,向下扫描 @Component、@Service、@Controller 等 |
第 4 章会把这层「复合」拆开讲透,这里先记住:三个能力被一个注解打包了。
public static void main(String[] args) 是标准的 Java 入口,说明 Spring Boot 应用就是一个普通 Java 程序,能直接 java -jar 跑,不需要外部容器。
SpringApplication.run(DemoApplication.class, args) 一行干了很多事:创建 Spring 应用上下文(ApplicationContext)、读取 DemoApplication 上的配置与扫描结果、执行自动配置、启动内嵌的 Tomcat、最后返回一个 ConfigurableApplicationContext。它把 DemoApplication.class 作为主配置源传入,因此这个类所在的位置同时决定了扫描起点。args 会把命令行参数透传进去,成为最高优先级的配置来源之一(第 6 章讲)。
包扫描规则:为什么启动类要放根包
这是本节最重要的一条规则:
@SpringBootApplication默认只扫描「启动类所在包及其所有子包」。
所以约定是:把启动类放在根包(如 com.example.demo),让所有业务组件都在它的子包里。这样扫描范围最大,com.example.demo.web、com.example.demo.service、com.example.demo.repository 全都能被扫到。
看一个放错位置的反例。假设你把启动类挪进了子包 config:
package com.example.demo.config; // 启动类在子包里
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
而控制器放在它的兄弟包 com.example.demo.web 里:
package com.example.demo.web; // 不在 config 的子包下
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
现在扫描根是 com.example.demo.config,com.example.demo.web 不在这个子树里。后果分两种,都很难一眼看懂:
- 如果只是接口扫描不到:应用照常启动、日志一片正常,但访问
/hello得到 404,因为HelloController根本没被注册成 Bean。 - 如果某个
@Controller通过构造器依赖了一个未被扫描的@Service:启动直接失败,报一个「找不到 Bean」的长错误:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.demo.web.HelloController
required a bean of type 'com.example.demo.service.GreetingService' that could not be found.
两种症状的根因是同一个:扫描没覆盖到。修复方式有两种:
- 推荐:把启动类移回根包
com.example.demo,保持「启动类在最外层」的约定; - 例外:确有需要时显式指定扫描范围:
@SpringBootApplication(scanBasePackages = {"com.example.demo", "com.example.shared"})
public class DemoApplication {
// ...
}
第一种几乎总是更好的选择——让结构自解释,比在注解里维护一串包名可靠得多。下面这张表把「启动类位置」和「扫描结果」的关系列清楚:
| 启动类位置 | 扫描范围 | 结果 |
|---|---|---|
com.example.demo(根包) | 该包及全部子包 | 正确,推荐 |
com.example.demo.config | config 及其子包 | 兄弟包漏扫,404 / 找不到 Bean |
com.example(过高层) | 整个 com.example | 能跑,但会扫到无关的第三方包,启动变慢 |
配置文件的位置与优先级
src/main/resources/application.properties 是 Spring Boot 的默认配置文件,生成时是空的。你可以写 properties 格式:
spring.application.name=demo
server.port=8080
也可以删掉它、改用等价的 application.yml:
spring:
application:
name: demo
server:
port: 8080
两种格式能力相同。如果同一个位置同时存在 application.properties 和 application.yml,properties 的优先级更高——这算一个历史包袱,实践中二选一即可,别混着放。
Spring Boot 会从多个位置找配置文件,后出现的覆盖先出现的:
| 顺序 | 位置 | 说明 |
|---|---|---|
| 1 | classpath:/(即 resources/) | 默认位置,打进 jar |
| 2 | classpath:/config/ | jar 内 config/ 子目录 |
| 3 | file:./(运行目录) | jar 外的同目录文件 |
| 4 | file:./config/ | jar 外 config/ 子目录 |
| 5 | file:./config/*/ | jar 外 config/ 的任意子目录 |
越靠后的优先级越高,设计意图很明确:打包时写默认值(第 1 条),部署时用外置文件覆盖(第 3–5 条),不用重新打包。这就是「外置化配置」的基础,第 6 章会给出完整的多来源优先级与 Profile 用法。
现在验证一下配置生效:把 server.port 改成 9090 再启动,日志里会看到 Tomcat started on port 9090,而不再是默认的 8080。
.gitignore 建议
初始化器生成的 .gitignore 已经覆盖了常见情况,核心是三类不该提交的内容:
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
### IntelliJ IDEA ###
.idea/
*.iws
*.iml
*.ipr
### Eclipse ###
.classpath
.factorypath
.project
.settings/
.springBeans
.sts4-cache
### VS Code ###
.vscode/
### macOS ###
.DS_Store
target/:构建产物,每次构建都能重新生成,提交它只会让 diff 噪音爆炸;.idea/、.vscode/、.settings/:个人 IDE 配置,不同人机器上不同,不该强加给团队;HELP.md:初始化器生成的说明文件,无实际价值。
注意 .mvn/wrapper/maven-wrapper.jar 前面那个 !,它表示强制包含——wrapper 的 jar 是团队共享构建工具版本的关键,属于「通常该忽略的目录里唯一要留下的东西」。
mvn spring-boot:run 与 IDE 直接运行
两种方式都能启动项目,但走的是不同的路径:
| 维度 | mvn spring-boot:run | IDE 直接运行 main |
|---|---|---|
| 编译 | 走 Maven 生命周期 | IDE 自己的编译器 |
| 资源处理 | process-resources 阶段复制到 target/classes | 依赖 IDE 的输出目录配置 |
| 依赖解析 | 完全按 pom.xml | 用 IDE 导入的模块依赖 |
| 传参 | -Dspring-boot.run.arguments=--server.port=9090 | Run Configuration 的 Program arguments |
| 典型场景 | CI、命令行、验证打包一致性 | 日常调试、断点 |
命令行方式:
mvn spring-boot:run
它的优势是与最终打包使用的类路径完全一致——如果这里能跑,mvn package 出的 jar 基本也能跑。缺点是每次都要走完整生命周期,慢一些。
IDE 直接运行更快,但有个经典坑:你改了 application.properties 后 IDE 没重新复制资源到输出目录,程序读到的还是旧配置,于是你盯着「为什么改了没生效」发呆。遇到这种情况,Build → Rebuild Project 一下,或者干脆用 mvn spring-boot:run 交叉验证。另一个相关变化:Spring Boot 4.x 里 DevTools 的 Live Reload 默认是关闭的,想用热重载得显式开启,别以为「没自动重启就是配置错了」。
两种方式启动后,你都会看到同一个 Spring Boot banner 与 Tomcat 启动日志(4.1.1 实测约 1 秒内完成启动,banner 里会打印 (v4.1.1))。这段日志的逐行含义,以及第一个 REST 接口怎么加,是下一章 3.1 第一个 REST 接口
的内容。
小结
- Maven 目录遵循约定:
src/main/java放代码,src/main/resources放配置与静态资源,src/test/java放测试,target/是构建产物。 @SpringBootApplication是@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan的复合注解。SpringApplication.run(DemoApplication.class, args)创建上下文、执行自动配置、启动内嵌服务器,并以启动类所在包为扫描根。- 包扫描默认只覆盖启动类所在包及其子包,所以启动类必须放在根包;放错位置会表现为接口 404 或「找不到 Bean」。
- 配置文件默认在
classpath:/application.properties(或.yml),外置位置优先级更高,便于部署时覆盖而不重新打包。 .gitignore应忽略target/与各类 IDE 配置,但要保留 Maven Wrapper 的 jar。mvn spring-boot:run与 IDE 直接运行结果一致但路径不同,前者与打包一致性更高。
到这里,项目「能启动、能改配置」这条主线已经打通。下一节我们往项目里加第一个 REST 接口,看它如何被扫描、注册、路由,并观察 4.x 的真实启动日志。
阅读导航:上一节:2.2 用 start.spring.io 生成项目 · 下一节:3.1 第一个 REST 接口 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。