《Spring Boot 入门》2.3 目录结构与启动类

本节读懂 demo 项目:给出标准 Maven 目录树并解释各目录职责,逐行拆解启动类与 @SpringBootApplication、SpringApplication.run;重点讲包扫描规则,说明启动类为何必须放根包;再讲 application.properties 与 yml 的位置与优先级、.gitignore 建议,以及两种运行方式的差别。

本节目标:读懂上一节生成的 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.

两种症状的根因是同一个:扫描没覆盖到。修复方式有两种:

  1. 推荐:把启动类移回根包 com.example.demo,保持「启动类在最外层」的约定;
  2. 例外:确有需要时显式指定扫描范围:
@SpringBootApplication(scanBasePackages = {"com.example.demo", "com.example.shared"})
public class DemoApplication {
    // ...
}

第一种几乎总是更好的选择——让结构自解释,比在注解里维护一串包名可靠得多。下面这张表把「启动类位置」和「扫描结果」的关系列清楚:

启动类位置扫描范围结果
com.example.demo(根包)该包及全部子包正确,推荐
com.example.demo.configconfig 及其子包兄弟包漏扫,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 会从多个位置找配置文件,后出现的覆盖先出现的:

顺序位置说明
1classpath:/(即 resources/)默认位置,打进 jar
2classpath:/config/jar 内 config/ 子目录
3file:./(运行目录)jar 外的同目录文件
4file:./config/jar 外 config/ 子目录
5file:./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:runIDE 直接运行 main
编译走 Maven 生命周期IDE 自己的编译器
资源处理process-resources 阶段复制到 target/classes依赖 IDE 的输出目录配置
依赖解析完全按 pom.xml用 IDE 导入的模块依赖
传参-Dspring-boot.run.arguments=--server.port=9090Run 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 接口 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计