Spring Boot 参数校验 Validation 终极指南
1. 概述
Spring Validation 基于 JSR-303(Bean Validation)规范,通过@Validated
注解实现声明式校验。核心优势:
- 零侵入性:基于 AOP 实现方法拦截校验
- 规范统一:兼容 Bean Validation 标准注解
- 功能扩展:支持分组校验、嵌套校验等高级特性
- 高效开发:减少 80% 的参数校验代码量
💡 关键区别:
@Validated
是 Spring 对@Valid
的增强封装,支持分组校验,而@Valid
支持嵌套校验
2. 注解体系
2.1 Bean Validation 标准注解
分类 | 注解 | 说明 |
---|---|---|
空值检查 | @NotBlank | 字符串非空且 trim() 后长度 > 0(仅适用于字符串) |
@NotEmpty | 集合/数组元素数 > 0,字符串长度 > 0(适用于集合、数组、字符串) | |
@NotNull | 字段值不能为 null | |
@Null | 字段值必须为 null | |
数值检查 | @DecimalMax(value) | 数值必须 ≤ 指定值(支持小数) |
@DecimalMin(value) | 数值必须 ≥ 指定值(支持小数) | |
@Digits(integer,fraction) | 整数部分最多 integer 位,小数部分最多 fraction 位 | |
@Positive | 必须为正数 | |
@PositiveOrZero | 必须为正数或 0 | |
@Max(value) | 数值必须 ≤ 指定值(仅限整数) | |
@Min(value) | 数值必须 ≥ 指定值(仅限整数) | |
@Negative | 必须为负数 | |
@NegativeOrZero | 必须为负数或 0 | |
布尔检查 | @AssertTrue | 必须为 true |
@AssertFalse | 必须为 false | |
长度检查 | @Size(min,max) | 字符串/集合/数组长度在 [min,max] 范围内 |
日期检查 | @Future | 必须是将来日期 |
@FutureOrPresent | 必须是将来或当前日期 | |
@Past | 必须是过去日期 | |
@PastOrPresent | 必须是过去或当前日期 | |
其他检查 | @Email | 符合邮箱格式(可配置宽松模式) |
@Pattern(regexp) | 符合正则表达式 |
2.2 Hibernate Validator 扩展注解
分类 | 注解 | 说明 |
---|---|---|
范围检查 | @Range(min,max) | 数值必须在 [min,max] 范围内(支持整型、BigDecimal) |
字符串检查 | @Length(min,max) | 字符串长度在 [min,max] 范围内 |
格式检查 | @URL | 合法 URL 格式(可指定协议/主机/端口等参数) |
安全校验 | @SafeHtml | 过滤危险 HTML 标签(防御 XSS 攻击) |
其他检查 | @LuhnCheck | 银行卡号校验(Luhn 算法) |
@CNPJ | 巴西企业税号校验 | |
@CPF | 巴西个人税号校验 |
2.3 @Valid vs @Validated
特性 | @Valid | @Validated |
---|---|---|
分组校验 | ❌ 不支持 | ✅ 支持 |
嵌套校验 | ✅ 支持 | ❌ 不支持 |
校验触发 | 自动触发 | 需配合AOP使用 |
3. 快速入门
3.1 添加依赖
<dependencies><!-- 实现对 Spring MVC 的自动化配置 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 保证 Spring AOP 相关的依赖包 --><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId></dependency><!-- 方便等会写单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies>
- spring-boot-starter-web 依赖里,已经默认引入 hibernate-validator 依赖,所以本示例使用的是 Hibernate Validator 作为 Bean Validation 的实现框架。
3.2 DTO 对象示例
public class UserAddDTO {/*** 账号*/@NotEmpty(message = "登录账号不能为空")@Length(min = 5, max = 16, message = "账号长度为 5-16 位")@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")private String username;/*** 密码*/@NotEmpty(message = "密码不能为空")@Length(min = 4, max = 16, message = "密码长度为 4-16 位")private String password;// ... 省略 setting/getting 方法
}
3.3 启用校验
@RestController
@RequestMapping("/users")
@Validated
public class UserController {private Logger logger = LoggerFactory.getLogger(getClass());@GetMapping("/get")public void get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {logger.info("[get][id: {}]", id);}@PostMapping("/add")public void add(@Valid UserAddDTO addDTO) {logger.info("[add][addDTO: {}]", addDTO);}}
4. 统一异常处理
4.1 @Valid 的异常处理
当使用 @Valid 注解进行参数校验时,校验失败会抛出 MethodArgumentNotValidException
全局拦截示例:
@RestControllerAdvice
public class GlobalExceptionHandler {/**** 触发场景* 对象参数校验失败(如 @RequestBody + @Valid)* 常见使用组合* @Valid + DTO 对象* 校验注解适用对象* 对象属性级校验(@NotNull/@Size 等)*/@ExceptionHandler(value = MethodArgumentNotValidException.class)public Result handleValidException(MethodArgumentNotValidException e) {BindingResult bindingResult = e.getBindingResult();List<String> errors = bindingResult.getFieldErrors().stream().map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()).collect(Collectors.toList());return Result.error(400, "参数校验失败", errors);}
}
4.2 @Validated 的异常处理
当使用 @Validated 注解时,需要分场景处理:
场景 1:Controller 层方法参数校验
如果直接在 Controller 方法参数上使用 @Validated 校验简单类型(如 @RequestParam、@PathVariable),校验失败会抛出 ConstraintViolationException。
全局拦截示例:
@RestControllerAdvice
public class GlobalExceptionHandler {/**** 触发场景* 方法参数直接校验* 常见使用组合* @Validated + 方法参数校验* 校验注解适用对象* 方法参数级校验(@RequestParam + @NotBlank 等)* 需要在类上标注 @Validated 才能触发 ConstraintViolationException*/@ExceptionHandler(ConstraintViolationException.class)public Result handleConstraintViolation(ConstraintViolationException e) {List<String> errors = e.getConstraintViolations().stream().map(v -> v.getPropertyPath() + ": " + v.getMessage()).collect(Collectors.toList());return Result.error(400, "参数校验失败", errors);}}
场景 2:校验对象参数
如果校验对象参数(如 @RequestBody),行为与 @Valid 一致,抛出 MethodArgumentNotValidException(处理方式同 @Valid)。
完整异常处理配置
@RestControllerAdvice
public class GlobalExceptionHandler {/**** 触发场景* 对象参数校验失败 如 (@RequestBody + @Valid/@RequestBody + @Validated)* 常见使用组合* (@Valid + DTO 对象/@Validated + DTO 对象)* 校验注解适用对象* 对象属性级校验(@NotNull/@Size 等)*/@ExceptionHandler(MethodArgumentNotValidException.class)public Result handleMethodArgumentNotValid(MethodArgumentNotValidException e) {BindingResult result = e.getBindingResult();List<String> errors = result.getFieldErrors().stream().map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage()).collect(Collectors.toList());return Result.error(400, "对象参数校验失败", errors);}/**** 触发场景* 方法参数直接校验* 常见使用组合* @Validated + 方法参数校验* 校验注解适用方法参数* 方法参数级校验(@RequestParam + @NotBlank 等)* 需要在类上标注 @Validated 才能触发 ConstraintViolationException*/@ExceptionHandler(ConstraintViolationException.class)public Result handleConstraintViolation(ConstraintViolationException e) {List<String> errors = e.getConstraintViolations().stream().map(v -> v.getPropertyPath() + ": " + v.getMessage()).collect(Collectors.toList());return Result.error(400, "简单参数校验失败", errors);}
}
4.3 关键总结
注解 | 使用场景 | 抛出异常 |
---|---|---|
@Valid | 校验对象参数(如 @RequestBody) | MethodArgumentNotValidException |
@Validated | 校验简单类型参数(如 @RequestParam) | ConstraintViolationException |
@Validated | 校验对象参数(需配合 @Valid 使用) | MethodArgumentNotValidException |
5. 自定义约束
在大多数项目中,无论是 Bean Validation 定义的约束,还是 Hibernate Validator 附加的约束,都是无法满足我们复杂的业务场景。所以,我们需要自定义约束。
开发自定义约束一共只要两步:
- 1)编写自定义约束的注解;
- 2)编写自定义的校验器 ConstraintValidator 。
下面,就让我们一起来实现一个自定义约束,用于校验参数必须在枚举值的范围内。
5.1 ArrayValuable
public interface ArrayValuable<T> {/*** @return 数组*/T[] array();
}
5.2 CommonStatusEnum
@Getter
@AllArgsConstructor
public enum CommonStatusEnum implements ArrayValuable<Integer> {ENABLE(0, "开启"),DISABLE(1, "关闭");public static final Integer[] ARRAYS = Arrays.stream(values()).map(CommonStatusEnum::getStatus).toArray(Integer[]::new);/*** 状态值*/private final Integer status;/*** 状态名*/private final String name;@Overridepublic Integer[] array() {return ARRAYS;}}
5.3 @InEnum
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.ANNOTATION_TYPE,ElementType.CONSTRUCTOR,ElementType.PARAMETER,ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {InEnumValidator.class}
)
public @interface InEnum {/*** @return 实现 ArrayValuable 接口的类*/Class<? extends ArrayValuable<?>> value();String message() default "必须在指定范围 {value}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};}
5.4 InEnumValidator
public class InEnumValidator implements ConstraintValidator<InEnum, Object> {private List<?> values;@Overridepublic void initialize(InEnum annotation) {ArrayValuable<?>[] values = annotation.value().getEnumConstants();if (values.length == 0) {this.values = Collections.emptyList();} else {this.values = Arrays.asList(values[0].array());}}@Overridepublic boolean isValid(Object value, ConstraintValidatorContext context) {// 为空时,默认不校验,即认为通过if (value == null) {return true;}// 校验通过if (values.contains(value)) {return true;}// 校验不通过,自定义提示语句context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句return false;}}
5.5 UserUpdateStatusDTO
public class UserUpdateStatusDTO{/*** 用户编号*/@NotNull(message = "用户编号不能为空")private Integer id;/*** 状态*/@NotNull(message = "状态不能为空")@InEnum(value = CommonStatusEnum .class, message = "状态必须是 {value}")private Integer status;// ... 省略 set/get 方法
}
5.6 UserController
@PostMapping("/update_status")
public void updateStatus(@Valid UserUpdateStatusDTO updateStatusDTO) {logger.info("[updateStatus][updateStatusDTO: {}]", updateStatusDTO);
}
6. 分组校验
6.1 UserUpdateStatusDTO
public class UserUpdateStatusDTO {/*** 分组 01 ,要求状态必须为 true*/public interface Group01 {}/*** 状态 02 ,要求状态必须为 false*/public interface Group02 {}/*** 状态*/@AssertTrue(message = "状态必须为 true", groups = Group01.class)@AssertFalse(message = "状态必须为 false", groups = Group02.class)private Boolean status;// ... 省略 set/get 方法
}
6.2 UserController
@PostMapping("/update_status_true")
public void updateStatusTrue(@Validated(UserUpdateStatusDTO.Group01.class) UserUpdateStatusDTO updateStatusDTO) {logger.info("[updateStatusTrue][updateStatusDTO: {}]", updateStatusDTO);
}@PostMapping("/update_status_false")
public void updateStatusFalse(@Validated(UserUpdateStatusDTO.Group02.class) UserUpdateStatusDTO updateStatusDTO) {logger.info("[updateStatusFalse][updateStatusDTO: {}]", updateStatusDTO);
}
7. 手动触发校验
@Service
public class ManualValidateService {@Autowiredprivate Validator validator;public void validate(UserAddDTO addDTO) {Set<ConstraintViolation<UserAddDTO>> result = validator.validate(addDTO);// 打印校验结果 // <4>for (ConstraintViolation<UserAddDTO> constraintViolation : result) {// 属性:消息System.out.println(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());}}
}
掌握这些核心要点,你的 Spring Boot 参数校验体系将兼具 健壮性 与 可维护性!