【无标题】

SpringBoot参数校验@Validated、@Valid(javax.validation)
一、应用场景
在实际开发中,前端校验并不安全,任何人都可以通过接口来调用我们的服务,就算加了一层token的校验,有心人总会转空子,来传各式各样错误的参数,如果后端不校验,导致数据库数据混乱、特别是关于金额的数据,可能一个接口把公司都给干倒了
二、原生参数校验
0、返回类(可以不用看)
java复制代码/**

  • 用于返回

  • @param
    */
    @ApiModel(“统一返回类”)
    public class Results {
    public static final String ERROR = “500”;
    public static final String SUCCESS = “200”;

    /**

    • 返回码
      */
      @ApiModelProperty(“返回码,正确码为:200”)
      private String resCode ;

    /**

    • 返回消息
      */
      @ApiModelProperty(“返回消息”)
      private String msg ;

    /**

    • 返回实体
      */
      @ApiModelProperty(“返回实体”)
      private T obj;

    public static Results success(){
    return success(SUCCESS,“成功”,null);
    }

    public static Results success(String msg){
    return success(SUCCESS,msg,null);
    }

    public static Results success(T obj){
    return success(SUCCESS,“成功”,obj);
    }

    public static Results success(String msg,T obj){
    return success(SUCCESS,msg,obj);
    }

    public static Results success(String resCode,String msg,T obj){
    Results result = new Results();
    result.setResCode(resCode);
    result.setMsg(msg);
    result.setObj(obj);
    return result;
    }

    public static Results failed() {
    return failed(ERROR,“失败”,null);
    }

    public static Results failed(String msg) {
    return failed(ERROR,msg,null);
    }

    public static Results failed(String msg,T obj) {
    return failed(ERROR,msg,obj);
    }

    public static Results failed(String resCode,String msg) {
    return failed(resCode,msg,null);
    }

    public static Results failed(Integer resCode,String msg) {
    return failed(String.valueOf(resCode),msg);
    }

    public static Results failed(String resCode,String msg,T obj) {
    Results result = new Results();
    result.setResCode(resCode);
    result.setMsg(msg);
    result.setObj(obj);
    return result;
    }

    public static Results failedNoPermission() {
    return failed(90005,“没有权限”);
    }
    public static Results failedNoPermission(String msg) {
    return failed(90005,msg);
    }

    public static Results failedParameterException() {
    return failed(90004,“参数异常”);
    }
    public static Results failedParameterException(String msg) {
    return failed(90004,msg);
    }

    public static Results failedLoginException() {
    return failed(90002,“登录失败”);
    }
    public static Results failedLoginException(String msg) {
    return failed(90002,msg);
    }

    public String getResCode() {
    return resCode;
    }

    public void setResCode(String resCode) {
    this.resCode = resCode;
    }

    public String getMsg() {
    return msg;
    }

    public void setMsg(String msg) {
    this.msg = msg;
    }

    public T getObj() {
    return obj;
    }

    public void setObj(T obj) {
    this.obj = obj;
    }

    @Override
    public String toString() {
    return “Results{” +
    “resCode='” + resCode + ‘’’ +
    “, msg='” + msg + ‘’’ +
    “, obj=” + obj +
    ‘}’;
    }
    }

1、实体类
java复制代码@ApiModel(“测试 validation 入参”)
@Data
public class TestDto {
@ApiModelProperty(value = “名字”,required = true)
private String name;
@ApiModelProperty(value = “年龄”,required = true)
private Integer age;
@ApiModelProperty(value = “爱好”,required = true)
private List hobbies;
}

2、服务层(为了方便,我直接跟Controller写在一起了)
我们可以看见如果参数过大,要一个一个筛选条件十分浪费时间
java复制代码@RestController
// lombok 的日志注解
@Slf4j
// swagger 的注解
@Api(“测试”)
public class TestController {

@PostMapping("/testValidation")
// swagger 的注解
@ApiOperation("测试 validation")
public Results testValidation(@RequestBody TestDto dto){
    try {
        log.info("test 入参 dto={}",dto);
        // 这要一个一个的塞,很浪费时间
        if (dto.getName() == null || "".equals(dto.getName().trim())){
            return Results.failed("名字不能为空");
        }
        if (dto.getAge() == null){
            return Results.failed("年龄不能为空");
        }
        if (dto.getHobbies() == null || dto.getHobbies().size() == 0){
            return Results.failed("爱好不能为空");
        }
        return Results.success();
    } catch (Exception e) {
        log.error("test 报错",e);
        return Results.failed();
    }
}

}

三、使用 javax.validation 进行参数校验
1、导包
xml复制代码
javax.validation
validation-api
2.0.1.Final


org.springframework.boot
spring-boot-starter-validation

2、全局异常处理类

springboot开发中,在进行 入参校验 时,会抛出异常,最终全局统一捕获异常。

表单绑定到 java bean 出错时,会抛出 BindException 异常
将请求体解析并绑定到 java bean 时,如果出错,则抛出 MethodArgumentNotValidException 异常
普通参数(非 java bean)校验出错时,会抛出 ConstraintViolationException 异常

java复制代码@RestControllerAdvice
public class ExceptionControllerAdvice {

/**
 * 将请求体解析并绑定到 java bean 时,如果出错
 * 表单绑定到 java bean 出错
 * 校验参数 @RequestBody 时的异常
 * 注意 @Validated 需要放在 自定义实体类(入参) 的类上
 * 例如 :
 * public Results testUpdate(@Validated(Update.class) @RequestBody TestDto dto){}
 */
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Results MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    // 从异常对象中拿到ObjectError对象
    BindingResult br = e.getBindingResult();
    if (br.hasFieldErrors()) {
        List<FieldError> fieldErrorList = br.getFieldErrors();
        List<String> errors = new ArrayList<>(fieldErrorList.size());
        for (FieldError error : fieldErrorList) {
            errors.add(error.getField() + ":" + error.getDefaultMessage());
        }
        // 然后提取错误提示信息进行返回
        return Results.failed(errors.toString());
    }
    // 然后提取错误提示信息进行返回
    return Results.failed("校验错误");
}

/**
 * 表单绑定到 java bean 出错
 *
 * @param e
 * @return
 */
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BindException.class)
public Results MethodArgumentNotValidExceptionHandler(BindException e) {
    // 从异常对象中拿到ObjectError对象
    BindingResult br = e.getBindingResult();
    if (br.hasFieldErrors()) {
        List<FieldError> fieldErrorList = br.getFieldErrors();
        List<String> errors = new ArrayList<>(fieldErrorList.size());
        for (FieldError error : fieldErrorList) {
            errors.add(error.getField() + ":" + error.getDefaultMessage());
        }
        // 然后提取错误提示信息进行返回
        return Results.failed(errors.toString());
    }
    // 然后提取错误提示信息进行返回
    return Results.failed("校验错误");
}

/**
 * 普通参数(非 java bean)校验出错
 * 校验参数 @RequestParam @PathVariable 时的异常
 * 注意 @Validated 需要放在controller的类上
 * 例如:
 *
 * @RestController
 * @RequestMapping("/cs")
 * @Validated public class TestController {
 * @PostMapping("/test") public Results test(
 * @Pattern(regexp = "^\\d{19}$", message = "用户ID,应为19位数字") String id,
 * @NotBlank(message = "名字不能为空") String name
 * ) {}
 * }
 */
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(ConstraintViolationException.class)
public Results constraintViolationException(ConstraintViolationException e) {
    Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
    if (CollectionUtils.isEmpty(violations)) {
        log.error("constraintViolationException violations 为空", e);
        return Results.failed();
    }
    Map<String, String> map = violations.stream()
            .collect(Collectors.toMap(o -> {
                PathImpl x = (PathImpl) o.getPropertyPath();
                return x.getLeafNode().toString();
            }, ConstraintViolation::getMessage, (k1, k2) -> k1));
    return Results.failed(map.toString());
}

}

如果不加这个全局处理类,只会给前端返回这样的参数
在这里插入图片描述

加上全局配置类
在这里插入图片描述

3、实体类
java复制代码@ApiModel(“测试 validation 入参”)
@Data
public class TestDto {

@ApiModelProperty(value = "名字",required = true)
// 适用于 String 类型的数据上,加了@NotBlank 注解的参数不能为 Null 且 trim() 之后 size > 0,必须有实际字符
@NotBlank(message = "名字不能为空")
private String name;

@ApiModelProperty(value = "年龄",required = true)
@NotNull(message = "年龄不能为空")
// 适用于基本数据类型(Integer,Long,Double等等),当 @NotNull 注解被使用在 String 类型的数据上,则表示该数据不能为 Null(但是可以为 Empty)
private Integer age;

@ApiModelProperty(value = "爱好",required = true)
// 适用于 String、Collection集合、Map、数组等等,加了@NotEmpty 注解的参数不能为 Null 或者 长度为 0
@NotEmpty(message = "年龄不能为空")
private List<String> hobbies;

}

4、服务层(为了方便,我直接跟Controller写在一起了)
必须要加上 @Valid 或者 @Validated,后续我会讲解这两个有什么不同,目前来说,都可以用,但推荐用 @Validated
java复制代码@RestController
@Slf4j
@Api(“测试”)
public class TestController {

@PostMapping("/testValidation")
@ApiOperation("测试 validation")
// 必须要加上 @Valid 或者 @Validated
public Results testValidation(
    // 必须要加上 @Valid 或者 @Validated
    @Valid @RequestBody TestDto dto){
    try {
        log.info("test 入参 dto={}",dto);
        return Results.success();
    } catch (Exception e) {
        log.error("test 报错",e);
        return Results.failed();
    }
}

}

6、测试
在这里插入图片描述

四、javax.validation 包下其它常用的校验注解:
双等于中间是常用的(双等于符号在markdown中为高亮,不知道为啥掘金这里不太行(0.0),我是在一个markdown软件中创作,导入到掘金就不行了,大伙将就看(0^0))

在这里插入图片描述

在这里插入图片描述

注解含义@Null任何类型 必须为null==@NotBlank字符串、字符 字符类不能为null,且去掉空格之后长度大于@NotNull任何类型 不能为null@Length(min = 6, max = 8, message = “密码长度为6-8位。”)字符串的大小必须在指定的范围内@NotEmpty适用于 String、Collection集合、Map、数组等等,加了@NotEmpty 注解的参数不能为 Null 或者 长度为 0@AssertTrueBoolean、boolean 布尔属性必须是true@AssertFalseBoolean、boolean 布尔属性必须是false@Min(10)==必须是一个数字,其值必须大于等于指定的最小值(我这填的是10)(整型)@Max(10)必须是一个数字,其值必须小于等于指定的最大值(我这填的是10)(整型)@DecimalMin(“10”)必须是一个数字,其值必须大于等于指定的最小值(我这填的是10)(字符串,可以是小数)@DecimalMax(“10”)必须是一个数字,其值必须小于等于指定的最大值(我这填的是10)(字符串,可以是小数)@Size(max = 10,min = 1)集合 限定集合大小@Digits(integer = 3, fraction = 2, message = “请输入有效的数字”)private double number;@Digits 用于验证数字的整数位数和小数位数。该注解的 integer 和 fraction 属性分别用于指定整数位数和小数位数的限制。integer 属性用于指定数字的最大整数位数。它是一个整数值,表示数字允许的最大整数位数。例如,integer = 3 表示数字最多可以有三位整数部分。fraction 属性用于指定数字的最大小数位数。它是一个整数值,表示数字允许的最大小数位数。例如,fraction = 2 表示数字最多可以有两位小数部分。@Past时间、日期 必须是一个过去的时间或日期@Future时期、时间 必须是一个未来的时间或日期@Email字符串 必须是一个邮箱格式@Pattern(regexp = “[a-zA-Z]*”, message = “密码不合法”)字符串、字符 正则匹配字符串@Range(max = 150, min = 1, message = “年龄范围应该在1-150内。”)数字类型(原子和包装) 限定数字范围(长整型)@URL(protocol=,host=, port=,regexp=, flags=)被注释的字符串必须是一个有效的url@CreditCardNumber被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性@ScriptAssert(lang=, script=, alias=)要有Java Scripting API 即JSR 223 (“Scripting for the JavaTM Platform”)的实现@SafeHtml(whitelistType=, additionalTags=)classpath中要有jsoup包
五、@Validated 与 @Valid 比较
1、文字讲解

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring’s JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。
@Valid属于javax.validation包下,是jdk给提供的 是使用Hibernate validation的时候使用
@Validated是org.springframework.validation.annotation包下的,是spring提供的 是只用Spring Validator校验机制使用
说明:java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现
@Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

分组

@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。

注解地方

@Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)上 所以可以用@Valid实现嵌套验证
总结:
@Valid 和 @Validated 两者都可以对数据进行校验,待校验字段上打的规则注解(@NotNull, @NotEmpty等)都可以对 @Valid 和 @Validated 生效;
@Valid 进行校验的时候,需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不 return ,则并不会阻止程序的执行;
@Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示。
总体来说,@Validated 使用起来要比 @Valid 方便一些,它可以帮我们节省一定的代码,并且使得方法看上去更加的简洁。

2、代码讲解,groups属性

在开发中,新增、修改两个接口,一般关系就在于新增时ID可以为空,修改时ID不能为空,那我们如果要使用 validation 用于参数校验,创建两个实体类就非常的不划算,这时

①、创建一个update接口
java复制代码
import javax.validation.groups.Default;

public interface Update extends Default {
}

②、修改实体类
java复制代码@ApiModel(“测试 validation 入参”)
@Data
public class TestDto {
@ApiModelProperty(value = “ID”,required = true)
// 新增时ID为空,修改时ID不能为空
@NotNull(message = “ID不能为空”,groups = Update.class)
private Integer id;
@ApiModelProperty(value = “名字”,required = true)
@NotBlank(message = “名字不能为空”)
private String name;
}

③、服务层(为了方便,我直接跟Controller写在一起了)
java复制代码@RestController
@Slf4j
@Api(“测试”)
public class TestController {

@PostMapping("/testAdd")
@ApiOperation("测试 新增")
public Results testAdd(@Validated @RequestBody TestDto dto){
    try {
        log.info("testAdd 入参 dto={}",dto);
        return Results.success();
    } catch (Exception e) {
        log.error("testAdd 报错",e);
        return Results.failed();
    }
}

@PostMapping("/testUpdate")
@ApiOperation("测试 新增")
public Results testUpdate(@Validated(Update.class) @RequestBody TestDto dto){
    try {
        log.info("testUpdate 入参 dto={}",dto);
        return Results.success();
    } catch (Exception e) {
        log.error("testUpdate 报错",e);
        return Results.failed();
    }
}

}

⑤、测试
新增:

修改:

六、校验List
0、错误案例:
仅在外层包一个@Valid是校验不了List中实体类的参数的
java复制代码 @ResponseBody
@PostMapping(“/testList”)
public Results testList(@RequestBody @Valid List dto){
try {
log.info(“testList 入参 dto={}”,dto);
return Results.success();
} catch (Exception e) {
log.error(“testList 报错”,e);
return Results.failed();
}
}

方法其实有两种,我就只讲比较优雅且方便的方法
1、创建ValidList类(可以直接复制使用)
java复制代码/**

  • 可被校验的List
  • @param 元素类型
  • @author Deolin
    */
    @Data
    public class ValidList implements List {
    @Valid
    private List list = new ArrayList<>();
    @Override
    public int size() {
    return list.size();
    }
    @Override
    public boolean isEmpty() {
    return list.isEmpty();
    }
    @Override
    public boolean contains(Object o) {
    return list.contains(o);
    }
    @Override
    public Iterator iterator() {
    return list.iterator();
    }
    @Override
    public Object[] toArray() {
    return list.toArray();
    }
    @Override
    public T[] toArray(T[] a) {
    return list.toArray(a);
    }
    @Override
    public boolean add(E e) {
    return list.add(e);
    }
    @Override
    public boolean remove(Object o) {
    return list.remove(o);
    }
    @Override
    public boolean containsAll(Collection<?> c) { return list.containsAll(c); } @Override public boolean addAll(Collection<? extends E> c) { return list.addAll(c); } @Override public boolean addAll(int index, Collection<? extends E> c) { return list.addAll(index, c); } @Override public boolean removeAll(Collection<?> c) {
    return list.removeAll©;
    }
    @Override
    public boolean retainAll(Collection<?> c) {
    return list.retainAll©;
    }
    @Override
    public void clear() {
    list.clear();
    }
    @Override
    public E get(int index) {
    return list.get(index);
    }
    @Override
    public E set(int index, E element) {
    return list.set(index, element);
    }
    @Override
    public void add(int index, E element) {
    list.add(index, element);
    }
    @Override
    public E remove(int index) {
    return list.remove(index);
    }
    @Override
    public int indexOf(Object o) {
    return list.indexOf(o);
    }
    @Override
    public int lastIndexOf(Object o) {
    return list.lastIndexOf(o);
    }
    @Override
    public ListIterator listIterator() {
    return list.listIterator();
    }
    @Override
    public ListIterator listIterator(int index) {
    return list.listIterator(index);
    }
    @Override
    public List subList(int fromIndex, int toIndex) {
    return list.subList(fromIndex, toIndex);
    }
    }

2、测试
controller:
java复制代码 @ResponseBody
@PostMapping(“/testList”)
public Results testList(@RequestBody @ValidList List dto){
try {
log.info(“testList 入参 dto={}”,dto);
return Results.success();
} catch (Exception e) {
log.error(“testList 报错”,e);
return Results.failed();
}
}

经过我们写的全局异常处理类,得出的结果
在这里插入图片描述

七、自定义 枚举校验注解
1、注解
java复制代码/**

  • 枚举校验注解
    */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {EnumValueValidator.class})
    public @interface EnumValue {

    // 默认错误消息
    String message() default “必须为指定值”;

    String[] strValues() default {};

    int[] intValues() default {};

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

    // 负载
    Class<? extends Payload>[] payload() default {};

    // 指定多个时使用
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
    EnumValue[] value();
    }
    }

2、枚举校验注解处理类
java复制代码/**

  • 枚举校验注解处理类
    */
    public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

    private String[] strValues;
    private int[] intValues;

    @Override
    public void initialize(EnumValue constraintAnnotation) {
    strValues = constraintAnnotation.strValues();
    intValues = constraintAnnotation.intValues();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
    if (value instanceof String) {
    for (String s : strValues) {
    if (s.equals(value)) {
    return true;
    }
    }
    } else if (value instanceof Integer) {
    for (int s : intValues) {
    if (s == ((Integer) value).intValue()) {
    return true;
    }
    }
    }
    return false;
    }
    }

3、使用
java复制代码@Data
@Accessors(chain = true)
public class Test {
@EnumValue(strValues = {“1”, “2”, “3”}, message = “传入类型不正确”)
private String type;
@EnumValue(intValues = {1, 2, 3}, message = “传入类型不正确”)
private String status;
}

转载来源链接:https://juejin.cn/post/7382775817581756416

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值