项目结构:约定优于配置
本教程共 48 篇 · 第 4 篇 · 更新于 2026-08-13 · 约 5 分钟阅读
本节目标:掌握 Spring Boot 的标准包结构,搞懂
@SpringBootApplication的组成与扫描机制,学完你会搭出规范的项目骨架。
约定优于配置
Spring Boot 的设计哲学,一句话:约定优于配置(Convention over Configuration)。
框架先把常规情况下的最佳实践定成默认值。你按约定放文件、命名、分包,框架就少让你配置;你要搞特殊,再显式覆盖。
项目结构是第一课。目录怎么建、主类放哪里,都有约定。
两条铁律
第一,不要用默认包。 Java 允许类不写 package 声明,但 Spring Boot 应用这么干会出大问题。自动配置和组件扫描都要扫包,默认包里所有 jar 的类都会被读一遍,扫描范围失控,各种奇怪报错。
第二,包名用反域名。 比如公司域名是 example.com,包名就是 com.example。这是 Java 世界的通用惯例,保证包名全球不重名。包名全小写,多级包用点连接,比如 com.example.myapplication.customer;类名用大驼峰。命名惯例从第一天就养成。
主类放在根包
主类(带 main 方法和 @SpringBootApplication 的类)应该放在包结构的最顶层,也就是根包。
为什么?@SpringBootApplication 默认扫描主类所在包及其所有子包。主类放根包,扫描范围正好覆盖整个项目,不多不少。
官方推荐的布局长这样:
com
└── example
└── myapplication
├── MyApplication.java # 主类,放根包
├── customer
│ ├── Customer.java # 实体:描述数据
│ ├── CustomerController.java # 控制器:收请求
│ ├── CustomerService.java # 服务:写业务
│ └── CustomerRepository.java # 仓库:管数据
└── order
├── Order.java
├── OrderController.java
├── OrderService.java
└── OrderRepository.java
主类本身很简单:
package com.example.myapplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@SpringBootApplication 到底是什么
它是个组合注解,一个顶三个:
@SpringBootConfiguration:声明这是一个配置类@EnableAutoConfiguration:开启自动配置,按 classpath 依赖猜测你要什么@ComponentScan:扫描主类所在包及子包,把带@Component等注解的类注册成 Bean
拆开写,等价于:
package com.example.myapplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
Note拆开写和
@SpringBootApplication效果相同,但没人这么写。一个注解更省事,理解它由三部分组成,是为了搞懂背后的机制。
组件扫描:Spring 怎么找到你的类
@ComponentScan 是这套机制的核心。它会扫主类所在包及子包,把符合条件的类注册成 Bean,放进 Spring 容器统一管理。
什么样的类会被扫到?带这些「组件注解」的类:
@Component:通用组件@Service:业务服务层@Repository:数据访问层@Controller/@RestController:控制层
Bean 是 Spring 对受管对象的称呼,先记住这个词,第 7 章细讲。
Tip这几个注解的功能本质相同,区别是语义标签。Spring 和开发者都能一眼看出类的角色。这就是为什么你随手写的控制器,Spring 会自动接管,不用手动 new。
分层思想:包按业务切
Spring Boot 对包结构没有强制要求,但社区沉淀了两套组织方式。
按技术分层:controller、service、repository、entity 各建一个包。小项目很清晰,项目一大就乱——一个业务功能要跨四个包翻找。
按业务分包(package-by-feature):每个业务模块一个包,包内自含控制器、服务、数据访问。官方推荐的正是这种,比如上面的 customer 包和 order 包,模块之间高内聚、低耦合。
| 组织方式 | 优点 | 缺点 |
|---|---|---|
| 按技术分层 | 结构直观,新人好上手 | 项目大了跨包翻找,功能分散 |
| 按业务分包 | 模块内聚,功能集中 | 需要先理解业务边界 |
不管哪种方式,三层职责是固定的:
- Controller:接收 HTTP 请求,校验参数,返回响应
- Service:写业务逻辑,组织多个数据操作
- Repository:访问数据库,增删改查
Tip新手学分层,先记职责:Controller 收请求、Service 写业务、Repository 管数据。按业务分包后,这些类自然落在各自的模块包里,三层关系一目了然。
一个最小分层的例子
把上一章的 Hello World 拆开,感受一下分层。控制器单独一个类:
package com.example.myapplication.customer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CustomerController {
private final CustomerService customerService;
public CustomerController(CustomerService customerService) {
this.customerService = customerService;
}
@GetMapping("/customers")
public String list() {
return customerService.buildGreeting();
}
}
服务类负责业务:
package com.example.myapplication.customer;
import org.springframework.stereotype.Service;
@Service
public class CustomerService {
public String buildGreeting() {
return "Hello from Customer Service!";
}
}
注意两个细节:CustomerService 用 @Service 标注,会被组件扫描自动发现;控制器通过构造器拿到服务,没有手动 new。这就是依赖注入的雏形,第 7 章会系统讲。
为什么不手动 new
新手常问:服务类直接 new 一个不就完了,为什么要让 Spring 管?
手动 new 的问题在「依赖关系」:CustomerController 依赖 CustomerService,CustomerService 又依赖别的东西。项目一大,对象之间的依赖链全靠手写组装,改一处崩一片。
Spring 的做法是反转:你声明依赖,容器负责创建和注入。构造器写上参数,Spring 自动把现成的 Bean 送进来。代码只关心「我要什么」,不关心「去哪找」。这就是控制反转(IoC),第 7 章展开讲。
主类放错的后果
主类放进子包,组件扫描范围就缩到子包。根包里的其他组件统统扫不到:接口 404、Bean 找不到,排查半天。
Warning新手最常见的坑:把主类挪进 controller 之类的子包。
@SpringBootApplication一挪,扫描范围就变了。主类必须留在根包。
实在要挪,可以显式指定扫描包:
@SpringBootApplication(scanBasePackages = "com.example.myapplication")
但这是补救措施,不是常规做法。默认结构最省心。
实体类放哪
实体类(Customer)放在业务包内,和它的控制器、服务放一起。第 26 章起讲数据访问时,实体类会频繁出现,位置约定和现在一样:跟着业务模块走。
小结
骨架是约定,更是习惯。按规范摆文件,团队协作才不吵架。
- 不用默认包,包名用反域名
- 主类放根包,
@SpringBootApplication一个注解管扫描和自动配置 - 包按业务切分,模块内自含控制层、服务层、数据层
- 主类乱放是高频翻车点,守住根包
骨架搭对了,后面写代码才能顺。下一章看 Spring Boot 有哪几种启动方式。