在后端开发中,数据校验是系统稳定的第一道防线。它不仅要验证参数格式(如手机号是否合法),更要守护业务规则(如 “订单金额不能为负”“库存不能超卖”)。一套完善的校验体系,能在请求到达业务逻辑前拦截无效数据,减少异常处理成本,提升系统可靠性。
数据校验的三层防护网
1. 参数格式校验:过滤明显无效数据
通过注解式校验(如 Spring 的@Valid),在 Controller 层快速拦截格式错误的参数:
@PostMapping("/users")
public Result createUser(@Valid @RequestBody UserCreateDTO userDTO) {
userService.create(userDTO);
return Result.success();
}
// 数据传输对象(DTO)
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\d{9}$", message = "手机号格式错误")
private String phone;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能为负数")
@Max(value = 150, message = "年龄不能超过150")
private Integer age;
}
常用校验注解:
@NotNull:不能为 null(适用于对象)@NotBlank:字符串不能为 null 且 trim 后不为空@Pattern:符合正则表达式(如手机号、邮箱)@Min/@Max:数值在指定范围内@Valid:嵌套校验(如 DTO 中包含子对象)
2. 业务规则校验:拦截逻辑无效数据
在 Service 层针对业务场景进行校验,确保数据符合业务规则:
@Service
public class OrderService {
@Autowired
private ProductMapper productMapper;
@Autowired
private UserMapper userMapper;
public void createOrder(OrderCreateDTO orderDTO) {
// 1. 校验商品是否存在且状态正常
Product product = productMapper.selectById(orderDTO.getProductId());
if (product == null) {
throw new BusinessException("商品不存在");
}
if (product.getStatus() != ProductStatus.AVAILABLE) {
throw new BusinessException("商品已下架");
}
// 2. 校验库存是否充足
if (product.getStock() < orderDTO.getQuantity()) {
throw new BusinessException("库存不足,当前库存:" + product.getStock());
}
// 3. 校验用户状态(如是否被禁用)
User user = userMapper.selectById(orderDTO.getUserId());
if (user.getStatus() == UserStatus.FROZEN) {
throw new BusinessException("用户账号已冻结,无法下单");
}
// 4. 执行下单逻辑(省略)
}
}
业务校验原则:
- 基于业务场景定制(如 “秒杀订单限购 1 件”“VIP 用户可超库存下单”)
- 包含上下文依赖(如 “用户等级决定最大下单数量”)
- 明确错误信息(让用户 / 开发者知道具体原因)
3. 数据一致性校验:防止并发导致的逻辑漏洞
在并发场景下,通过数据库锁或版本控制确保校验与操作的原子性:
// 扣减库存(防止超卖)
@Transactional
public void deductStock(Long productId, int quantity) {
// 1. 加行锁查询库存(确保校验和更新的原子性)
Product product = productMapper.selectForUpdateById(productId);
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 2. 扣减库存
productMapper.updateStock(productId, product.getStock() - quantity);
}
并发校验方案:
- 悲观锁:
select ... for update(适合写多读少场景) - 乐观锁:基于
version字段(适合读多写少场景) - 分布式锁:Redis/ZooKeeper 锁(跨服务并发场景)
校验体系的进阶实践
1. 统一异常处理:标准化错误响应
通过@ControllerAdvice统一捕获校验异常,返回标准化响应:
@ControllerAdvice
public class GlobalExceptionHandler {
// 处理参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidException(MethodArgumentNotValidException e) {
// 获取第一个错误信息
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.fail(400, message);
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.fail(400, e.getMessage());
}
// 处理系统异常
@ExceptionHandler(Exception.class)
public Result handleSystemException(Exception e) {
log.error("系统异常", e);
return Result.fail(500, "系统繁忙,请稍后再试");
}
}
2. 动态校验规则:支持规则配置化
对于频繁变化的校验规则(如 “活动期间最大下单金额”),可将规则存储在数据库,动态加载:
@Service
public class DynamicValidationService {
@Autowired
private ValidationRuleMapper ruleMapper;
// 动态校验订单金额
public void validateOrderAmount(Long orderId, BigDecimal amount) {
// 从数据库查询当前有效的金额规则
ValidationRule rule = ruleMapper.selectValidRule("order_max_amount");
if (rule == null) {
return; // 无规则时不校验
}
BigDecimal maxAmount = new BigDecimal(rule.getValue());
if (amount.compareTo(maxAmount) > 0) {
throw new BusinessException(rule.getErrorMessage());
}
}
}
避坑指南
-
避免校验逻辑重复:相同校验逻辑(如手机号格式)封装为工具类或自定义注解
-
校验信息要具体:错误信息需明确(如 “手机号格式错误” 而非 “参数错误”)
-
区分前端 / 后端校验:前端校验仅为用户体验,后端校验才是安全保障(防止绕过前端的恶意请求)
-
性能平衡:复杂校验(如跨库查询)需缓存结果,避免频繁校验影响性能
数据校验体系的完善程度,直接反映了系统的健壮性。它不是 “多余的代码”,而是 “减少故障的投资”—— 通过在源头拦截无效数据,让系统远离大部分异常场景,这是后端开发 “防患于未然” 的智慧。