校验注解:@Valid 和 @Validated区别与用法(附详细案例)

不管现实多么惨不忍睹,都要持之以恒地相信,这只是黎明前短暂的黑暗而已。不要惶恐眼前的难关迈不过去,不要担心此刻的付出没有回报,别再花时间等待天降好运。真诚做人,努力做事!你想要的,岁月都会给你。校验注解:@Valid 和 @Validated区别与用法(附详细案例),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

一、案例对象

        本文章会以案例为主,讲解@Valid@Validated这两个注解的区别与用法。

1.首先,创建一个学生对象,如下:

import lombok.Data;

/**
 * 学生对象
 */
@Data
public class Student {

    /*** 姓名*/
    private String name;

    /*** 年龄*/
    private Integer age;

    /*** 性别*/
    private Integer sex;

    /*** 手机号*/
    private String phone;

}

 这里我使用Lombok注解,省去了构造方法、get/set方法。

2.PersonController里有一个,新增学生的方法 addStudent():

import com.st.microservice.usercenter.infrastructure.entity.Student;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 人员controller
 */
@RestController
@RequestMapping("/person")
public class PersonController {

    @PostMapping("/addStudent")
    public String addStudent(Student student) {
        // TODO 学生信息入库
        return "新增学生成功";
    }

}

现有个需求:学生的姓名不能为空,且长度不能超过10个字符;如果,在不用注解的情况下,我们大致会这样写:

/**
 * 人员controller
 */
@RestController
@RequestMapping("/person")
public class PersonController {

    @PostMapping("/addStudent")
    public String addStudent(Student student) {
        String name = student.getName();
        if (name == null || name.trim().length() == 0) {
            return "学生姓名不能为空";
        }
        if (name.trim().length() > 10) {
            return "学生姓名不得超过10个字符";
        }
        // TODO 学生信息入库
        return "新增学生成功";
    }

}

用Apifox测试一下:

(1)正常情况

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

(2)姓名为空情况

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

(3)姓名超长情况 

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

可以看到,结果是没什么问题的。 那么,这时候又来了新的需求:学生的年龄是必填项,且年龄范围在1~100岁之间。我们只需要再加一个判断语句就好:

/**
 * 人员controller
 */
@RestController
@RequestMapping("/web/omp/person")
public class PersonController {

    @PostMapping("/addStudent")
    public String addStudent(@RequestBody Student student) {
        String name = student.getName();
        if (name == null || name.trim().length() == 0) {
            return "学生姓名不能为空";
        }
        if (name.trim().length() > 10) {
            return "学生姓名不得超过10个字符";
        }

        Integer age = student.getAge();
        if (age == null) {
            return "学生年龄不能为空";
        }
        if (age < 1 || age > 100) {
            return "学生年龄不能小于1岁或大于100岁";
        }
        // TODO 学生信息入库
        return "新增学生成功";
    }

}

这也是没问题的,但是有一个问题。我们只校验了2个字段,就写了10多行的校验代码,要是校验更多的字段,岂不是要写更多的代码?通常来说,当一个方法中的无效业务代码量过多时,往往代码设计存在问题。

那么如何解决呢?首先大家应该会想到将对应的验证过程抽成一个验证方法,如下:

// 学生对象 属性校验
private String verify(Student student) {
        String name = student.getName();
        if (name == null || name.trim().length() == 0) {
            return "学生姓名不能为空";
        }
        if (name.trim().length() > 10) {
            return "学生姓名不得超过10个字符";
        }

        Integer age = student.getAge();
        if (age == null) {
            return "学生年龄不能为空";
        }
        if (age < 1 || age > 100) {
            return "学生年龄不能小于1岁或大于100岁";
        }
        return null;
    }
    @PostMapping("/addStudent")
    public String addStudent(@RequestBody Student student) {
        String result = verify(student);
        if (result != null) {
            return result;
        }
        // TODO 学生信息入库
        return "新增学生成功";
    }

但这种方式只是抽了一个方法,虽然业务方法看起来清爽了很多,但实际的代码量并没有下降,有种换汤不换药的感觉。

二、@Valid注解

此时,我们可以使用 Spring 中的 @valid 注解,具体如下:

1、首先,pom文件中要加入@valid依赖:(如果你是 springboot 项目,可以不用引入了,它已经存在于最核心的 web 开发包里面

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

如果你不是 springboot 项目,那么引入下面依赖即可:

<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>
 
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.1.Final</version>
</dependency>

2、可以优化代码了,在 Student 类的属性上加如下注解:

/**
 * 学生对象
 */
@Data
public class Student {

    /*** 姓名*/
    @NotBlank(message = "请输入姓名")
    @Length(message = "名称不能超过个 {max} 字符", max = 10)
    private String name;

    /*** 年龄*/
    @NotNull(message = "请输入年龄")
    @Range(message = "年龄范围为 {min} 到 {max} 岁之间", min = 1, max = 100)
    private Integer age;

    /*** 性别*/
    @NotNull(message = "请选择性别")
    private Integer sex;

    /*** 手机号*/
    private String phone;

}

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

既然验证,那么就肯定会有验证结果,所以我们需要用一个东西来存放验证结果,做法也很简单,在参数直接添加一个BindingResult,具体如下: 

import org.springframework.validation.BindingResult;
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;

/**
 * 人员controller
 */
@RestController
@RequestMapping("/web/omp/person")
public class PersonController {

    @PostMapping("/addStudent")
    public String addStudent(@RequestBody @Valid Student student, BindingResult bindingResult) {
        // 所有字段是否验证通过,true-数据有误,false-数据无误
        if (bindingResult.hasErrors()){
            // 有误,则返回前端第一条错误信息
            return bindingResult.getAllErrors().get(0).getDefaultMessage();
        }
        // TODO 学生信息入库
        return "新增学生成功";
    }

}

因为测试结果和之前的一样,这里就不展示了。 这样代码不但简洁了很多,想要的校验也有了,  真的是yyds。

一些项目中常用的字段属性校验注解,可以参考我的这片文章:常用注解

三、@Validated注解

简单来说,@Validated注解是@Valid注解的一个升级版。

我们可以看到,在使用 @Valid 进行验证时,需要用一个对象去接收校验结果,最后根据校验结果判断,从而提示用户。

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

当我们把校验逻辑注释掉后,再次执行上面的请求后。校验注解:@Valid 和 @Validated区别与用法(附详细案例)

 可以看到我们的程序继续往后执行了。

现在,我们去掉方法参数上的 @Valid 注解和其配对的 BindingResult 对象,

然后再校验的对象前面添加上 @Validated 注解。

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

这个时候,我们再次请求,可以看到,我们的程序报异常了。

那么,从这里我们可以得知,当我们的数据存在校验不通过的时候,程序就会抛出

org.springframework.validation.BindException 的异常。

在实际开发的过程中,我们肯定不能讲异常直接展示给用户,而是给能看懂的提示。

于是,我们不妨可以通过捕获异常的方式,将该异常进行捕获。

首先我们创建一个校验异常捕获类 ValidExceptionHandler ,然后加上 @RestControllerAdvice 注解,该注解表示他会去抓所有 @Controller 标记类的异常,并在异常处理后返回以 JSON 或字符串的格式响应前端。

在异常捕捉到后,我们同上面的 @valid 校验一样,只返回第一个错误提示。

直接上代码:

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.List;

/**
 * 捕捉异常
 */
@Slf4j
@ControllerAdvice
public class ValidatedExceptionHandler {

    /**
     * 处理@Validated参数校验失败异常
     */
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> exceptionHandler(MethodArgumentNotValidException exception) {
        BindingResult result = exception.getBindingResult();
        StringBuilder stringBuilder = new StringBuilder();
        if (result.hasErrors()) {
            List<ObjectError> errors = result.getAllErrors();
            errors.forEach(p -> {
                FieldError fieldError = (FieldError) p;
                log.warn("Bad Request Parameters: dto entity [{}],field [{}],message [{}]", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
                stringBuilder.append(fieldError.getDefaultMessage());
            });
        }
        return Result.error(stringBuilder.toString());
    }

}

重启项目,再次请求,发现不报错了,校验成功。

四、嵌套参数校验和分组参数校验

  • 嵌套参数验证(验证实体中的其他需要被验证的对象集合或其他对象)

(1)实体类

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

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

/**
 * 学生对象
 */
@Data
public class Student {

    /*** 姓名*/
    @NotBlank(message = "请输入姓名!")
    @Length(message = "名称不能超过个 {max} 字符!", max = 10)
    @Valid
    private String name;

    /*** 年龄*/
    @NotNull(message = "请输入年龄!")
    @Range(message = "年龄范围为 {min} 到 {max} 岁之间!", min = 1, max = 100)
    @Valid
    private Integer age;

    /*** 性别*/
    @NotNull(message = "请选择性别!")
    private Integer sex;

    /*** 手机号*/
    private String phone;

}

(2)控制类

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;

/**
 * 人员controller
 */
@RestController
@RequestMapping("/web/omp/person")
public class PersonController {

    @PostMapping("/addStudent")
    public Result<String> addStudent(@Validated @RequestBody Student student) {
        // TODO 学生信息入库
        return Result.success("新增学生成功");
    }

}

a.校验通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

b.校验不通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

  • 分组参数验证(将不同的校验规则分给不同的组,在使用时,指定不同的校验规则)

a.创建两个接口类

/**
 * 分组校验1
 */
public interface Group1 {
}
/**
 * 分组校验2
 */
public interface Group2 {
}

b.实体类

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

import javax.validation.constraints.*;

/**
 * 学生对象2
 */
@Data
public class StudentDto {

    /*** 姓名*/
    @NotBlank(message = "请输入姓名!", groups = {Group1.class})
    @Length(max = 10, message = "名称不能超过个10字符!")
    private String name;

    /*** 年龄*/
    @NotNull(message = "请输入年龄!")
    @Min(value = 1, message = "年龄不得小于1岁!", groups = {Group1.class})
    @Max(value = 120, message = "年龄不得大于120岁!", groups = {Group2.class})
    private Integer age;

    /*** 性别*/
    @NotNull(message = "请选择性别")
    private Integer sex;

    /*** 手机号*/
    @Pattern(regexp = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$", message = "手机号码有误!", groups = {Group2.class})
    private String phone;

    /*** 邮箱*/
    @Email(message = "邮箱有误!", groups = {Group2.class})
    private String email;

}

c.控制类

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;

/**
 * 人员controller
 */
@RestController
@RequestMapping("/web/omp/person")
public class PersonController {

    // 未分组校验
    @PostMapping("/add1")
    public Result<String> add1(@Validated @RequestBody StudentDto dto) {
        // TODO 学生信息入库
        return Result.success(dto);
    }

    // 按Group1规则校验
    @PostMapping("/add2")
    public Result<String> add2(@Validated(Group1.class) @RequestBody StudentDto dto) {
        // TODO 学生信息入库
        return Result.success(dto);
    }

    // 按Group2规则校验
    @PostMapping("/add3")
    public Result<String> add3(@Validated(Group2.class) @RequestBody StudentDto dto) {
        // TODO 学生信息入库
        return Result.success(dto);
    }

}

 d.测试结果:

1.未分组校验通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

2. 未分组校验未通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

3.Group1分组校验通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

4.Group1分组校验未通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

5.Group2分组校验通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

6.Group2分组校验未通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

7.使用默认分组

ps:将控制层的add3()方法做以下调整:

    // 按默认分组规则校验
    @PostMapping("/add3")
    public Result<String> add3(@Validated(Default.class) @RequestBody StudentDto dto) {
        // TODO 学生信息入库
        return Result.success(dto);
    }

Default.class为Validated依赖中含有的接口类,非自定义接口类 

  • 默认分组,参数校验通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

  •  默认分组,参数校验未通过

校验注解:@Valid 和 @Validated区别与用法(附详细案例)

五、@Valid 和 @Validated 区别

先看下两者的源码:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Valid {
}
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
    Class<?>[] value() default {};
}
  • 相同点:

        @Valid 和 @Validated 两者都可以对数据进行校验,在校验字段上加上规则注解(@NotNull, @NotEmpty等)都可以对 @Valid 和 @Validated 生效。

  • 不同点:

        @Valid 进行校验的时候,需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不 return ,则并不会阻止程序的执行;

        @Valid:没有分组的功能;

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

        @Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示

        @Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制;

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

        两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能。

        总体来说,@Validated 使用起来要比 @Valid 方便一些,它可以帮我们节省一定的代码量,并且使得方法看上去更加的简洁。

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、评论、收藏➕关注,您的支持是我坚持写作最大的动力。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/161444.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!