Spring Validation参数校验

ValidValidated 的区别

区别 Valid Validated
提供者 JSR-303规范 Spring
是否支持分组 不支持 支持
标注位置 METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
嵌套校验 支持 不支持

引入依赖

如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。
如果spring-boot版本大于2.3.x,则需要手动引入依赖:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

预定义对象的说明

Result结果

接口统一返回Result格式的结果:

package com.qiankai.valid.common;

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * 返回结果
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 10:38
 */
@Data
@Accessors(chain = true)
public class Result<T> {
    private int code;
    private String message;
    private T data;

    public boolean ok() {
        return this.code == 0;
    }

    public static <T> Result<T> success() {
        return new Result<T>().setCode(0).setMessage("成功");
    }

    public static <T> Result<T> success(T data) {
        return new Result<T>().setCode(0).setMessage("成功").setData(data);
    }

    public static <T> Result<T> failure() {
        return new Result<T>().setCode(-1).setMessage("失败");
    }

    public static <T> Result<T> failure(int code, String msg) {
        return new Result<T>().setCode(code).setMessage(msg);
    }

    public static <T> Result<T> failure(int code, String msg, T data) {
        return new Result<T>().setCode(-1).setMessage("失败").setData(data);
    }
}

ErrorCode

全局异常错误码,后面统一处理异常会用到:

一般为了更好的处理全局异常,使用的错误码都是定义成枚举类型(包含错误码和错误描述),我这边方便演示就随便定义了一个类

package com.qiankai.valid.common;

/**
 * 错误码
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:08
 */
public final class ErrorCode {

    /**
     * 参数校验失败错误码
     */
    public static final int ARGUMENT_VALID_FAILURE = -2;
}

常用参数校验

在DTO上添加注解,实现参数校验,假设存在 UserDTO 如下:

@Data
public class UserDTO {
    private Long userId;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

RequestBody校验

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

/**
 * RequestBody 参数校验
 * 校验失败会抛出 MethodArgumentNotValidException 异常
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 10:37
 */
@RequestMapping("/api/user01")
@RestController
public class User01Controller {

    /**
     * RequestBody 参数校验
     * 使用 @Valid 和 @Validated 都可以
     */
    @PostMapping("/save/1")
    public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
        return Result.success();
    }

    @PostMapping("/save/2")
    public Result save2User(@RequestBody @Valid UserDTO userDTO) {
        return Result.success();
    }
}

RequestParam / PathVariable 校验

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserDTO;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * RequestMapping / PathVariable 参数校验
 * 校验失败会抛出 ConstraintViolationException 异常
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 10:57
 */
@RequestMapping("/api/user02")
@RestController
@Validated
public class User02Controller {

    /**
     * 此时必须在Controller上标注 @Validated 注解,并在入参上声明约束注解
     */

    /**
     * 路径变量
     * 添加约束注解 @Min
     */
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);
        userDTO.setAccount("11111111111111111");
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.success(userDTO);
    }

    /**
     * 查询参数
     * 添加约束注解 @Length @NotNull
     */
    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(10000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.success(userDTO);
    }
}

全局异常处理

上面如果校验失败,会抛出 MethodArgumentNotValidException 或者 ConstraintViolationException 异常。
在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。
比如我们系统要求无论发送什么异常,http的状态码必须返回200,由业务码去区分系统的异常情况。

package com.qiankai.valid.exception;

import com.qiankai.valid.common.ErrorCode;
import com.qiankai.valid.common.Result;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

/**
 * 统一异常处理
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:05
 */
@RestControllerAdvice
public class CommonExceptionHandler {
    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, msg);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.failure(ErrorCode.ARGUMENT_VALID_FAILURE, ex.getMessage());
    }
}

使用全局异常前

使用全局异常后

分组校验

有时候,为了区分业务场景,对于不同场景下的数据验证规则可能不一样(例如新增时可以不用传递 ID,而修改时必须传递ID),可以使用分组校验。

代码示例

DTO 如下:
在约束注解上声明适用的分组

package com.qiankai.valid.dto;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * 分组校验
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:12
 */
@Data
public class UserGroupValidDTO {

    @NotNull(groups = Update.class)
    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

Controller 如下:
Validated注解上指定校验的分组

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 分组校验
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 11:19
 */
@RestController
@RequestMapping("/api/user_group_valid")
public class UserGroupValidController {

    @PostMapping("/save")
    public Result saveUser(@RequestBody @Validated(UserGroupValidDTO.Save.class) UserGroupValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }

    @PostMapping("/update")
    public Result updateUser(@RequestBody @Validated(UserGroupValidDTO.Update.class) UserGroupValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }
}

嵌套校验

上面的校验主要是针对基本类型进行了校验,如果DTO中包含了自定义的实体类,就需要用到嵌套校验。

代码示例

DTO 如下:

package com.qiankai.valid.dto;

import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

/**
 * 嵌套校验
 * DTO中的某个字段也是一个对象,这种情况下,可以使用嵌套校验
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 12:21
 */
@Data
public class UserNestedValidDTO {
    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 此时DTO类的对应字段必须标记@Valid注解
     */
    @Valid
    @NotNull(groups = {Save.class, Update.class})
    private Job job;

    @Data
    public static class Job {

        @NotNull(groups = {Update.class})
        @Min(value = 1, groups = Update.class)
        private Long jobId;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

Controller 如下:

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserNestedValidDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 嵌套校验
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 13:31
 */
@RestController
@RequestMapping("/api/user_nested_valid")
public class UserNestedValidController {

    @PostMapping("/save")
    public Result saveUser(@RequestBody @Validated(UserNestedValidDTO.Save.class) UserNestedValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }

    @PostMapping("/update")
    public Result updateUser(@RequestBody @Validated(UserNestedValidDTO.Update.class) UserNestedValidDTO userDTO) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }
}

接口调用示例

嵌套参数校验-save-success

嵌套参数校验-update-success

根据DTO以及Controller中的校验规则,在update时,如果不传 jobId 嵌套校验就会报错,如下:

嵌套参数校验-update-failure

集合校验

如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。
此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!我们可以使用自定义list集合来接收参数:

代码示例

先自定义一个用于包装List的集合


package com.qiankai.valid.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Delegate;

import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;

/**
 * 包装 List类型,并声明 @Valid 注解
 * @param <E>
 */
@Getter
@Setter
public class ValidationList<E> implements List<E> {

    @Delegate // @Delegate是lombok注解
    @Valid // 一定要加@Valid注解
    public List<E> list = new ArrayList<>();

    // 一定要记得重写toString方法
    @Override
    public String toString() {
        return list.toString();
    }
}

@Delegate注解受lombok版本限制,1.18.6以上版本可支持。如果校验不通过,会抛出 NotReadablePropertyException,同样可以使用统一异常进行处理。

Controller 如下:

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import com.qiankai.valid.entity.ValidationList;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 集合校验
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 14:14
 */
@RestController
@RequestMapping("/api/valid_list")
public class ValidListController {

    @PostMapping("/saveList")
    public Result saveList(@RequestBody @Validated(UserGroupValidDTO.Save.class) ValidationList<UserGroupValidDTO> userList) {
        // 校验通过,才会执行业务逻辑处理
        return Result.success();
    }
}

调用结果

集合校验调用参数校验-failure

编程式校验

上面都是通过注解来进行校验,也可以使用编程的方式进行校验:

Controller:

package com.qiankai.valid.controller;

import com.qiankai.valid.common.Result;
import com.qiankai.valid.dto.UserGroupValidDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.ConstraintViolation;
import java.util.Set;

/**
 * 编程式校验参数
 *
 * @author kai_qian
 * @version v1.0
 * @since 2020/09/08 15:34
 */
@RequestMapping("/api/valid_with_code")
@RestController
public class ValidWithCodeController {
    @Autowired
    private javax.validation.Validator globalValidator;

    /**
     * 编程式校验
     */
    @PostMapping("/saveWithCodingValidate")
    public Result saveWithCodingValidate(@RequestBody UserGroupValidDTO userGroupValidDTO) {
        Set<ConstraintViolation<UserGroupValidDTO>> validate = globalValidator.validate(userGroupValidDTO, UserGroupValidDTO.Save.class);
        // 如果校验通过,validate为空;否则,validate包含未校验通过项
        if (validate.isEmpty()) {
            // 校验通过,才会执行业务逻辑处理

        } else {
            for (ConstraintViolation<UserGroupValidDTO> userGroupValidDTOConstraintViolation : validate) {
                // 校验失败,做其它逻辑
                System.out.println(userGroupValidDTOConstraintViolation);
                // throw new RuntimeException();
            }
        }
        return Result.success();
    }
}

结果如下:

配置快速失败

Spring Validation默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。

/**
 * 配置快速失败
 */
@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失败模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

参考博客:https://juejin.im/post/6856541106626363399

posted @ 2020-09-08 14:07  它山之玉  阅读(4588)  评论(0编辑  收藏  举报