由浅入深使用validation框架进行参数校验

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1 引言

平时在业务开发过程中 controller 层的参数校验有时会存在下面这样的判断

public String add(UserVO userVO) {
    if(userVO.getAge() == null){
        return "年龄不能为空";
    }
    if(userVO.getAge() > 120){
        return "年龄不能超过120";
    }
    if(userVO.getName().isEmpty()){
        return "用户名不能为空";
    }
    // 省略一堆参数校验...
    return "OK";
}

业务代码还没开始写呢光参数校验就写了一堆判断。这样写虽然没什么错但是给人的感觉就是不优雅不专业。

其实java给我们提供了一组规范JSR-303spring又实现并扩展了这个规范让我们能够优雅无侵入的实现参数的校验

2 JSR-303 规范

2.1 JSRJava 规范提案

JSR即为Java Specification Requests的缩写是Java 规范提案常常定义一组规则有的规则有默认的实现有的则需要用户自己去实现。

2.2 JSR-303Bean Validation规范

JSR-303 是JAVA EE 6 中的一项子规范叫做Bean Validation为Bean验证定义了元数据模型和API默认的元数据模型是通过Annotations注解来描述的支持扩展。

官方参考的默认实现是Hibernate Validator当然用户也可以自己扩展比如常用的Spring JSR-303

注意这只是一组规范对应到代码就是一组接口不能直接使用
这组规范在javax所提供的jar:jakarta.validation-api-xxx.jar中要想使用则需要导入并实现这组规范

 <dependency>
     <groupId>jakarta.validation</groupId>
     <artifactId>jakarta.validation-api</artifactId>
     <version>2.0.2</version>
 </dependency>

2.3 JSR-303 基本的约束原理

一个 constraint 通常由 annotation 和相应的 constraint validator 组成它们是一对多的关系。

也就是说可以有多个 constraint validator 对应一个 annotation。

在运行时Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。

  • constraint一个约束
    • annotation注解
    • constraint validator约束校验器可有多个

有些时候在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制可以通过两种方法去实现。

  • 一种是组合现有的 constraint 来生成一个更复杂的 constraint
  • 另外一种是开发一个全新的 constraint。

2.4 JSR-303 中内置的约束

内置的约束注解如下图
在这里插入图片描述

3 JSR-303规范的实现

3.1 Hibernate Validator

这是官方参考的默认实现Hibernate JSR-303导入并实现了jakarta.validation-api并对JSR-303默认的约束做了扩展注意此实现与 Hibernate ORM 没有任何关系。

3.1.1 Hibernate Validator 附加的 constraint

Constraint详细信息
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串的必须非空
@Range被注释的元素必须在合适的范围内

3.1.2 导入依赖

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.63</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.3.Final</version>
    <scope>compile</scope>
</dependency>

3.1.3 简单使用

package myValid;

import lombok.Data;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import java.util.Iterator;
import java.util.Set;

@Data
public class User {

    @NotNull(message = "email不可以为空")
    @Email(message = "email不合法")
    private String email;

    public static void main(String[] args) {
        final User user = new User();
        user.setEmail("xxxxxx.com");
        //调用JSR303验证工具校验参数
        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        Iterator<ConstraintViolation<User>> iter = violations.iterator();
        if (iter.hasNext()) {
            final ConstraintViolation<User> violation = iter.next();
            // email:email不合法
            System.out.println(violation.getPropertyPath() + ":" + violation.getMessage());
            // TODO 可以进行指定约束异常的抛出
        }
    }

}

3.2 Spring JSR-303

3.2.1 官方说明

JSR-303的javax.validation的变体。有效支持验证组的规范。为方便使用Spring的JSR-303支持而设计但不是JSR-303特有的。

可以与Spring MVC处理程序方法参数一起使用。通过org.springframework.validation.SmartValidator的验证提示概念支持验证组类充当提示对象。

也可以与方法级验证一起使用表示特定类应该在方法级进行验证充当相应验证拦截器的切入点但也可以在带注释的类中为方法级验证指定验证组。在方法级别应用此注释允许覆盖特定方法的验证组但不作为切入点然而类级注释对于触发特定bean的方法验证是必要的。也可以用作自定义原型注释或自定义组特定的验证注释上的元注释。

3.2.2 通俗解释

  • 实现了JSR-303标准
  • 支持分组验证特有
  • 可以与Spring MVC收参的方法一起使用

4 Spring Validator的使用

4.1 导入依赖

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

点进入发现Spring Validator包含了Hibernate Validator所以spring环境下我们直接使用Spring Validator会非常非常方便

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>2.7.0</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-el</artifactId>
      <version>9.0.63</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.2.3.Final</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

4.2 常用的约束说明

验证注解验证的数据类型说明
@AssertFalseBoolean,boolean验证注解的元素值是false
@AssertTrueBoolean,boolean验证注解的元素值是true
@NotNull任意类型验证注解的元素值不是null
@Null任意类型验证注解的元素值是null
@Min(value=值)BigDecimalBigInteger, byte,short, int, long等任何Number或CharSequence存储的是数字子类型验证注解的元素值大于等于@Min指定的value值
@Maxvalue=值和@Min要求一样验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值)和@Min要求一样验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值)和@Min要求一样验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数)和@Min要求一样验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限)字符串、Collection、Map、数组等验证注解的元素值的在min和max包含指定区间之内如字符长度、集合大小
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值日期类型比当前时间早
@Future与@Past要求一样验证注解的元素值日期类型比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空不为null、去除首位空格后长度为0不同于@NotEmpty@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、Collection、Map、数组验证注解的元素值不为null且不为空字符串长度不为0、集合大小不为0
@Range(min=最小值, max=最大值)BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式)CharSequence子类型如String验证注解的元素值是Email也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式)String任何CharSequence的子类型验证注解的元素值与指定的正则表达式匹配
@Valid任何非原子类型指定递归验证关联的对象如用户对象中有个地址对象属性如果想在验证用户对象时一起验证地址对象的话在地址对象上加@Valid注解即可级联验证

4.3 重点使用技巧

一般情况下我们都是校验方法上面的形参既然是形参就分两种情况

  • 一种是校验形参本身这种情况需要在当前类上面添加@Validated注解同时使用@NotNull等注解对该形参进行校验
  • 另一种则是形参内部属性的校验这种情况可以在当前类上面添加@Validated注解也可以不加但是一定要在该形参上添加@Validated注解
  • 未通过校验时会抛出org.springframework.web.bind.MethodArgumentNotValidExceptionorg.springframework.validation.BindException等异常我们捕获统一返回即可

下面进行举例说明比如说我们有一个DocumentTransController

@RestController
@RequestMapping("/documentTrans")
public class DocumentTransController {

    private final DocumentTransService documentTransService;
    public DocumentTransController(DocumentTransService documentTransService) {
        this.documentTransService = documentTransService;
    }

    /**
     * 接收翻译请求接收文件
     * @return
     */
    @RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)
    public String transDocumentByFile(MultipartFile file,DocumentInfoBo documentInfoBo) {
        return documentTransService.documentTrans(file,documentInfoBo);
    }
    
}

@Data
public class DocumentInfoBo {
    /**
     * 所属用户id
     */
    @NotNull(message = "用户id不能为空")
    private Long userId;
    
    private UserInfo userInfo;

    @Data
    static class UserInfo {
        @NotNull(message = "用户名不能为空")
        private String username;
        private String phone;
    }
}    

4.3.1 校验方法形参本身

我现在想要校验transDocumentByFile()的file不能为空则需要在DocumentTransController 类上添加@Validated注解同时在(MultipartFile file)前面添加@NotNull 注解如下

@RestController
@RequestMapping("/documentTrans")
@Validated
public class DocumentTransController {

    private final DocumentTransService documentTransService;

    public DocumentTransController(DocumentTransService documentTransService) {
        this.documentTransService = documentTransService;
    }

    /**
     * 接收翻译请求接收文件
     * @return
     */
    @RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)
    public String transDocumentByFile(@NotNull MultipartFile file, DocumentInfoBo documentInfoBo) {
        return documentTransService.documentTrans(file,documentInfoBo);
    }
    
}

4.3.2 校验方法形参内部属性

在4.3.1的基础上我现在想要校验DocumentInfoBo对象内的userId属性不能为空则需要在(DocumentInfoBo documentInfoBo)前面添加@Validated注解

@RestController
@RequestMapping("/documentTrans")
@Validated
public class DocumentTransController {

    private final DocumentTransService documentTransService;

    public DocumentTransController(DocumentTransService documentTransService) {
        this.documentTransService = documentTransService;
    }

    /**
     * 接收翻译请求接收文件
     * @return
     */
    @RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)
    public String transDocumentByFile(@NotNull MultipartFile file,@Validated DocumentInfoBo documentInfoBo) {
        return documentTransService.documentTrans(file,documentInfoBo);
    }
    
}

4.3.3 嵌套验证

现在我想在4.3.1和4.3.2基础上添加对userInfo属性的校验则需要在userInfo上面添加 @Valid @NotNull(message = "userInfo不能为空")即可

@Data
public class DocumentInfoBo {
    /**
     * 所属用户id
     */
    @NotNull(message = "用户id不能为空")
    private Long userId;
    
    @Valid
    @NotNull(message = "userInfo不能为空")
    private UserInfo userInfo;

    @Data
    static class UserInfo {
        @NotNull(message = "用户名不能为空")
        private String username;
        private String phone;
    }
}    

4.3.4 分组参数校验

第一步、定义一个校验组类声明四个接口对应不同场景校验

public class ValidationGroups {

	public interface Select {
	}
	
	public interface Insert {
	}
	
    public interface Update {
    }
    
    public interface Detail {
    }
  
    public interface Delete{

    }
}

第二步、在实体类具体属性添加校验规则及校验分组

@Data
public class Person {

    @NotNull(message = "personId不能为null",groups = Select.class)
    private Integer personId;
    
    @NotEmpty(message = "name不能为空",groups = Insert.class)
    private String name;
    
    private Integer age;
}

第三步、在控制层创建查询和新增接口添加@Validated注解指定分组可多个。

    @GetMapping("/select")
    public Object select(@Validated({ValidationGroups.Select.class}) Person person) {
        return person;
    }

    @GetMapping("/insert")
    public Object insert(@Validated(ValidationGroups.Insert.class) Person person) {
        return person;
    }

4.4 重点注意事项

Spring Validator不光可以用在Controller层同样也可以作用于service、或者其他bean

4.5 重点@Validated和@Valid的区别

很多时候@Validated和@Valid的界限分的并没有那么清楚很多场景@Validated和@Valid效果也等同那么他们到底有什么区别呢什么时候该用哪个注解呢

在检验Controller的入参是否符合规范时使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同

@Validated@Valid
分组提供分组功能可在入参验证时根据不同的分组采用不同的验证机制。无分组功能
可注解位置可以用在类型、方法和方法参数上。但是不能用在成员属性上可以用在方法、构造函数、方法参数和成员属性上两者是否能用于成员属性上直接影响能否提供嵌套验证的功能
嵌套验证用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性上。也无法提供框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性上提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
  • @Valid 被注释的元素是一个对象需要检查此对象的所有字段值
  • @Validated 被注解的元素是一个对象或者一个类需要检查此对象的所有字段值

使用建议常规情况使用@Validated注解嵌套验证时使用@Valid注解

4.6 校验异常统一处理示例

异常对应

  • 使用form data方式调用接口校验异常抛出 BindException
  • 使用 json 请求体调用接口校验异常抛出 MethodArgumentNotValidException
  • 单个参数校验异常抛出ConstraintViolationException
@RestControllerAdvice
public class ExceptionHandle {

    private final Logger logger = LoggerFactory.getLogger(getClass());
    
 	@ExceptionHandler(value = Exception.class)
    public JSONResult<Object> handle(Exception e) {
        logger.error(e.getMessage(), e);
        if (e instanceof MyException) {
            MyException myException = (MyException) e;
            return JSONResult.error(myException.getCode(),myException.getMessage());
        } else {
            return JSONResult.error(ResultEnum.UNKNOWN_ERROR.getCode(), e.getMessage());
        }
    }

    /**
     * 方法参数校验
     * 由于spring捕获异常的问题导致此异常的编码格式无法修改为UTF-8
     * 故不使用RestControlle的ResponseBody,自己实现httpServletResponse的IO。
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public JSONResult<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        final BindingResult bindingResult = e.getBindingResult();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        String msg = ResultEnum.VALIDATION_ERROR.getMsg();
        if(!fieldErrors.isEmpty()){
            // 返回错误信息
            msg = fieldErrors.get(0).getField()+":"+fieldErrors.get(0).getDefaultMessage();
        }
        logger.error(e.getMessage(), e);
        return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), msg.isEmpty()?e.getCause().getMessage():msg);
    }

    /**
     * 捕获校验异常
     * @param e
     * @return
     */
    @ExceptionHandler(ValidationException.class)
    public JSONResult<Object> handleValidationException(ValidationException e) {
        logger.error(e.getMessage(), e);
        return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), e.getCause().getMessage());
    }


    /**
     * 捕获校验绑定异常
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    public JSONResult<Object> handleBindException(BindException e) {
        logger.error(e.getMessage(), e);
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        String msg = "";
        if(!fieldErrors.isEmpty()){
            // 返回错误信息
            msg = fieldErrors.get(0).getDefaultMessage();
        }
        return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), msg.isEmpty()?e.getCause().getMessage():msg);
    }

    /**
     * 捕获违反约束异常
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public JSONResult<Object> handConstraintViolationException(ConstraintViolationException e) {
        final Iterator<ConstraintViolation<?>> iterator = e.getConstraintViolations().iterator();
        while (iterator.hasNext()) {
            final ConstraintViolation<?> constraintViolation = iterator.next();
            final String errorMsg = constraintViolation.getMessage();
            logger.error(e.getMessage(), e);
            return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), errorMsg);
        }
        return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), e.getMessage());
    }

5 扩展注解失效的原因排查

1、是否忘记导入依赖
在2.3.0版本之前spring-boot-starter-web是默认集成validation依赖的但是在2.3.0开始就去掉了该依赖所以需要自己添加。

2、如果要校验对象本身则需要在所在类上面添加@Validated注解
在这里插入图片描述
3、涉及嵌套验证是否忘记添加@Valid注解

6 使用ConstraintValidator自定义注解和校验器

https://blog.csdn.net/weixin_43702146/article/details/125657418

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6