springboot进行参数校验


文章目录

  • 前言
  • 普通的逻辑代码校验
  • PathVariable 路径变量校验
  • JSR规范是什么?
  • springboot-JSR303参数校验
  • 引入参数校验依赖
  • 官网提供的校验注解
  • 方法上对某参数校验
  • 全局异常拦截错误信息
  • javaBean 参数校验
  • 全局异常捕获升级
  • 参数分组校验隔离
  • 为什么校验要分组隔离?
  • 如何选择触发不同的校验分组?
  • List< Bean>类型参数校验
  • 参数校验-自定义注解
  • 定义校验注解
  • 定义校验规则
  • 实践测试
  • 项目源码


前言

实际项目中,通常会对用户的请求参数进行校验…而参数校验呢,又分为前端校验,后端校验…

本文呢,就来讲一讲咱们后端常用的参数校验方式

普通的逻辑代码校验

最传统的后端参数校验,则是接受到参数后,根据一定规则去比对,判断是否符合操作参数要求…

代码示例如下:

public AjaxResult addUser(User user) {
        if (user.getAge()<18) {
            throw new CommonException("未成年人,不允许添加");
        }
        if (user.getAge()>65) {
            throw new CommonException("超过法定退休年龄");
        }
        //TODO 这里假设不符合规范...
        if (user.getAccount().equals("asdasd")) {
            throw new CommonException("账户不符合规范");
        }
        return AjaxResult.success(true);
    }

这样的代码,按照逻辑处理来说,是没有问题的…

但按照代码简介性处理方式等,则显得代码非常冗余,如同胶水代码…最终要得是,如果其他方法也需要判断user信息的合理的话,又需要在写(copy)一份…

这种参数校验方式呢,代码丑陋冗余,写法low…不是特别推荐…

PathVariable 路径变量校验

通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中,其也可以对参数类型进行校验…如果参数类型不匹配,则会抛出404异常…

代码示例如下:

@RequestMapping("/add/user/{name}/{age}")
    public AjaxResult demo(@PathVariable("age") Integer age, @PathVariable("name") String name) {
        //TODO 逻辑处理...
        return AjaxResult.success(age + name);
    }

这种方式呢,仅仅只能校验传入参数是否符合方法规范(参数数量、参数类型),但如果不符合规范,其异常信息404又不太好处理,容易与实际意义上的url不存在混淆…进而忽视了参数校验的意义…

所以这种方式呢,我们仅仅还是将其作为一个restful风格参数传递方式较好…作为参数校验方面入手,不太符合其原本PathVariable的初衷…也不是特别推荐…

接下来…进入重头戏…

参数校验,既然有需求,那么肯定就有实现…

JSR规范是什么?

JSR是Java Specification Requests的缩写,意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求

那么我们项目中如何使用JSR呢?

使用JSR303即可…JSR-303JAVA EE 6 中的一项子规范,叫做 Bean Validation ,即对象校验…

我们接下来就演示在spring 项目中 使用jsr303规范 实现对参数的校验…

springboot-JSR303参数校验

引入参数校验依赖
<!--jsr3参数校验器-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

接下来,就可以快乐的使用啦!

其提供了许多的参数校验注解,我们只需要在需要校验的参数地方打上注解即可使用,当入参不符合注解规范,则会抛出异常…

官网提供的校验注解

例如下方注解…

约束注解

作用说明

@Length

被注释的字符串的大小必须在指定的范围内

@NotBlank

被注释的字符串的必须非空

@NotEmpty

被注释的字符串、集合、Map、数组必须非空

@Email

被注释的元素必须是电子邮箱地址

@AssertFalse

被注释的元素必须为 false

@AssertTrue

被注释的元素必须为 true

@DecimalMax(value)

被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value)

被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Digits (integer, fraction)

被注释的元素必须是一个数字,其值必须在可接受的范围内

@Null

被注释的元素必须为 null

@NotNull

被注释的元素必须不为 null

@Min(value)

被注释的元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value)

被注释的元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max, min)

被注释的元素的大小必须在指定的范围内

@Past

被注释的元素必须是一个过去的日期

@Future

被注释的元素必须是一个将来的日期

@Pattern(value)

被注释的元素必须符合指定的正则表达式



可以查阅hibernate-validatorvalidator 相关资料…

方法上对某参数校验

需要注意的是,在方法参数上使用校验注解,必须在其类上打上@Validated注解

@RequestMapping("/add/demo")
    public AjaxResult demo(@Max(value =65,message = "最大年龄为65")
                            @Min(value = 18,message = "最小年龄为18")
                                       Integer age,
                           @NotBlank(message = "年龄不能为空")
                           String name) {
        //TODO 逻辑处理...
        return AjaxResult.success(age + name);
    }

结果如下…

spring gateway 防抖_spring

spring gateway 防抖_User_02

这个错误信息,对用户来说,体验性是不好的,所以呢,我们通常呀结合RestControllerAdvice +ExceptionHandler 对异常进行拦截…统一以json形式将错误进行返回…

全局异常拦截错误信息
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolationException;

/**
 * @author leilei
 * @version 1.0
 * @date 2020/10/22 21:21
 * @desc: 增强类  可以自己在 @ExceptionHandler(xxx.class)  定义异常类型  这样就会以json形式展示错误信息
 */
@Slf4j
@RestControllerAdvice
public class ExceptionsHandler {

  /** 参数不符合规范异常 */
  @ExceptionHandler({ConstraintViolationException.class})
  public AjaxResult ConstraintViolationException(ConstraintViolationException ex) {
    log.error("参数不符合规范异常"+ex.getMessage());
    return AjaxResult.error(ex.getMessage());
  }
}

spring gateway 防抖_spring_03

javaBean 参数校验

实际开发中,可能涉及到传入参数为一个java bean 对象…可能是以JSON形式接受,可能直接是以对象参数接受…但,我们需要对参数对象进行校验…

那么,这个时候呢,我们就可以像Swagger一般,在需要校验的实体类的字段上,添加校验注解,然后,在入参方法中加上@Validated注解即可…

@Data
public class User {
  @NotBlank(message = "账户不能为空")
  @Length(min = 11,max = 11,message = "账户长度限定为十一位")
  private String account;
  @NotBlank(message = "用户密码不能为空")
  @Length(min = 6,max = 12,message = "密码长度不正确5<密码<12")
  private String passWord;
  @Max(value = 65, message = "age应<=65")
  @Min(value = 18, message = "age应=>18")
  private Integer age;
  @NotBlank(message = "邮箱不能为空")
  @Email(message = "邮件格式不对")
  private String email;
  private Boolean sex;
}

接口方法

注意,校验对象参数必须添加 @Validated@Valid注解

二者区别:

@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上

@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

@RequestMapping("/addUser")
 public AjaxResult add(@Validated @RequestBody User user) {
   return userService.addUser(user);
 }

那么这个时候呢,网上一大堆,都需要在接口方法中加上BindingResult 才能捕获到异常…这种做法…

说实话,可以,是可以…但方式有点憨憨…

为什么不全局异常捕获呢????好好地接口要啥参数填啥参数就行了,搞个额外参数干啥呢???

全局异常捕获升级
@ExceptionHandler(value = {MethodArgumentNotValidException.class , BindException.class})
  public AjaxResult runtimeExceptionHandler(Exception ex) {
    log.error("参数校验异常:{}", ex.getMessage());
    BindingResult bindingResult = null;
    if (ex instanceof MethodArgumentNotValidException) {
      bindingResult = ((MethodArgumentNotValidException)ex).getBindingResult();
    } else if (ex instanceof BindException) {
      bindingResult = ((BindException)ex).getBindingResult();
    }
    StringBuilder errorMessage = new StringBuilder();
    if (bindingResult != null) {
      bindingResult.getFieldErrors().parallelStream()
              .forEach(e -> errorMessage.append(e.getDefaultMessage()).append(" !"));
    }
    return AjaxResult.error(errorMessage.toString(), 400);
  }

spring gateway 防抖_User_04

参数分组校验隔离

为什么校验要分组隔离?

开发中,在新增或者修改用户传递的对象信息可能校验的规则不同

例如:新增时要求ID为空,修改时要求ID 不等于空…

那么这个时候,就需要对校验注解进行分组隔离了…即新增走新增校验规则,修改时走修改校验规则…

spring gateway 防抖_spring_05

从我们使用的注解中可以看到,其还能设置一个group属性,默认为{},这个就是参数校验分组,我们之前不设置group属性,那么在使用到这个javabean的时候,其作用在参数上的所有校验注解都会生效,

所以呢,咱们可以对我们设置的参数校验注解进行分组设置…

例如这样…

我们对User中的ID 参数校验注解进行分组,当新增时,ID必须为null,当修改时ID 必须不能为Null…

@Data
public class User {
  @Null(message = "ID必须为null",groups = User.Default.class)
  @NotNull(message = "ID不能为null",groups = User.Update.class)
  private Integer id;
  @NotBlank(message = "账户不能为空",groups = User.Default.class)
  @Length(min = 11,max = 11,message = "账户长度限定为十一位",groups = User.Default.class)
  private String account;

  @NotBlank(message = "用户密码不能为空",groups = User.Default.class)
  @Length(min = 6,max = 12,message = "密码长度不正确5<密码<12",groups = User.Default.class)
  private String passWord;

  @Max(value = 65, message = "age应<=65",groups = User.Default.class)
  @Min(value = 18, message = "age应=>18",groups = User.Default.class)
  private Integer age;

  @NotBlank(message = "邮箱不能为空",groups = User.Default.class)
  @Email(message = "邮件格式不对",groups = User.Default.class)
  private String email;
  private Boolean sex;
  public interface Default {
  }

  public interface Update {
  }

}

如何便是完成了参数注解分组校验隔离了…

需要注意的是:环境名设置必须在当前实体类采用内部接口形式否则会报错

spring gateway 防抖_User_06

如何选择触发不同的校验分组?

依然使用Validated注解,其注解可以指定Value 属性…value指明注解环境即可…

例如我这里的…

@RequestMapping("/addUser")
    public AjaxResult add(@Validated(value = User.Default.class) @RequestBody User user) {
        return userService.addUser(user);
    }

如此,便指明了参数校验环境分组为Default…那么,我添加的ID字段 校验的@Null则会生效… 以及之前的字段校验注解也会生效

新增测试 则会使用User.Default.class分组的校验分组

spring gateway 防抖_spring_07

修改测试 则会使用User.Update.class 校验分组,目前来说,Update分组,只有ID不为空校验…

@RequestMapping("/editUser")
    public AjaxResult update(@Validated(value = User.Update.class) @RequestBody User user) {
        return userService.update(user);
    }

spring gateway 防抖_spring gateway 防抖_08

List< Bean>类型参数校验

即有时候,接口穿的参数可是一个Json集合…我们有需要对JSON集合中的实体类的属性做校验…这个情况,如下进行呢?

在原本User参数校验的情况下,咱们来辨析controller接口试一试

@RequestMapping("/addUser/batch")
    public AjaxResult addBatch(@RequestBody @Validated(value =User.Default.class) List<User> users) {
        //TODO 业务路基操作
        return AjaxResult.success();
    }

spring gateway 防抖_spring gateway 防抖_09

居然直接新增成功了!!!这说明,参数校验根本没有生效啊!!!

解决办法:

自定义一个POJO实体类…实现LIst接口…我们将传递过来的集合实体泛型作为我们的POJO泛型…

例如下边这样:

@Data
public class ValidResultList<E> implements List<E> {

    @Valid
    private List<E> 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<E> iterator() {
        return list.iterator();
    }

    @Override
    public Object[] toArray() {
        return list.toArray();
    }

    @Override
    public <T> 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(c);
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return list.retainAll(c);
    }

    @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<E> listIterator() {
        return list.listIterator();
    }

    @Override
    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index);
    }

    @Override
    public List<E> subList(int fromIndex, int toIndex) {
        return list.subList(fromIndex,toIndex);
    }
}

controller接口

将我们编写的POJO 类作为参数

public AjaxResult addBatch(@RequestBody @Validated(value =User.Default.class) ValidResultList<User> users) {
        //TODO 业务路基操作
        return AjaxResult.success();
    }

spring gateway 防抖_User_10

如此一来,便可成功捕获道字段不匹配的信息了!!…但是呢,也存在一个问题,,,其提示参数没有隔离开来…

参数校验-自定义注解

可能内置的校验注解无法符合我们的业务需求,这个时候呢,我们便可以自定义注解…

自定义校验注解呢 分为两个步骤:

#1.定义注解
#2.定义校验规则类

比如我们查看源码:@Null 注解,其注解类中包含了这些信息…

spring gateway 防抖_spring_11

那么,我们照猫画虎,一样来根据自己的业务定义一个校验注解…

比如说,校验字段存在于我们的字典表中…

定义校验注解

首先,咱们需要定义一个注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
//指定作用字段上以及方法参数上
@Target({ElementType.PARAMETER,ElementType.FIELD})
//指定校验规则类
@Constraint(validatedBy = HasDictionaryCheck.class)
public @interface HasDictionary {
	//默认提示信息
    String message() default "字典表中该值不存在";
	//默认分组
    Class<?>[] groups() default {};

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

}

注解有了,那的有校验规则吧!不然咋知道其符不符合规范呢?所以啊,下一步是定义该注解的校验规则类

定义校验规则

然后,我们需要定义注解对应的校验规则类
重点:

校验类需实现ConstraintValidator接口

两个泛型 前一个为要校验的注解,后一个为校验值类型

public class HasDictionaryCheck implements ConstraintValidator<HasDictionary,Object> {

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        //TODO 业务逻辑
        return value.equals("leilei");
    }
}

由于本Demo 暂写死校验逻辑 期望打上注解的值 传参为leilei…否则校验失败…

如此,自定义校验注解便完成了…咱们来测试一波…

咱们需要在原来的实体类中打上校验注解

spring gateway 防抖_java_12

由于调用的接口指定了校验分组为User.Default.class分组,所以咱们新的注解也要指定一下分组,其才可生效…

spring gateway 防抖_java_13

测试

spring gateway 防抖_spring boot_14

如果,咱们想自己手动约束其传入参数…例如这样…根据自己填入的规则限定传入参数

spring gateway 防抖_spring_15

那么,咱们依然可以照猫画虎,定义一个校验注解…

比如,要求前端传过来的值必须包含在我们后端设置的值之中

注解:

/**
 * @author lei
 * @version 1.0
 * @date 2020/10/24 17:04
 * @desc 自定义值 参数校验  例如参数必须是(张三,李四,王五)中所包含的值
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Constraint(validatedBy = HasContainCheck.class)
public @interface HasContain {
    //自己设置的校验值
    String values();
    //默认提示信息
    String message() default " ";
    //默认分组
    Class<?>[] groups() default {};

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

校验规则

public class HasContainCheck implements ConstraintValidator<HasContain, Object> {

    private String values;

    /**
     * 此方法是在加载该校验注解最先执行的方法,可在内做逻辑操作处理......
     *
     * @param hasContain
     */
    @Override
    public void initialize(HasContain hasContain) {
        this.values = hasContain.values();
    }

    /**
     * 参数校验规则
     * @param value
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        String[] parameters = values.split(",");
        long count = Arrays.stream(parameters)
                .filter(value::equals)
                .count();
        return count > 0;
    }
}
实践测试

spring gateway 防抖_java_16

spring gateway 防抖_spring gateway 防抖_17

自定义参数校验注意事项:

spring gateway 防抖_spring_18

我们自定义校验类在实现了ConstraintValidator接口后,校验类实际交由了Spring来管理,我们是可以在类中直接注入spring容器中其他bean的,我们在做校验业务逻辑的时候,可以充分利用这一点,来壮大我们的自定义校验注解

项目源码

springboot-jsr303