Spring-Validation的学习与使用

Spring-Validation

在 Web 应用中,客户端提交数据之前都会进行数据的校验,比如用户注册时填写的邮箱地址是否符合规范、用户名长度的限制等等,不过这并不意味着服务端的代码可以免去数据验证的工作,用户也可能使用 HTTP 工具直接发送违法数据。为了保证数据的安全性,服务端的数据校验是必须的。

Spring-ValidationHibernate Validation 进行了二次封装,在 SpringMVC 模块中添加了自动校验机制,可以利用注解对 Java Bean的字段的值进行校验,并将校验信息封装进特定的类中

如果spring boot的版本大于2.3.x,那么我们需要手动导入依赖:

<dependency>
    <groupId>org.hibernate.validatorgroupId>
    <artifactId>hibernate-validatorartifactId>
    <version>6.2.0.Finalversion>
dependency>

如果版本小于2.3.x,那么 spring-boot-starter-web会自动导入这个依赖

我这里使用 2.2.5版本的spring boot,所以不需要手动导入:

<parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.2.5.RELEASEversion>
parent>


  <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

共同点

所有的具体限制类型的注解,都必须有@Validation或者 @Validate 注解存在

所有限制:

  • @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)被注释的元素必须符合指定的正则表达式
  • @Email被注释的元素必须是电子邮箱地址
  • @Length 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range 被注释的元素必须在合适的范围内

Get方法参数校验

Get 的请求的参数一般是使用 RequestParamPathVariable来传递,此时,对Controller类使用 @Validated注解 ,并在参数上声明 约束注解,如果校验失败,抛出ConstraintViolationException异常

这里我发现一点,就是我们使用@PathVariable时,必须要在请求路径上写明参数,比如@PathVariable("name"),那么请求参数就必须是 /xxx/{name}

@RequstParam的参数检验:

@RestController
@Validated
public class ValidController {
    @RequestMapping("/first")
    public String first(@Length(min = 4,max = 5)@RequestParam("name") String name, @RequestParam("age") String age){
        System.out.println("名字是 "+name);
        System.out.println("年龄是 "+age);
        return "first";
    }
}

当我们访问 http://127.0.0.1:8080/first?name=啊&age=10

报错如下:

在这里插入图片描述

@PathVariable的参数检验:

@RestController
@Validated
public class ValidController {
    @GetMapping("/{age}")
    public String firstandPathV(@Max(100) @Min(0) @PathVariable("age")int age){
        System.out.println("年龄是"+age);
        return String.valueOf(age);
    }
}

Spring-Validation的学习与使用_第1张图片

Spring-Validation的学习与使用_第2张图片

可以发现,参数校验失败的message都是直接指明的,所以我们可以使用message在前端使用

POST和PUT的请求校验

putpost请求一般都是使用 @RequestBody来传递参数,然后后端是使用 DTO 对象来接收,所以我们只需要给DTO 对象加上 @Validated 注解就能实现自动参数校验。这里抛出的异常是

MethodArgumentNotValidException ,spring在参数校验失败时会把这个异常自动转化为400(Bad Request),而前面使用Get方法参数校验失败时返回的则是 500

DTO表示数据传输对象(Data Transfer Object) ,用于服务器和客户端之间交互传输使用的,在spring web项目中可以表示用于接收请求参数的 Bean对象

编写DTO对象:

public class Student {

    @Length(min = 4)
    private String name;

    @NotNull
    private int age;
}

如图,name的校验是最小长度是4, age的校验是不为空

@RestController
@Validated
public class ValidController {
    @PostMapping("/second")
    public String second(@RequestBody @Validated Student student){
        System.out.println("年龄是"+student.getAge());
        return "second";
    }
}

当我们校验name失败时:

Spring-Validation的学习与使用_第3张图片

返回的是400,但是JAVA程序内部是不会报错的

但是这里我发现age的非空校验无效

Spring-Validation的学习与使用_第4张图片

如图,请求的参数 age的值是null ,但是访问之后:

在这里插入图片描述

校验失败,年龄的值是默认值0。

但是当我在String name上加上 @NotNull注解时,使用上面的json串会报错。

所以我猜 int age在赋值时是无法赋值 null的,所以age的值默认还是0,这就能通过 @NotNull 的参数校验,而String name 赋值时能被赋值 null,所以无法通过 @NotNull的参数校验。所以以后尽量不要使用无法被赋值为null的类型对象使用 @NotNull进行参数校验

也可以将注解写在对象前面,在类上就不需要写了:

@RestController
public class VController_1 {
    @PostMapping("/third")
    public String third(@RequestBody @Validated Student student){
        return "third";
    }
}

统一异常处理

前面已经说过,用GET方法参数校验是直接抛出异常,POST和PUT 参数校验则是返回400。

但是一般使用统一异常处理来返回更友好的提示,使状态码返回200,而由业务码去区分系统的异常情况

创建异常处理类:

@RestControllerAdvice
public class HandleExce {
    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public String handleExce(MethodArgumentNotValidException e){
        BindingResult result = e.getBindingResult();
        StringBuilder sb=new StringBuilder();
        for(FieldError fieldError:result.getFieldErrors()){     sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(",");
        }
        String msg=sb.toString();
        return msg;
    }
}

当我们访问验证错误时(注意MethodArgumentNotValidException是使用RequestBody校验失败时的报错):

Spring-Validation的学习与使用_第5张图片

主要是遍历异常对象 e 并把所有内容取出的for循环

处理ConstraintViolationException异常:

 @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public String handleExce2(ConstraintViolationException e){
        String message = e.getMessage();
        return message;
    }

访问错误后返回的是:

Spring-Validation的学习与使用_第6张图片

分组校验:

可能存在多个方法需要使用同一个DTO类来接收参数,而不同的方法的校验规则可能是不一样的,这时,简单地在DTO类的字段上加约束注解无法解决这个问题。因此, 分组校验就出来专门解决这个问题。

在DTO上配置分组:

public class Student {
    public interface  big{}
    public interface  small{}

    @Length(min = 4)
    @NotNull
    private String name;

    @Max(value = 100 ,groups = big.class)
    @Max(value = 20 ,groups = small.class)
    private int age;
}

我们使用的类内部的接口来进行分组,那么我们在使用的时候也容易选择具体的组

 @PostMapping("/test_group_big")
    public String test_group(@RequestBody @Validated(Student.big.class) Student student){
        return "test_group_big";
    }
    @PostMapping("/test_group_small")
    public String test_group_s(@RequestBody @Validated(Student.small.class)Student student){
        return "test_group_small";
    }

如上,一个使用的是 max=100,一个是max=20,那么我们测试是否符合:

Spring-Validation的学习与使用_第7张图片

Spring-Validation的学习与使用_第8张图片

如图,确实实现了分组的校验配置

注意:

  • 如果使用了分组校验,那么只有符合分组条件的校验才会生效,省略了 group的校验全部不生效

嵌套校验

我们前面所作的DTO内的属性都是基本数据类型String,但是当属性有一个是对象的时候,然后这个对象也有校验规则,那么就需要嵌套校验

DTO:

public class Student {
    public interface  big{}
    public interface  small{}

    @Length(min = 4)
    @NotNull
    private String name;

    @Max(value = 100 ,groups = big.class)
    @Max(value = 20 ,groups = small.class)
    private int age;

    @Valid
    private Classes classes;

    //一系列getter和setter,这里省略
}
  • 这里必须要使用 @Valid指定我们的嵌套对象

我们的嵌套类 Classes:

public class Classes {

    @NotNull
    String name;

    @Max(40)
    @Min(1)
    int counts;

   //一系列setter和getter
}

Controller:

 @PostMapping("/test_nest")
    public String test_nest(@RequestBody @Validated Student student){
        System.out.println(student.getClasses().getName());
        System.out.println(student.getClasses().getCounts());
        return "test_nest";
    }

Spring-Validation的学习与使用_第9张图片

如图,校验成功

嵌套的集合校验

如果嵌套的不是单个对象,而是对象的集合,那么使用方法也一样:

public class Student {
    @Valid
    private List<Classes> classes;

}

现在我们的Student类嵌套了List集合的校验,但是我们并不需要改动,spring Validation会自动给所有的Classes类进行校验,测试结果如下:

Spring-Validation的学习与使用_第10张图片

集合校验

刚刚是嵌套在 Student类里面的Classes对象集合,Spring Validation会自动对集合内所有对象进行校验,但是如果请求体传的是json数组作为Student集合,我们直接使用list或set来接收,那么校验就不会生效

 @PostMapping("/test_before")
    public String test_list_before(@RequestBody @Validated List<Student> stulist){
        for (Student student : stulist) {
            System.out.println(student.getName());
            System.out.println(student.getAge());
        }
        return "test_nest";
    }

我们这里使用的是List直接接收数组

Spring-Validation的学习与使用_第11张图片

我们的Student的name原本设置的是 @Length(min = 4),但是可以发现,这里校验并没有生效

我们这时需要自定义一个list :(只需要看listtostring即可,其他都是list的接口方法)

list上使用 @Valid 即可

public class Stulist<E>  implements List<E> {
    
    @Valid
    public List<E> list=new ArrayList<>();
    
    @Override
    public String toString() {
        return list.toString();
    }
    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 new Object[0];
    }

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

    @Override
    public boolean removeAll(Collection<?> c) {
        return false;
    }

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

    @Override
    public void clear() {

    }

    @Override
    public E get(int index) {
        return null;
    }

    @Override
    public E set(int index, E element) {
        return null;
    }

    @Override
    public void add(int index, E element) {

    }

    @Override
    public E remove(int index) {
        return null;
    }

    @Override
    public int indexOf(Object o) {
        return 0;
    }

    @Override
    public int lastIndexOf(Object o) {
        return 0;
    }

    @Override
    public ListIterator<E> listIterator() {
        return null;
    }

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

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

然后Controller:

  @PostMapping("/test_list")
    public String test_list(@RequestBody @Validated Stulist<Student> stulist){
        for (Student student : stulist) {
            System.out.println(student.getName());
            System.out.println(student.getAge());
        }
        return "test_nest";
}

Spring-Validation的学习与使用_第12张图片

如图,校验有效

自定义校验注解

框架提供的都是一些简单校验,我们可以自定义校验来满足我们的需求

我们创建 @EncryptID 这个注解

@Target({ElementType.METHOD, ElementType.FIELD,ElementType.ANNOTATION_TYPE,ElementType.CONSTRUCTOR,ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ENConstraintValidator.class)
public @interface EncryptID {

    //默认错误消息
    String message() default "加密格式错误";

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

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

还需要创建约束校验类:

public class ENConstraintValidator implements ConstraintValidator<EncryptID,String> {
    //由数字或者a-f的字母组成,长度在32-256之间
    private static final Pattern PATTERN= Pattern.compile("^[a-f\\d]{32,256}$");
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if(s!=null){
            Matcher matcher=PATTERN.matcher(s);
            return matcher.find();
        }
        return true;
    }
}

这样,我们就可以使用 @EncryptID这个注解进行校验了:

这里用在Classes类的name属性

public class Classes {

    @NotNull
    @EncryptID
    String name;

Spring-Validation的学习与使用_第13张图片

校验失败时,返回的是注解的默认 message

快速失败

spring validation默认会检验完所有的字段,然后才抛出异常,可以通过配置,开启 Fail Fast模式,一旦校验失败就立即返回

import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import org.hibernate.validator.HibernateValidator;

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

你可能感兴趣的:(Java,spring,java,restful)