springboot参数校验 Validation

1、概述

在做开发的时候后端做参数校验检验必不可少,java提出了 Bean Validation 规范,并且经理了JSR303、JSR349、JSR380 三次标准。

Bean Validation 只提供规范,不提供具体的实现。

目前大多数开发中我们使用 Hibernate Validator 这个实现。

在spring 项目中,Spring Validation 提供了对 Bean Validation 的内置封装支持,可以使用 @Validated 注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。而在实现原理上,也是基于 Spring AOP 拦截,实现校验相关的操作

2、注解

2.1、 Bean Validation 中内置的 constraint

Constraint详细信息
@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@AssertTrue被注释的元素必须为 true
@AssertFalse被注释的元素必须为 false
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期
@Future被注释的元素必须是一个将来的日期
@Pattern(value)被注释的元素必须符合指定的正则表达式
@NotBlank只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0
@NotEmpty集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null

2.2、Hibernate Validator 附加的 constraint

Constraint详细信息
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串的必须非空
@Range被注释的元素必须在合适的范围内

2.3、@Valid 和 @Validated

@Valid注解, 是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。

@ValidatedSpring Validation 定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validatedvalue 属性,支持分组校验。

Spring Validation @Validated 注解,实现声明式校验。

相比较来说,使用@Valid注解的地方,多了==【成员变量】==,【构造方法】和【方法返回】,在有嵌套对象的使用只能使用@Valid

//User.java
public class User {

    private String id;

    @Valid
    private Resume resume;
}

//Resume.java
public class Resume {

    @NotBlank
    private String contact;
}

3、实践

3.1pom.xml中引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

3.2、UserAddDTO

@Data
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;
}

3.3、UserContoller

@Slf4j
@RestController
@RequestMapping("/users")
@Validated
public class UserContoller {

    @GetMapping("/get")
    public void get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
        log.info("[get][id: {}]", id);
    }

    @PostMapping("/add")
    public void add(@Valid @RequestBody UserAddDTO addDTO) {
        log.info("[add][addDTO: {}]", addDTO);
    }
}

  • 对于get(id)方法

在这里插入图片描述

在这里插入图片描述

抛出的异常是javax.validation.ConstraintViolationException

  • 对于add(@Valid @RequestBody UserAddDTO addDTO)方法

在这里插入图片描述

在这里插入图片描述

抛出的异常时org.springframework.web.bind.MethodArgumentNotValidException

4、校验异常–全局处理

从上面可以看出返回的数据不友好,我们可以根据全局异常处理,进行友好提示

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理 ServiceException 异常
     */
    @ResponseBody
    @ExceptionHandler(value = ServiceException.class)
    public Result serviceExceptionHandler(HttpServletRequest req, ServiceException ex) {
        log.debug("[serviceExceptionHandler]", ex);
        // 包装 Result 结果
        return Result.error(ex.getCode(), ex.getMessage());
    }

    // GlobalExceptionHandler.java

    //处理参数校验ConstraintViolationException异常
    @ResponseBody
    @ExceptionHandler(value = ConstraintViolationException.class)
    public Result constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
        log.debug("[constraintViolationExceptionHandler]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(constraintViolation.getMessage());
        }
        // 包装 Result 结果
        return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

    //处理参数校验BindException异常
    @ResponseBody
    @ExceptionHandler(value = BindException.class)
    public Result bindExceptionHandler(HttpServletRequest req, BindException ex) {
        log.info("[bindExceptionHandler]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ObjectError objectError : ex.getAllErrors()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(objectError.getDefaultMessage());
        }
        // 包装 Result 结果
        return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

    //处理参数校验MethodArgumentNotValidException异常
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result methodArgumentNotValidExceptionHandle(HttpServletRequest req, MethodArgumentNotValidException ex) {
        log.info("[methodArgumentNotValidExceptionHandle]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(objectError.getDefaultMessage());
        }
        // 包装 Result 结果
        return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }


    /**
     * 处理其它 Exception 异常
     */
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public Result exceptionHandler(HttpServletRequest req, Exception e) {
        // 记录异常日志
        log.error("[exceptionHandler]", e);
        // 返回 ERROR Result
        return Result.error(ServiceExceptionEnum.SYSTEM_ERROR.getCode(),
                ServiceExceptionEnum.SYSTEM_ERROR.getMessage());
    }
}

参数校验异常处理是一下三个

 //处理参数校验ConstraintViolationException异常
    @ResponseBody
    @ExceptionHandler(value = ConstraintViolationException.class)
    public Result constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
        log.debug("[constraintViolationExceptionHandler]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(constraintViolation.getMessage());
        }
        // 包装 Result 结果
        return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

    //处理参数校验BindException异常
    @ResponseBody
    @ExceptionHandler(value = BindException.class)
    public Result bindExceptionHandler(HttpServletRequest req, BindException ex) {
        log.info("[bindExceptionHandler]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ObjectError objectError : ex.getAllErrors()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(objectError.getDefaultMessage());
        }
        // 包装 Result 结果
        return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

    //处理参数校验MethodArgumentNotValidException异常
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result methodArgumentNotValidExceptionHandle(HttpServletRequest req, MethodArgumentNotValidException ex) {
        log.info("[methodArgumentNotValidExceptionHandle]", ex);
        // 拼接错误
        StringBuilder detailMessage = new StringBuilder();
        for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
            // 使用 ; 分隔多个错误
            if (detailMessage.length() > 0) {
                detailMessage.append(";");
            }
            // 拼接内容到其中
            detailMessage.append(objectError.getDefaultMessage());
        }
        // 包装 Result 结果
        return Result.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

  • get(id)方法

在这里插入图片描述

  • add(@Valid @RequestBody UserAddDTO addDTO)

在这里插入图片描述

5、自定义约束

很多时候无论是Bean Validation 定义的约束,还是 Hibernate Validator都无法满足我们的开发需求,所以我们需要自定义约束

自定义约束的步骤:

1、编写自定义约束的注解

2、编写自定义的校验器 ConstraintValidator

5.1、IntArrayValuable

public interface IntArrayValuable {
    /**
     * @return int 数组
     */
    int[] array();
}

5.2、GenderEnum

@Getter
@AllArgsConstructor
public enum GenderEnum implements IntArrayValuable {

    MALE(1, "男"),
    FEMALE(2, "女");

    /**
     * 性别值
     */
    private final Integer value;
    /**
     * 性别名
     */
    private final String name;

    public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(GenderEnum::getValue).toArray();

    @Override
    public int[] 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 实现 IntArrayValuable 接口的
     */
    Class<? extends IntArrayValuable> value();

    /**
     * @return 提示内容
     */
    String message() default "必须在指定范围 {value}";

    /**
     * @return 分组
     */
    Class<?>[] groups() default {};

    /**
     * @return Payload 数组
     */
    Class<? extends Payload>[] payload() default {};

}

5.4、InEnumValidator 自定义的校验器

就是第二步

public class InEnumValidator implements ConstraintValidator<InEnum, Integer> {

    /**
     * 值数组
     */
    private Set<Integer> values;

    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        // 校验通过
        if (values.contains(value)) {
            return true;
        }
        //校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)
        context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
        context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()
                .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句
        return false;
    }

    @Override
    public void initialize(InEnum annotation) {
        IntArrayValuable[] values = annotation.value().getEnumConstants();
        if (values.length == 0) {
            this.values = new HashSet<>();
        } else {
            this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toSet());
        }

    }
}

5.5使用

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

    /**
     * 性别
     */
    @NotNull(message = "性别不能为空")
    @InEnum(value = GenderEnum.class, message = "性别必须是 {value}")
    private Integer gender;
}

@Slf4j
@Validated
@RestController
@RequestMapping("/users")
public class UserContoller {

    @GetMapping("/get")
    public void get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
        log.info("[get][id: {}]", id);
    }

    @PostMapping("/add")
    public void add(@Valid @RequestBody UserAddDTO addDTO) {
        log.info("[add][addDTO: {}]", addDTO);
    }
}

在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值