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 添加依赖
org.springframework.boot spring-boot-starter-web org.springframework spring-aspects org.springframework.boot spring-boot-starter-test test
- 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 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 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 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 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 { /** * @return 数组 */ T[] array(); }
5.2 CommonStatusEnum
@Getter @AllArgsConstructor public enum CommonStatusEnum implements ArrayValuable { 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; @Override public 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> value(); String message() default "必须在指定范围 {value}"; Class[] groups() default {}; Class values; @Override public 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()); } } @Override public 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 { @Autowired private Validator validator; public void validate(UserAddDTO addDTO) { Set result = validator.validate(addDTO); // 打印校验结果 // for (ConstraintViolation constraintViolation : result) { // 属性:消息 System.out.println(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage()); } } }
掌握这些核心要点,你的 Spring Boot 参数校验体系将兼具 健壮性 与 可维护性!
(图片来源网络,侵删)(图片来源网络,侵删)(图片来源网络,侵删)
- spring-boot-starter-web 依赖里,已经默认引入 hibernate-validator 依赖,所以本示例使用的是 Hibernate Validator 作为 Bean Validation 的实现框架。
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。