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>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-aspectsartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        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;

    @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<? 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;

    @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<ConstraintViolation<UserAddDTO>> result = validator.validate(addDTO);
	// 打印校验结果 // <4>
	    for (ConstraintViolation<UserAddDTO> constraintViolation : result) {
	        // 属性:消息
	        System.out.println(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());
    	}
	}
}

掌握这些核心要点,你的 Spring Boot 参数校验体系将兼具 健壮性可维护性

你可能感兴趣的:(SpringBoot,Java,Spring,spring,boot,数据库,java)