《JSR303 数据校验全攻略:从入门到实战,玩转优雅的参数验证》

361 阅读3分钟

1. JSR303 是啥?为什么会有它

JSR 是 Java Specification Request(Java 规范请求),相当于 Java 官方给出的“行业标准”。
JSR303 全称是 Bean Validation 1.0,它是一套标准化的 Java Bean 校验规范。

简单来说:

  • 以前我们做参数校验可能是这样的:

    if (user.getUsername() == null || user.getUsername().trim().isEmpty()) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    if (!user.getEmail().matches(...)) {
        throw new IllegalArgumentException("邮箱格式不正确");
    }
    

    这种写法问题是:代码到处都是 if,规则分散,不好维护。

  • 有了 JSR303,我们直接在模型(DTO/Entity)上声明校验规则,框架帮我们自动检查,不符合就抛出异常:

    public class UserDTO {
        @NotBlank(message = "用户名不能为空")
        private String username;
    
        @Email(message = "邮箱格式不正确")
        private String email;
    }
    

    这样不但干净,还能一次声明,多处复用,也方便国际化、统一异常处理。


2. 谁来帮它工作?

JSR303 只是标准,具体执行需要一个实现。最常见的实现是 Hibernate Validator(Spring Boot 默认集成的就是它)。

你可以把它想象成:

  • JSR303 = “交通法规”
  • Hibernate Validator = “交警队”

3. 常用的校验注解(带小贴士)

空值相关

注解说明常见场景
@NotNull不能为 null(长度可以为 0)数据库主键、对象
@NotEmpty不能为 null 且长度 > 0集合、数组、字符串
@NotBlank不能为 null 且必须包含至少一个非空格字符用户输入的文本

小贴士

  • String,多数场景用 @NotBlank
  • 对集合/数组用 @NotEmpty
  • @NotNull 只保证非空,不管长度。

长度/大小

注解说明
@Size(min, max)字符串、集合、数组长度范围
@Min / @Max数值上下限(整数)
@DecimalMin / @DecimalMax数值上下限(支持小数)
@Positive / @Negative必须为正数/负数
@PositiveOrZero / @NegativeOrZero必须为正数/负数或 0

格式相关

注解说明
@Pattern(regexp)正则表达式匹配
@Email邮箱格式
@URLURL 格式(Hibernate 扩展)

日期时间

注解说明
@Past必须是过去的时间
@Future必须是未来的时间
@PastOrPresent过去或现在
@FutureOrPresent未来或现在

嵌套对象

注解说明
@Valid用于校验嵌套对象(如果 UserDTO 里有 ProfileDTO,且 ProfileDTO 也有注解,就得加它)

4. 在 Spring 里怎么用?

4.1 基础用法

@RestController
public class UserController {

    @PostMapping("/users")
    public String createUser(@Valid @RequestBody UserDTO user) {
        return "ok";
    }
}

如果 user 不符合规则,Spring 会自动抛出 MethodArgumentNotValidException


4.2 分组校验

创建和更新规则可能不同:

public interface Create {}
public interface Update {}

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

Controller:

@PostMapping
public String create(@Validated(Create.class) @RequestBody UserDTO dto) { ... }

@PutMapping
public String update(@Validated(Update.class) @RequestBody UserDTO dto) { ... }

这里必须用 @Validated@Valid 不支持分组)。


5. 常见异常类型

异常触发场景
MethodArgumentNotValidException@RequestBody 校验失败
BindException表单提交绑定失败
ConstraintViolationException方法参数校验失败(非 @RequestBody
ValidationException校验框架的基础异常

6. 全局异常处理

@ControllerAdvice + @ExceptionHandler 统一处理:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(e -> e.getField() + ": " + e.getDefaultMessage())
            .toList();
        return ResponseEntity.badRequest().body(Map.of("errors", errors));
    }
}

7. 自定义注解

比如公司内部手机号规则:

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

public class PhoneValidator implements ConstraintValidator<Phone, String> {
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && value.matches("^1\d{10}$");
    }
}

8. 程序化校验(不用注解也能校验)

@Autowired
private Validator validator;

public void check(UserDTO user) {
    Set<ConstraintViolation<UserDTO>> violations = validator.validate(user);
    if (!violations.isEmpty()) {
        violations.forEach(v -> System.out.println(v.getPropertyPath() + " " + v.getMessage()));
    }
}

9. 国际化(i18n)

ValidationMessages.properties 里:

user.username.notblank=用户名不能为空

DTO:

@NotBlank(message = "{user.username.notblank}")
private String username;

这样切换语言文件即可实现多语言提示。