Spring Boot中的校验-Validation的使用

3,185 阅读7分钟

实际业务中,我们是离不开数据的校验的。比如注册用户,用户名和密码是不能为空的。

今天的分享,我用一个简单的使用 Spring Boot+MyBatis 程序添加用户为例,来进行讲解。

1,平常我们进行数据校验

例如这个添加用户的查询,我们要将用户类User的实例添加进数据库,代码如下:

首先是User类:

package com.example.validationtest.dataobject;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.io.Serializable;

/**
 * 用户类
 */
@Setter
@Getter
@NoArgsConstructor
public class User implements Serializable {

   /**
    * 主键id
    */
   private Integer id;

   /**
    * 用户名
    */
   private String username;

   /**
    * 密码
    */
   private String password;

   /**
    * 邮箱
    */
   private String email;

   /**
    * 年龄
    */
   private int age;

}

我们会在Service层里面调用dao来把数据添加进数据库,不过我们会在Service类里面写很多校验字段的东西,例如这个添加用户的方法片段:

@Autowired
private UserDAO userDAO;

@Override
public String add(User user) {
   if (StringUtils.isEmpty(user.getUsername())) {
      return "用户名不能为空!";
   }
   if (StringUtils.isEmpty(user.getPassword())) {
      return "密码不能为空!";
   }
   ...
   userDAO.add(user);
   return "添加成功!";
}

最后再使用Controller处理请求进行添加。

这样的逻辑没有错,但是当实体类多起来了,那就很麻烦了,要对每个字段进行if判断校验,不仅麻烦,而且代码冗余。

因此我们需要用到Spring Validation来解决这个问题。

2,配置Validation

在新建Spring Boot工程的时候,我们可以勾上Validation这个依赖。

image.png

也可以后续手动在pom.xml里面添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

3,使用注解对实体类的字段设定校验规则

Validation提供了很多校验规则注解,将其注解在字段上以设定其校验规则,这里列出几个常用的:

  • @NotEmpty 对象不允许为null或者空,例如字符串长度为0,集合、Map等对象长度为0,都判为空
  • @NotBlank 不允许为null和纯空格
  • @NotNull 不允许为空对象
  • @AssertTrue 值是否为true
  • @Size 约定字符串长度,其中参数min表示约定最短长度,max为最大长度
  • @Min 规定数值型属性的最小值
  • @Max 规定数值型属性的最大值
  • @Email 字符串必须为邮箱格式

每个校验都可以累加,一旦一个类中有校验失败,就会记录其中校验错误。每个校验都有message参数,可以自定义校验失败时返回的内容。

建议大多数时候使用@NotEmpty注解代替@NotBlank和@NotNull,不过校验数值类型时,还是需要使用@NotNull。

还是接着上面例子,我们将上述User类修改如下:

package com.example.validationtest.dataobject;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * 用户类
 */
@Setter
@Getter
@NoArgsConstructor
public class User implements Serializable {

   /**
    * 主键id
    */
   private Integer id;

   /**
    * 用户名
    */
   @NotEmpty(message = "用户名不能为空!")
   private String username;

   /**
    * 密码
    */
   @Size(min = 8, message = "密码长度不能小于8!")
   @NotEmpty(message = "密码不能为空!")
   private String password;

   /**
    * 邮箱
    */
   @Email(message = "邮箱格式错误!")
   private String email;

   /**
    * 年龄
    */
   @Min(value = 18, message = "年龄不能小于18!")
   @Max(value = 150, message = "年龄不能小于150!")
   private int age;

}

可以看看字段上的注解的使用,一般设定参数message自定义错误消息,还有value参数规定相应校验值等等。

有了这些校验注解,我们就不用再在Service里面写if去进行判断了!

4,编写Controller类并设定传入的User对象需要校验

现写一简易的Controller方法,接收POST请求传入User实例,传入时指定其需要校验,如果校验失败返回校验信息,成功则调用Service层添加用户(这里省略Service代码)代码如下:

@PostMapping("/add")
public String add(@RequestBody @Valid User user, BindingResult errors) {
   if (errors.hasErrors()) {
      return errors.getFieldError().getDefaultMessage();
   }
   userService.add(user);
   return "添加成功!";
}

注意这个Controller方法的参数,传入的User对象前面打了@Valid注解,旨在告诉Controller这个需要校验,且比起平常这里多了个参数为BindingResult对象,这个对象用于存放校验错误信息。一般通过BindingResult对象的hasErrors方法判断是否有错误,如果有错误,通过errors.getFieldError().getDefaultMessage()获取错误信息(上文errors就是我们的BindingResult对象)。例如我现在使用Postman工具发送请求数据体如下:

image.png

结果:

image.png

可见使用Validation的注解可以轻松实现校验,而不再需要我们逐个手写逻辑

5,优化校验规则-分组校验

同样是用户对象,我们可能在不同情景下,有着不同的校验规则。例如:

  • 用户id通常是自增主键,因此注册用户是传入的用户id字段可以为空,但是用户名密码等等不能为空
  • 然而,更新/修改用户信息时是根据用户id找到用户的,这时传入的用户id又不能为空了,但是用户名密码可以为空(一般来说,前端传入用户对象修改用户信息时,空的字段代表这个字段不需要修改保持原值,这时后端将空的字段用原来的值填上并写进数据库即可)

对于不同的校验规则,我们可以为其分别设定校验规则,并在不同的API使用不同的校验规则。

校验规则的参数要填的是,因此我们用空的接口表示校验规则(空接口可以理解为不同校验规则组的名字),这里我创建param软件包,在里面建立类ValidationRules,并在类中写两个空接口分别表示用户注册、修改的校验规则:

package com.example.validationtest.param;

/**
 * 校验规则类
 */
public class ValidationRules {

   /**
    * 注册(添加)用户规则
    */
   public interface UserAdd {

   }

   /**
    * 更新(修改)用户规则
    */
   public interface UserUpdate {

   }

}

然后,我们再修改User类如下:

package com.example.validationtest.dataobject;

import com.example.validationtest.param.ValidationRules;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.validation.constraints.*;
import java.io.Serializable;

/**
 * 用户类
 */
@Setter
@Getter
@NoArgsConstructor
public class User implements Serializable {

   /**
    * 主键id,将其设定分组为ValidationRules.UserUpdate,表示用户信息修改时校验该规则
    */
   @NotNull(groups = ValidationRules.UserUpdate.class, message = "用户id不能为空!")
   private Integer id;

   /**
    * 用户名,将其设定分组为ValidationRules.UserAdd,表示添加用户时校验该规则
    */
   @NotEmpty(groups = ValidationRules.UserAdd.class, message = "用户名不能为空!")
   private String username;

   /**
    * 密码,将长度校验规则同时加入到ValidationRules.UserAdd和ValidationRules.UserUpdate组,表示添加用户和用户信息修改时都要校验这个规则,空值校验只有添加时校验
    */
   @Size(groups = {ValidationRules.UserAdd.class, ValidationRules.UserUpdate.class}, min = 8, message = "密码长度不能小于8!")
   @NotEmpty(groups = ValidationRules.UserAdd.class, message = "密码不能为空!")
   private String password;

   // 下面都是一回事

   /**
    * 邮箱
    */
   @Email(groups = {ValidationRules.UserAdd.class, ValidationRules.UserUpdate.class}, message = "邮箱格式错误!")
   private String email;

   /**
    * 年龄
    */
   @Min(groups = {ValidationRules.UserAdd.class, ValidationRules.UserUpdate.class}, value = 18, message = "年龄不能小于18!")
   @Max(groups = {ValidationRules.UserAdd.class, ValidationRules.UserUpdate.class}, value = 150, message = "年龄不能小于150!")
   private int age;

}

可见,我们在每个校验注释中加上了groups参数,这个参数就是用于指定每个校验规则的分组。这个参数值必须是类class类型,用我们创建的空接口即可,就代表给这个规则指定分组在哪个类。一个校验规则不仅仅可以只给它指定一个分组,也可以指定多个分组(参数值写成数组)。

然后现在编写Controller类,给不同的接口应用不同的规则:

package com.example.validationtest.api;

import com.example.validationtest.dataobject.User;
import com.example.validationtest.param.ValidationRules;
import org.springframework.validation.BindingResult;
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;

/**
 * 测试校验(这里不做实际的用户添加删除工作,只是测试validation校验)
 */
@RestController
@RequestMapping("/api/user")
public class UserAPI {

   /**
    * 模拟添加用户,这里设置校验规则为ValidationRules.UserAdd
    */
   @PostMapping("/add")
   public String add(@RequestBody @Validated(ValidationRules.UserAdd.class) User user, BindingResult errors) {
      if (errors.hasErrors()) {
         return errors.getFieldError().getDefaultMessage();
      }
      return "添加成功!";
   }

   /**
    * 模拟修改用户,这里设置校验规则为ValidationRules.UserUpdate
    */
   @PostMapping("/update")
   public String update(@RequestBody @Validated(ValidationRules.UserUpdate.class) User user, BindingResult errors) {
      if (errors.hasErrors()) {
         return errors.getFieldError().getDefaultMessage();
      }
      return "修改成功!";
   }

}

测试添加接口:

image.png

同样拿这个数据测试修改接口:

image.png

这里在Controller中,我们这里换用了@Validated注解实现指定分组校验规则进行校验,其中参数值也是代表分组的类(也可以写多个,用数组表示),上述add接口指定了@Validated参数为ValidationRules.UserAdd.class,那么这个接口只会去校验用户类中,设定了groups参数值包含ValidationRules.UserAdd.class的规则。

可见分组校验是一个非常好用方便的功能。

示例仓库地址