前面章节已经覆盖了 Spring Boot 的核心技术点,从容器、数据访问到微服务治理。但是,知道怎么用和知道怎么把一堆代码组织成可维护的系统,完全不是一回事。在团队协作和长期迭代中,清晰的架构和统一的规范远比某个具体技巧重要得多。
本章将以一个真实的中型 Spring Boot 项目为蓝本,梳理出经过验证的架构模式与编码规范,帮助你在团队中建立共同的“代码语言”,减少沟通成本,降低腐化风险。
18.1 项目模块划分与分层架构
18.1.1 单体应用的两种分层模式
对于多数 Spring Boot 应用,推荐使用 分层架构,业务复杂度优先于技术炫技。常见的有两种:
- 传统三层架构(Controller – Service – Repository)
这是最朴素、最被广泛接受的模式。所有业务逻辑集中在 Service 层,Controller 仅负责参数接收和响应封装,Repository(或 DAO)负责数据存取。
- 领域驱动设计(DDD)风格的四层架构
在业务足够复杂时,可以引入 DDD 的“接口层 – 应用层 – 领域层 – 基础设施层”。但不要为了 DDD 而 DDD,大多数项目用整洁的三层结构再辅以模块拆分已经足够。
这里以三层 + 公共模块为例,描述典型的分层结构。
18.1.2 多模块项目结构
使用 Maven 或 Gradle 的多模块工程,可以把业务边界和依赖方向清晰地固定下来。一个电商系统的简化结构如下:
ecommerce
├── ecommerce-common // 公共工具、异常、常量、DTO
├── ecommerce-domain // 实体、Repository 接口、领域服务
├── ecommerce-service // 应用服务、业务编排、事务管理
├── ecommerce-web // Controller、配置、拦截器、统一返回
└── ecommerce-infrastructure // 与外部系统交互:Redis、MQ、第三方API
依赖方向严格向内:web → service → domain ← infrastructure。common 被所有模块依赖,不产生反向依赖。这样的模块边界让代码的“可测试性”和“可替换性”最大化。
如果你还不需要多模块的复杂度,单模块中通过包约定分层也很实用。
18.1.3 单模块内的包结构规范
在 src/main/java/com/example/project 下,建议采用以下包结构:
com.example.project
├── config // 配置类(@Configuration)
├── controller // 控制器
├── service // 业务接口
│ └── impl // 业务实现
├── repository // 数据访问接口(JPA Repository 或 MyBatis Mapper)
├── model // 模型
│ ├── entity // 数据库实体
│ ├── dto // 数据传输对象(请求/响应)
│ └── vo // 视图对象(前端展示)
├── exception // 自定义异常
├── constant // 常量、枚举
├── util // 通用工具类
├── interceptor // 拦截器
├── aspect // AOP 切面
└── Application.java // 启动类
关键原则:
service定义接口,service.impl存放实现。即便你只有一种实现,也要保留接口,方便未来 Mock 和扩展。model.dto和model.vo严格分离:DTO 用于服务间传输或请求参数绑定,VO 用于视图层展示。不要直接把实体返回给 Controller。- 禁止循环依赖:Service 之间可以互相调用,但必须警惕双向引用。如果出现
A → B且B → A的场景,说明业务边界需要重新划分。
18.2 命名规范与代码风格
统一的命名是团队协作的第一道防线。这里列出屡试不爽的实战规则。
18.2.1 接口与实现类
- Service 接口:
OrderService,UserService - Service 实现:
OrderServiceImpl,UserServiceImpl - Repository(Spring Data):
OrderRepository extends JpaRepository<Order, Long> - MyBatis Mapper:
OrderMapper
Controller 不要有复杂的业务逻辑,方法名要体现用途:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
@PostMapping
public Result<OrderVO> create(@Valid @RequestBody CreateOrderRequest request) {
return Result.success(orderService.createOrder(request));
}
}
18.2.2 实体与 DTO
- 数据库实体:使用
@Entity和@Table,类名与表名映射清晰,如Order,OrderItem。 - DTO 明确后缀:
CreateOrderRequestOrderResponseOrderSearchCriteria- 避免直接使用 Map 或 JSONObject 作为接口参数,丧失类型安全。
18.2.3 方法与变量命名
- 方法命名采用 动词 + 名词,体现操作:
createOrder,cancelOrder,findByUserId - 布尔方法加
is或can前缀:isExpired(),canCancel() - 变量名不缩写,宁可长:
orderRepository而不是orderRepo(IDE 有自动补全) - 常量使用全大写+下划线:
MAX_RETRY_TIMES,ORDER_STATUS_PENDING
18.3 统一返回格式与异常处理
一个项目中,API 返回格式混乱是最大痛疾之一。直接返回 String、Object、ResponseEntity 随意混搭,前端调用时要写无数个适配逻辑。必须从项目第一天就统一起来。
18.3.1 通用响应体
定义一个不可变的通用响应类:
public class Result<T> {
private int code;
private String message;
private T data;
// 私有构造,使用静态工厂
private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
}
所有的 Controller 方法务必返回 Result<T>。成功时调用 Result.success(data),业务失败时通过抛异常统一处理。
18.3.2 全局异常拦截
定义业务异常类和全局异常处理器:
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
// getter
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ":" + f.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.error(400, msg);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleUnknown(Exception e) {
log.error("未知错误", e);
return Result.error(500, "服务器内部错误");
}
}
实战要点:
- 不要吞掉异常信息,
BusinessException一定要带明确的业务错误码和提示。 - 参数校验失败时,返回给前端的消息要包含具体字段和原因,方便联调。
500错误对外只给模糊提示,具体堆栈记录到日志中。
18.3.3 状态码约定
定义枚举或常量管理业务状态码,与前端达成一致。例如:
| 状态码 | 含义 |
|-------|------|
| 200 | 成功 |
| 400 | 参数错误 |
| 401 | 未授权 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 409 | 冲突(如重复提交) |
| 500 | 服务器内部错误 |
业务错误可在 4xx 的范围内扩展自定义码,如 40001 表示用户名已存在。
18.4 日志规范
日志是问题排查的最后一道防线。良好的日志习惯能节约大量时间和生命。
18.4.1 日志级别使用准则
- ERROR:系统出现了严重故障,需要立即处理,如数据库连接失败、第三方接口不可用。
- WARN:潜在危险但不影响主流程,如使用了废弃的方法、配置缺失但走了默认值。
- INFO:关键业务流程节点,如订单状态变更、支付完成、定时任务开始/结束。
- DEBUG:开发调试用的详细信息,生产环境默认关闭。
禁止在循环内打印 INFO 日志,否则日志文件会瞬间撑爆磁盘。重要数据使用 占位符,而不是字符串拼接:
log.info("订单创建成功, orderId={}, userId={}", orderId, userId);
18.4.2 日志内容规范
- 必须携带关键业务标识,如订单号、用户ID。没有上下文的日志等于没有日志。
- 不得打印敏感信息(密码、身份证、手机号),必要时脱敏。
- 异常日志必须使用
log.error("消息", exception),打印完整堆栈,不能只 trace 一行e.getMessage()。
18.4.3 日志文件策略
生产环境推荐使用 logback-spring.xml 或 log4j2 配置文件,按如下策略:
- 按天滚动,保留 30 天
- 单文件大小限制(如 100 MB)
- 异步打印提高性能
- 根据包名设置不同级别,如框架层的
org.springframework设为 WARN,业务层设 DEBUG
18.5 代码质量工具与持续检查
规范如果只能靠 Code Review 来约束,终将逐渐走样。把检查自动化才是王道。
18.5.1 使用 Checkstyle 强制代码风格
在项目中配置 checkstyle.xml,并集成到 Maven / Gradle 构建中。可以将 Sun 或 Google 的 Java 风格作为基线,再根据团队习惯微调(如单行最大字符数、缩进等)。构建时执行 mvn checkstyle:check,未通过则构建失败。
18.5.2 使用 SpotBugs / PMD 静态分析
- SpotBugs(FindBugs 的后继)分析字节码,发现空指针、资源未关闭等潜在 Bug。
- PMD 检查未使用变量、重复代码、复杂度过高等问题。
- 集成到 CI 流水线,设置质量门槛。
18.5.3 单元测试覆盖率
使用 JaCoCo 生成覆盖率报告,约定核心模块(Service、工具类)覆盖率达到 80% 以上。不影响测试文化,但可以作为代码质量的重要参考。
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
</executions>
</plugin>
18.5.4 团队协作公约
最后,规范的落脚点是人。团队应在项目初期共同约定并形成文档(如 CONTRIBUTING.md),内容包括:
- 提交信息格式(如
feat: 添加用户注册接口) - 分支策略(Git Flow 或俗成约定)
- PR Review 标准(至少一人同意才能合并)
- 接口文档更新机制(使用 Spring REST Docs 或 Knife4j)
有了架构蓝图和行为约定,代码才能从一个“能跑的东西”变成一个“能活下去的系统”。当新同事加入时,他希望看到的是清晰的分层、一致的命名和优雅的错误处理,而不是全靠猜的迷宫。这正是本章希望帮你建立的东西。