人人都会AI编程

第 18 章 项目架构与代码规范

更新时间:2026-07-11

前面章节已经覆盖了 Spring Boot 的核心技术点,从容器、数据访问到微服务治理。但是,知道怎么用和知道怎么把一堆代码组织成可维护的系统,完全不是一回事。在团队协作和长期迭代中,清晰的架构和统一的规范远比某个具体技巧重要得多。

本章将以一个真实的中型 Spring Boot 项目为蓝本,梳理出经过验证的架构模式与编码规范,帮助你在团队中建立共同的“代码语言”,减少沟通成本,降低腐化风险。

18.1 项目模块划分与分层架构

18.1.1 单体应用的两种分层模式

对于多数 Spring Boot 应用,推荐使用 分层架构,业务复杂度优先于技术炫技。常见的有两种:

  1. 传统三层架构(Controller – Service – Repository)

这是最朴素、最被广泛接受的模式。所有业务逻辑集中在 Service 层,Controller 仅负责参数接收和响应封装,Repository(或 DAO)负责数据存取。

  1. 领域驱动设计(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

依赖方向严格向内:webservicedomaininfrastructurecommon 被所有模块依赖,不产生反向依赖。这样的模块边界让代码的“可测试性”和“可替换性”最大化。

如果你还不需要多模块的复杂度,单模块中通过包约定分层也很实用。

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.dtomodel.vo 严格分离:DTO 用于服务间传输或请求参数绑定,VO 用于视图层展示。不要直接把实体返回给 Controller。
  • 禁止循环依赖:Service 之间可以互相调用,但必须警惕双向引用。如果出现 A → BB → 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 明确后缀:
  • CreateOrderRequest
  • OrderResponse
  • OrderSearchCriteria
  • 避免直接使用 Map 或 JSONObject 作为接口参数,丧失类型安全。

18.2.3 方法与变量命名

  • 方法命名采用 动词 + 名词,体现操作:createOrder, cancelOrder, findByUserId
  • 布尔方法加 iscan 前缀:isExpired(), canCancel()
  • 变量名不缩写,宁可长:orderRepository 而不是 orderRepo(IDE 有自动补全)
  • 常量使用全大写+下划线:MAX_RETRY_TIMES, ORDER_STATUS_PENDING

18.3 统一返回格式与异常处理

一个项目中,API 返回格式混乱是最大痛疾之一。直接返回 StringObjectResponseEntity 随意混搭,前端调用时要写无数个适配逻辑。必须从项目第一天就统一起来。

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.xmllog4j2 配置文件,按如下策略:

  • 按天滚动,保留 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)

有了架构蓝图和行为约定,代码才能从一个“能跑的东西”变成一个“能活下去的系统”。当新同事加入时,他希望看到的是清晰的分层、一致的命名和优雅的错误处理,而不是全靠猜的迷宫。这正是本章希望帮你建立的东西。