Java 请求与领域校验
校验应放在哪一层:输入边界校验与领域不变量如何分工。
#concept
#growing
#dev
#java
[!info] related notes
Java 请求与领域校验
这个概念为什么出现
不可信输入是常态。校验失败应快速、明确;领域不变量应在对象出生与状态迁移时守住,而不是只靠数据库报错。
[!abstract] 一句话理解 输入校验检查形状与值域,领域逻辑守住不变量;身份认证、授权也必须在服务端执行,但不能由字段校验代替。
最小可运行示例
场景:下单命令校验
record PlaceOrder(String skuId, int qty) {
public PlaceOrder {
if (skuId == null || skuId.isBlank()) throw new IllegalArgumentException("skuId");
if (qty <= 0 || qty > 100) throw new IllegalArgumentException("qty");
}
}
public class PlaceOrderValidationDemo {
public static void main(String[] args) {
try {
new PlaceOrder("", 1);
} catch (IllegalArgumentException ex) {
System.out.println("reject: " + ex.getMessage());
}
System.out.println(new PlaceOrder("S1", 2));
}
}
javac PlaceOrderValidationDemo.java && java PlaceOrderValidationDemo
期望含:
reject: skuId
PlaceOrder[skuId=S1, qty=2]
结合场景再看三个关注点
- 构造期守不变量。
- 错误消息可映射到 API 字段。
- 此 record 只检查单条命令的值域;库存、SKU 是否存在和是否有权下单需在用例中核实。Bean Validation 也不取代这些规则。
核心概念与准确模型
1. 分层
| 层 | 校验什么 |
|---|---|
| 控制器 | 类型、必填、格式 |
| 应用服务 | 用例前置条件 |
| 领域 | 深度不变量 |
| 数据库 | 最后防线约束 |
2. Bean Validation
jakarta.validation 定义注解与 Validator API,需要 provider 与实际校验调用或框架集成;仅添加注解不会自动执行检查。@Valid 用于级联,也需校验流程被触发。不同约束的 null 语义不同,例如 @Size 允许 null,而 @NotNull 不允许。
边界情况与反直觉行为
- PATCH 需区分字段缺省与显式 null,不能直接套用完整创建请求的必填规则。
- 国际化消息。
- 检查库存后再扣减可能发生并发竞争;跨记录/资源约束应由事务、条件更新或数据库约束在写入时保证。
常见误区
[!warning] 常见误区:只做前端校验 可被绕过。
工程实践
- 统一错误响应结构。
- 复杂规则单测。
- 数据库约束双保险。
- 避免校验逻辑复制三份却不一致。
本节总结
- 边界 + 领域双层
- 失败要明确
- 框架注解是工具
自测题
- 为什么领域层仍要校验?
- 校验失败用 400 还是 422?
参考答案
- 用例可能被多种入口调用,不变量必须在核心守住。
- 400 可用于请求错误;422 表达内容类型和语法可理解但指令无法处理。按 API 契约稳定映射;不能把所有 IllegalArgumentException 都自动认作客户端错误,内部程序错误应另行处理。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Bean Validation | 规范/社区 | 注解校验 |