一、传统判断式校验的问题
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% 的判断逻辑,还能让你的系统更稳定、更优雅。