SpringBoot 数据校验实战:用 @Valid / @Validated 替代 30% 判断逻辑

80 阅读2分钟

一、传统判断式校验的问题

public Result createUser(UserCreateRequest request) {
    if (request.getUsername() == null || request.getUsername().trim().isEmpty()) {
        return Result.fail("用户名不能为空");
    }
    if (request.getUsername().length() < 4 || request.getUsername().length() > 20) {
        return Result.fail("用户名长度需4-20字符");
    }
    if (!Pattern.matches("^[a-zA-Z0-9_]+$", request.getUsername())) {
        return Result.fail("用户名只能包含字母数字下划线");
    }
    // ... 继续校验邮箱、手机号、部门等字段
    // 真正的业务逻辑早已被这些判断湮灭
    return userRepository.save(request);
}

问题总结:

  • 重复性高:每个字段都写一堆判断逻辑
  • 不具备复用性:不同接口复制粘贴
  • 业务逻辑与数据验证耦合严重

二、引入 JSR 380

Maven 依赖引入

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

常用注解速查表

注解作用说明示例
@NotNull值不允许为 null@NotNull Integer age
@NotBlank字符串非 null 且非空@NotBlank String name
@Size限定字符串或集合的长度@Size(min=2, max=10)
@Pattern用正则表达式进行匹配@Pattern(regexp="^\d{4}$")
@Min/@Max限定数值边界@Min(18) / @Max(100)
@Email校验邮箱格式@Email String email
@Future日期必须在未来@Future LocalDateTime expireTime

三、标准 DTO 校验实战

@Data
public class UserCreateDTO {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 4, max = 20, message = "用户名长度4-20个字符")
    @Pattern(regexp = "^\w+$", message = "用户名只能包含字母数字下划线")
    private String username;


    @Email(message = "邮箱格式不正确")
    @NotNull(message = "邮箱不能为空")
    private String email;


    @NotNull(message = "部门ID不能为空")
    @Digits(integer = 6, fraction = 0, message = "部门ID必须是6位数字")
    private Integer deptId;


    @FutureOrPresent(message = "生效时间必须大于当前")
    private LocalDateTime effectiveTime;
}

控制器层接收校验:

@PostMapping("/user")
public Result createUser(@Valid @RequestBody UserCreateDTO dto) {
    // 校验通过后执行业务逻辑
    return userService.create(dto);
}

四、进阶校验策略

分组校验:多场景复用

public interface ValidationGroup {
    interface Create {}
    interface Update {}
}


public class UserDTO {
    @Null(groups = ValidationGroup.Create.class, message = "创建时ID必须为空")
    @NotNull(groups = ValidationGroup.Update.class, message = "更新时ID不能为空")
    private Long id;


    @NotBlank
    private String username;
}


// 控制器
@PostMapping("/user")
public Result create(@Validated(ValidationGroup.Create.class) @RequestBody UserDTO dto) {
    return Result.success();
}

嵌套对象校验

@Data
public class OrderCreateDTO {
    @Valid
    @NotNull(message = "地址不能为空")
    private AddressDTO address;


    @Valid
    @NotEmpty(message = "订单项不能为空")
    private List<OrderItemDTO> items;
}

自定义注解校验

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "手机号格式错误";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}


public class PhoneValidator implements ConstraintValidator<Phone, String> {
    private static final Pattern PATTERN = Pattern.compile("^1[3-9]\d{9}$");
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value == null || PATTERN.matcher(value).matches();
    }
}

五、增强处理与动态规则

全局异常捕获

@RestControllerAdvice
public class ValidationExceptionHandler {


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handle(MethodArgumentNotValidException ex) {
        Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        return Result.fail(400, "参数错误", errors);
    }
}

动态规则:从数据库加载正则

@Target({PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = DynamicCheckValidator.class)
public @interface DynamicCheck {
    String ruleKey();
    String message() default "不符合动态规则";
}


public class DynamicCheckValidator implements ConstraintValidator<DynamicCheck, String> {
    private String ruleKey;
    public void initialize(DynamicCheck annotation) { this.ruleKey = annotation.ruleKey(); }
    public boolean isValid(String value, ConstraintValidatorContext context) {
        String rule = DynamicRuleLoader.getRule(ruleKey); // 从数据库拉取
        return Pattern.matches(rule, value);
    }
}

六、校验体验优化

国际化消息配置

NotBlank.userCreateDTO.username=用户名不能为空
Size.userCreateDTO.username=用户名长度必须在{min}到{max}个字符之间

前端自动获取规则(通过 OpenAPI Schema)

@Operation(parameters = {
    @Parameter(name = "username", required = true,
        schema = @Schema(minLength = 4, maxLength = 20, pattern = "^\w+$"))
})
@GetMapping("/user")
public Result query(@RequestParam String username) {
    return Result.success();
}

写在最后:校验不止是输入验证,更是系统健壮性的保障

使用 @Valid / @Validated 替代传统的判断式逻辑,不只是减少了代码量,它真正做到了:

  • 将规则与数据模型解耦
  • 让控制器回归业务本质
  • 统一错误处理口径,提高可维护性

参数校验不是可选项,而是高质量代码不可或缺的一环。用好它,不仅能减少 30% 的判断逻辑,还能让你的系统更稳定、更优雅。