在业务开发时最怕的一件事,就是校验逻辑写得哪儿都是:
- 有些在 Controller
- 有些在 Service
- 有些靠数据库
- 有些写在前端
- 有些写在 if 判断里
- 有些写成注解
久而久之,业务校验变成一锅粥,想重构都无从下手。其实,大部分系统的问题不是“不会校验”,而是不知道校验应该放哪一层。
这篇文章旨在厘清业务校验最核心的职责划分:Controller、Service、数据库分别负责什么校验?边界在哪里? 理解并实践这一分层原则,将显著提升整个系统的可维护性与代码清晰度。
1. 先明确一点:只有三类校验
业务逻辑里的所有校验,拆开本质只有三类:
① 格式校验
比如字符串不能为空、手机号格式正确、金额必须大于 0。
② 业务校验
比如库存够不够、用户是否具有权限、订单状态是否允许支付。
③ 数据一致性校验
比如唯一性、外键、约束、状态合法性。
这三类校验,分别应该由 Controller、Service 和数据库协同完成。
2. Controller 的职责:只做“格式校验”
Controller 层的职责非常单纯:验证请求参数是否合法,禁止格式错误的脏数据进入业务层。
属于 Controller 的校验:
- 必填字段校验(不能为空)
- 数字范围校验(>0、>=0)
- 字符串格式校验(手机号、邮箱)
- 列表不能为空
- DTO 基础合法性
在 Java 的 Spring 框架中,原生支持通过 @Valid 注解进行声明式校验:
@PostMapping("/create")
public Result create(@Valid @RequestBody OrderDTO dto) {
orderService.create(dto);
return Result.ok();
}
对应的 DTO 类:
@Data
public class OrderDTO {
@NotNull(message = "商品ID不能为空")
private Long productId;
@Min(value = 1, message = "购买数量必须 >= 1")
private Integer amount;
}
Controller 的核心目标:
- 快速拒绝格式错误的输入。
- 防止 Service 层处理无意义的数据,减轻业务层负担。
- 提供清晰的接口契约。
注意:Controller 不应进行任何涉及业务规则的判断。
3. Service 的职责:业务校验核心区
最重要的校验层就是 Service,它负责处理所有“业务规则”:只要涉及业务含义和状态的判断,都属于 Service。
典型业务校验场景:
- 商品是否存在
- 库存是否足够
- 用户是否有权限执行操作
- 是否重复下单
- 订单状态是否允许进行下一步(如支付)
- 金额是否超过用户上限
- 业务条件是否存在冲突
示例代码:
public void createOrder(OrderDTO dto) {
// 业务校验:商品是否存在、状态是否可售、库存是否充足
Product product = productMapper.findById(dto.getProductId());
if (product == null) {
throw new BizException("商品不存在");
}
if (product.getStock() < dto.getAmount()) {
throw new BizException("库存不足");
}
if (!product.getStatus().equals(Status.ON_SALE)) {
throw new BizException("商品已下架");
}
// 校验通过,执行业务逻辑
saveOrder(dto);
}
为何业务校验必须放在 Service 层?
- 更贴近业务规则:Service 是业务逻辑的聚合点。
- 易于测试与复用:独立的业务校验方法便于单元测试,也方便被定时任务、消息队列消费者等其他组件调用。
- 保持控制器纯净:避免业务规则分散在各个 Controller 中,导致维护困难。
4. 数据库的职责:数据一致性的最后防线
有些校验是程序无法完全保证的,必须由数据库作为兜底,确保“不可被程序错误或并发操作绕过的核心规则”。
属于数据库的校验(通过约束实现):
- 唯一索引(防止重复)
- 非空约束(NOT NULL)
- 外键约束(保证关联完整性)
- 字段类型与长度限制
- 枚举值范围(通过 CHECK 约束或字段类型,如 TINYINT)
示例 SQL:
CREATE TABLE user (
id BIGINT PRIMARY KEY,
phone VARCHAR(20) NOT NULL UNIQUE,
status TINYINT NOT NULL
);
为什么这些必须交给数据库?
- 防御程序 Bug:即使程序逻辑疏于校验,数据库也能从底层拦截。
- 保证强一致性:在并发场景下,数据库约束是保证数据最终一致性的基石。
- 终极安全保障:建立无法从应用层绕过的数据规则底线。
一句话总结三层协作关系:
- Controller 拦住无效的格式输入。
- Service 拦住不符合业务规则的非法操作。
- 数据库 拦住底线级别的数据一致性错误。
三者职责清晰,缺一不可。
5. 三层职责边界总结
直接给出清晰结论:
✔ Controller 负责(格式校验):
- 参数格式是否正确
- 字段是否为空
- 类型是否合法
- 数值是否在约定范围内
- 不涉及任何具体业务规则的判断
✔ Service 负责(业务校验):
- 相关实体是否存在(如商品、用户)
- 操作是否被业务规则允许(如是否有权限)
- 状态是否正确,流程是否可继续
- 业务条件是否满足(如库存、余额)
- 是否构成重复操作
✔ 数据库负责(一致性校验):
- 字段不可为 NULL(非空约束)
- 数据唯一性(唯一索引)
- 关联关系有效性(外键约束)
- 数据类型与范围(字段类型、CHECK约束)
- 强一致性规则兜底
将校验放在正确的层级,是构建可维护系统的关键。
6. 一个反例:业务校验放在 Controller 会怎样?
观察下面这段问题代码:
@PostMapping("/create")
public Result create(@RequestBody OrderDTO dto) {
// 错误示范:在Controller中进行业务判断
if (!productService.isOnSale(dto.getProductId())) {
return Result.fail("商品下架");
}
if (!productService.stockEnough(dto.getProductId(), dto.getAmount())) {
return Result.fail("库存不足");
}
// ... 调用service
}
带来的问题:
- 逻辑无法复用:其他入口(如消息处理、定时任务)无法直接复用这套校验。
- 业务规则分散:相同的商品状态判断可能散落在多个Controller中。
- 违背单一职责:Controller 混杂了参数校验、业务判断、流程协调等多重职责。
久而久之,系统将变得难以理解和维护。
7. 更专业的落地方式:颗粒化的校验方法
在 Service 层中,推荐将校验逻辑拆分为独立的、颗粒化的方法:
// 商品校验
private void validateProduct(Long productId) { ... }
// 库存校验
private void validateStock(Long productId, int amount) { ... }
// 用户权限校验
private void validateUserPermission(Long userId) { ... }
public void createOrder(OrderDTO dto) {
validateProduct(dto.getProductId());
validateStock(dto.getProductId(), dto.getAmount());
validateUserPermission(getCurrentUserId());
// 所有校验通过后,执行业务逻辑
doCreateOrder(dto);
}
优点:
- 高可复用性:校验方法可以被同一Service内的多个主方法调用。
- 单一职责:每个校验方法只关心一个特定的规则,清晰明确。
- 便于测试:可以针对每个校验方法编写独立的单元测试。
- 易于维护:规则变更时,只需修改对应的校验方法。
此时,控制器将变得非常干净,只负责参数格式校验和请求转发:
@PostMapping("/create")
public Result create(@Valid @RequestBody OrderDTO dto) {
orderService.createOrder(dto); // 一行调用,清晰明了
return Result.ok();
}
总结
业务校验该放在哪一层?核心原则如下:
| 校验类型 |
负责层级 |
核心目标 |
| 格式校验 |
Controller |
输入“正确不正确” |
| 业务校验 |
Service |
操作“允许不允许” |
| 一致性校验 |
数据库 |
数据“绝不允许发生” |
遵循 Controller 校验格式、Service 校验业务、数据库保证底线 的分层规范,是在企业级项目中构建清晰、健壮、易维护架构的标准实践。通过明确各层边界,并利用像数据库约束这样的底层机制,可以显著提升代码质量和系统可靠性。