一,数据校验框架的产生背景
以Web项目为例,用户需要填写表单信息保存提交,页面输入信息需要进行数据格式校验,并且返回对应的错误提示,以此来达到数据校验的目的,从而避免无效数据被保存或者提交,这些检查工作包括必填项检查、数值检查、长度检查、身份证号码、手机号码检查等工作,当请求参数格式不正确的时候,需要程序监测到,对于前后端分离开发过程中,数据校验还需要返回对应的状态码和错误提示信息。
如果将这些字段校验和业务逻辑混合一起写,则会:
- 代码极其臃肿,且不容易维护
- 干扰原有逻辑
二,数据校验框架的演变过程
-
原始阶段:逐个字段、逐个对象进行硬编码校验
-
使用自定义工具类
-
标准化阶段:JSR-303 (Bean Validation 1.0)
为了解决上述问题,Java社区引入了JSR-303(Bean Validation 1.0),这是Java EE 6的一部分。JSR-303定义了一组标准注解和机制,用于声明性地进行参数校验。Hibernate Validator成为了JSR-303的参考实现。
-
现代阶段:JSR-380 (Bean Validation 2.0)
随着Java 8的引入,JSR-380(Bean Validation 2.0)作为JSR-303的改进版被发布,成为Java EE 8的一部分。JSR-380增加了对新的数据类型(如
Optional)的支持,并引入了一些新的注解。 -
集成框架:Spring Boot和Hibernate Validator
Spring Boot进一步简化了Bean Validation的使用,通过自动配置和依赖管理,使得在Spring应用中使用JSR-303/JSR-380变得非常简单。
三,SpringBoot参数校验快速入门
-
导包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> -
创建一个实体类,并在其字段上使用注解来进行校验:
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class User { @NotNull(message = "ID不能为空") private Long id; @NotBlank(message = "姓名不能为空") @Size(min = 2, max = 30, message = "姓名长度必须在2到30个字符之间") private String name; // getter和setter方法 } -
在Controller中,通过
@Valid或@Validated注解启用参数校验@PostMapping("/valid") public Result createUser(@Valid @RequestBody User user) { return Result.success(1); } -
创建全局异常管理器来接受校验失败的异常用来返回统一响应结果
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public Result handleValidationExceptions(MethodArgumentNotValidException ex) { StringBuilder errors = new StringBuilder(); for (FieldError error : ex.getBindingResult().getFieldErrors()) { errors.append(error.getField()).append(": ").append(error.getDefaultMessage()).append("; "); } return Result.error(errors.toString()); } @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result handleConstraintViolationException(ConstraintViolationException ex) { StringBuilder errors = new StringBuilder(); ex.getConstraintViolations().forEach(violation -> errors.append(violation.getPropertyPath()).append(": ").append(violation.getMessage()).append("; ") ); return Result.error(errors.toString()); } }
四,常用的字段校验注解
4.1 基本校验注解
-
@NotNull:确保属性不能为null@NotNull(message = "字段不能为空") private String name; -
@NotEmpty:确保字段的值不是null且长度大于0(用于集合或字符串)。@NotEmpty(message = "列表不能为空") private List<String> items; -
@NotBlank:确保字段的值不是null且经过修剪后长度大于0(用于字符串)。@NotBlank(message = "字段不能为空且不能为空白字符") private String description; -
@Size:验证字符序列、集合、数组的大小是否在指定范围内。@Size(min = 2, max = 30, message = "姓名长度必须在2到30个字符之间") private String name;
4.2 数字校验注解
-
@Min:验证字段的值大于或等于指定的最小值(用于数字)@Min(value = 18, message = "年龄必须大于或等于18") private int age; -
@Max:验证字段的值小于或等于指定的最大值(用于数字)@Max(value = 65, message = "年龄必须小于或等于65") private int age; -
@DecimalMin:验证字段的值大于或等于指定的最小值(用于小数)。@DecimalMin(value = "0.1", message = "金额必须大于或等于0.1") private BigDecimal amount; -
@DecimalMax:验证字段的值小于或等于指定的最大值(用于小数)。@DecimalMax(value = "100.0", message = "金额必须小于或等于100.0") private BigDecimal amount; -
@Digits:验证字段的值是否符合指定的整数和小数位数@Digits(integer = 3, fraction = 2, message = "数字必须是一个最多包含3位整数和2位小数的数值") private BigDecimal price; -
@Positive:验证字段的值为正数@Positive(message = "数量必须是正数") private int quantity; -
@PositiveOrZero:验证字段的值为正数或零。@PositiveOrZero(message = "数量必须是正数或零") private int quantity; -
@Negative:验证字段的值为负数@Negative(message = "损失必须是负数") private int loss; -
@NegativeOrZero:验证字段的值为负数或零。@NegativeOrZero(message = "损失必须是负数或零") private int loss;
4.3 时间日期校验注解
-
@Past:验证日期是否在过去@Past(message = "生日必须在过去") private LocalDate birthDate; -
@PastOrPresent:验证日期是否在过去或现在。@PastOrPresent(message = "注册日期必须在过去或现在") private LocalDate registrationDate; -
@Future:验证日期是否在未来。@Future(message = "预约日期必须在未来") private LocalDate appointmentDate; -
@FutureOrPresent:验证日期是否在未来或现在。@FutureOrPresent(message = "预约日期必须在未来或现在") private LocalDate appointmentDate;
4.4 字符串校验注解
-
@Email:验证字段是否符合电子邮件格式@Email(message = "电子邮件格式不正确") private String email; -
@Pattern:验证字段是否符合指定的正则表达式。@Pattern(regexp = "^[A-Za-z0-9]+$", message = "用户名只能包含字母和数字") private String username;
4.5 其他常用校验注解
-
@AssertTrue:验证字段的值为true。@AssertTrue(message = "必须同意条款") private boolean agreed; -
@AssertFalse:验证字段的值为false。@AssertFalse(message = "必须不同意条款") private boolean declined;
4.6 自定义校验注解
-
自定义注解
@Constraint(validatedBy = CustomValidator.class) @Target({ ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface CustomConstraint { String message() default "自定义校验错误信息"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } -
自定义注解实现
public class CustomValidator implements ConstraintValidator<CustomConstraint, String>{ @Override public void initialize(CustomConstraint constraintAnnotation) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 实现自定义校验逻辑 return value != null && value.matches("[A-Za-z]+"); } } -
使用自定义注解
public class User { @CustomConstraint private String customField; // 其他字段和方法 }
4.7 创建全局异常处理器拦截异常
在Spring Boot中,处理参数校验时可能会遇到两种主要的异常类型:MethodArgumentNotValidException和ConstraintViolationException。这两种异常处理的场景和用途略有不同。
MethodArgumentNotValidException
-
异常类型:
MethodArgumentNotValidException是 Spring Framework 提供的一个异常类,用于处理使用@Valid注解进行参数校验失败的情况。 -
触发时机:当你在Controller层使用
@Valid注解对请求体进行校验时,如果校验失败,Spring会抛出此异常。常见于 POST 和 PUT 请求中的请求体(@RequestBody)校验。
ConstraintViolationException
-
异常类型:
ConstraintViolationException是 Java Bean Validation API 提供的一个异常类,用于处理约束校验失败的情况。 -
触发时机:当在方法参数上使用 Bean Validation 注解(如
@PathVariable、@RequestParam)进行校验时,如果参数不符合约束条件,会抛出此异常。适用于请求参数、路径变量等的校验。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationExceptions(MethodArgumentNotValidException ex) {
StringBuilder errors = new StringBuilder();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.append(error.getField()).append(": ").append(error.getDefaultMessage()).append("; ");
}
return Result.error(errors.toString());
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handleConstraintViolationException(ConstraintViolationException ex) {
StringBuilder errors = new StringBuilder();
ex.getConstraintViolations().forEach(violation ->
errors.append(violation.getPropertyPath()).append(": ").append(violation.getMessage()).append("; ")
);
return Result.error(errors.toString());
}
}