SpringBoot:参数校验的使用(validator)

项目源码地址:https://gitee.com/huanglei1111/springboot-demo/tree/master/springboot-validator

SpringBoot参数校验的使用(validator)

一、validator简介

Bean Validation是Java定义的一套基于注解的数据校验规范,目前已经从JSR 303的1.0版本升级到JSR 349的1.1版本,再到JSR 380的2.0版本(2.0完成于2017.08),已经经历了三个版本 。需要注意的是,JSR只是一项标准,它规定了一些校验注解的规范,但没有实现,比如@Null、@NotNull、@Pattern等,它们位于 javax.validation.constraints这个包下。而hibernate validator是对这个规范的实现,并增加了一些其他校验注解,如 @NotBlank、@NotEmpty、@Length等,它们位于org.hibernate.validator.constraints这个包下

如果我们的项目使用了Spring Boot,hibernate validator框架已经集成在 spring-boot-starter-web中,所以无需再添加其他依赖。如果不是Spring Boot项目,需要添加如下依赖。

		<dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.16.Final</version>
            <scope>compile</scope>
        </dependency>

温馨提示:
这里要注意配套的版本问题,不然基本类型上面用NotEmpty或者NotBlank 会出现上面的错
Spring boot 中spring-boot-starter-web 中已经匹配的版本。如果不在pom.xml新增validation-api和hibernate-validator就不会出现版本不匹配

二、注解介绍

内置注解

注解说明
@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)被注释的元素必须符合指定的正则表达式

扩展注解

注解说明
@NotBlank被注释的元素不能为null,且长度必须大于0,只能用于注解字符串
@Email被注释的元素必须是电子邮箱地址
@Length(min=,max=)被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的元素值不为null且不为空,支持字符串、集合、Map和数组类型
@Range被注释的元素必须在规定的范围内

三、validator的使用(手动校验)

创建校验工具类

package com.hl.springbootvalidator.util;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.hl.springbootcommon.exception.ParamException;
import org.apache.commons.collections.MapUtils;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.*;

public class BeanValidatorUtil {
    private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory();
    //返回map
    public static <T> Map<String,String> validate(T t, Class... groups){
        Validator validator=VALIDATOR_FACTORY.getValidator();
        Set validateResult=validator.validate(t,groups);
        //如果为空
        if (validateResult.isEmpty()){
            return Collections.emptyMap();
        }else{
            //不为空时表示有错误
            LinkedHashMap errors= Maps.newLinkedHashMap();
            //遍历
            Iterator iterator=validateResult.iterator();
            while (iterator.hasNext()){
                ConstraintViolation violation=(ConstraintViolation) iterator.next();
                errors.put(violation.getPropertyPath().toString(),violation.getMessage());
            }
            return errors;
        }
    }
    //返回list
    public static Map<String,String> validateList(Collection<?> collection){
        //基础校验collection是否为空
        com.google.common.base.Preconditions.checkNotNull(collection);
        //遍历collection
        Iterator iterator=collection.iterator();
        Map errors;
        do {
            //如果循环下一个为空直接返回空
            if (!iterator.hasNext()){
                return Collections.emptyMap();
            }
            Object object=iterator.next();
            errors=validate(object,new Class[0]);
        }while (errors.isEmpty());
        return errors;
    }

     // 校验某一对象是否合法
    public static Map<String,String> validateObject(Object first,Object... objects){
        if (objects !=null && objects.length > 0 ){
            return validateList(Lists.asList(first,objects));
        } else {
            return validate(first , new Class[0]);
        }
    }
    //校验参数方法
    public static void check(Object param) throws ParamException {
        Map<String,String> map= BeanValidatorUtil.validateObject(param);
        //如果错误集合map不为空则抛出异常
        if (MapUtils.isNotEmpty(map)){
            throw  new ParamException(map.toString());
        }
    }
}

对一个对象进行校验

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/insert")
    public HttpResponseTemp<?> getUser2(@RequestBody UserParam userParam){
        Map<String, String> map = BeanValidatorUtil.validateObject(userParam);
        if (MapUtils.isNotEmpty(map)){
            throw ApiException.wrapMessage(ResultStat.SERVER_INTERNAL_ERROR,map.toString());
        }
        return ResultStat.OK.wrap(map,"校验完成");
    }
@Data
public class UserParam {

    private Integer id;

    @NotBlank(message = "用户名不可以为空")
    @Length(min = 1, max = 20, message = "用户名长度需要在20个字以内")
    private String username;

    @NotBlank(message = "电话不可以为空")
    @Pattern(regexp = "^[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}$",message = "只能是数字")
    @Length(min = 1, max = 13, message = "电话长度需要在13个字以内")
    private String telephone;

    @NotBlank(message = "邮箱不允许为空")
    @Pattern(regexp = "^([a-zA-Z]|[0-9])(\\w|\\-)+@[a-zA-Z0-9]+\\.([a-zA-Z]{2,4})$",message = "邮箱格式不正确")
    @Length(min = 5, max = 50, message = "邮箱长度需要在50个字符以内")
    private String mail;

    @NotEmpty
    private List<Integer> lists;
  }

在这里插入图片描述

当邮箱格式和电话号码格式错误的时候

在这里插入图片描述

对象内嵌进行校验

@Data
public class UserParam {

    private Integer id;

    @NotBlank(message = "用户名不可以为空")
    @Length(min = 1, max = 20, message = "用户名长度需要在20个字以内")
    private String username;

    @NotBlank(message = "电话不可以为空")
    @Length(min = 1, max = 13, message = "电话长度需要在13个字以内")
    private String telephone;

    @NotBlank(message = "邮箱不允许为空")
    @Length(min = 5, max = 50, message = "邮箱长度需要在50个字符以内")
    private String mail;

    @Valid
    private Phone phone;

  }

@Data
public class Phone {
	@NotBlank
  	private String operatorType;        
  	@NotBlank    
  	private String phoneNum;
}
@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/insert2")
    public HttpResponseTemp<?> getUser3(@RequestBody UserParam userParam){
        Map<String, String> map = BeanValidatorUtil.validateObject(userParam);
        if (MapUtils.isNotEmpty(map)){
            throw ApiException.wrapMessage(ResultStat.SERVER_INTERNAL_ERROR,map.toString());
        }
        return ResultStat.OK.wrap(map,"校验完成");
    }
}

在这里插入图片描述

四、validator的使用(自动校验)

1.BindingResult 返回错误信息

@RestController
@RequestMapping("/user/auto")
public class UserAutoController {

    @PostMapping("/insert")
    public HttpResponseTemp<?> getUser2(@Valid @RequestBody UserParam userParam, BindingResult result){
        List<FieldError> fieldErrors = result.getFieldErrors();
        if(!fieldErrors.isEmpty()){
            List<String> collect = fieldErrors.stream()
                    .map(o -> o.getDefaultMessage())
                    .collect(Collectors.toList());
            return ResultStat.OK.wrap(collect,"校验错误");
        }

        return ResultStat.OK.wrap(JSONUtil.toJsonStr(userParam),"校验完成");
    }
}    

在这里插入图片描述

2.全局异常处理

如果每个Controller方法中都写一遍对BindingResult信息的处理,使用起来还是很繁琐。可以通过全局异常处理的方式统一处理校验异常。

当我们写了@validated注解,不写BindingResult的时候,Spring 就会抛出异常。由此,可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。

全局异常处理类只需要在类上标注@RestControllerAdvice,并在处理相应异常的方法上使用@ExceptionHandler注解,写明处理哪个异常即可。

@RestControllerAdvice
public class GlobalControllerAdvice {
    private static final String BAD_REQUEST_MSG = "客户端请求参数错误";

    // <1> 处理 form data方式调用接口校验失败抛出的异常 
    @ExceptionHandler(BindException.class)
    public HttpResponseTemp<?> bindExceptionHandler(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.toList());
        return ResultStat.PARAM_ERROR.wrap(collect,BAD_REQUEST_MSG);
    }

    // <2> 处理 json 请求体调用接口校验失败抛出的异常 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public HttpResponseTemp<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.toList());
        return ResultStat.PARAM_ERROR.wrap(collect,BAD_REQUEST_MSG);
    }
    // <3> 处理单个参数校验失败抛出的异常
    @ExceptionHandler(ConstraintViolationException.class)
    public HttpResponseTemp<?> constraintViolationExceptionHandler(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
        List<String> collect = constraintViolations.stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.toList());
        return ResultStat.PARAM_ERROR.wrap(collect,BAD_REQUEST_MSG);
    }
}

事实上,在全局异常处理类中,我们可以写多个异常处理方法,课代表总结了三种参数校验时可能引发的异常:

  1. 使用form data方式调用接口,校验异常抛出 BindException
  2. 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
  3. 单个参数校验异常抛出ConstraintViolationException

全局异常处理类可以添加各种需要处理的异常,比如添加一个对Exception.class的异常处理,当所有ExceptionHandler都无法处理时,由其记录异常信息,并返回友好提示。

3.三种参数校验情况

注:单个参数校验需要在参数上增加校验注解,并在类上标注@Validated

@RestController
@RequestMapping("/user/auto")
@Validated
public class UserAutoController {
    @GetMapping
    public HttpResponseTemp<?> testOne(@Max(value = 4,message = "最大值超过4") int id){
        System.out.println(id);
        return ResultStat.OK.wrap(id,"测试一个参数成功");
    }

    @GetMapping("/testData")
    public HttpResponseTemp<?> testData(@Valid Phone phone){
        System.out.println(phone.toString());
        return ResultStat.OK.wrap(phone,"表单校验成功");
    }

    @PostMapping("/insert2")
    public HttpResponseTemp<?> getUser2(@Valid @RequestBody UserParam userParam){
        System.out.println(userParam.toString());
        return ResultStat.OK.wrap(JSONUtil.toJsonStr(userParam),"校验完成");
    }
}

4.分组校验

如果同一个参数,需要在不同场景下应用不同的校验规则,就需要用到分组校验了。比如:新注册用户还没起名字,我们允许name字段为空,但是不允许将名字更新为空字符。

分组校验有三个步骤:

  1. 定义一个分组类(或接口)
  2. 在校验注解上添加groups属性指定分组
  3. Controller方法的@Validated注解添加分组类
public interface Update extends Default{
}
@Data
public class UserVO {

    @NotNull
    private int id;
    @NotBlank(message = "name 不能为空",groups = Update.class)
    private String name;

}
@PostMapping("/update")
    public HttpResponseTemp<?> update(@Validated({Update.class}) UserVO userVO) {
        return ResultStat.OK.wrap(userVO,"校验完成");
    }

    @PostMapping("/insert3")
    public HttpResponseTemp<?> insert(@Valid UserVO userVO) {
        return ResultStat.OK.wrap(userVO,"校验完成");
    }

测试insert:

{
	"result": {
		"id": 0,
		"name": null
	},
	"resultCode": 200,
	"resultMsg": "校验完成"
}

测试update:

{
	"result": [
		"name 不能为空"
	],
	"resultCode": 400,
	"resultMsg": "客户端请求参数错误"
}

注意:

校验注解(如: @NotBlank)和@validated默认都属于Default.class分组

在编写Update分组接口时,如果继承了Default,下面两个写法就是等效的:

​ @Validated({Update.class})
​ @Validated({Update.class,Default.class})

如果Update不继承Default@Validated({Update.class})就只会校验属于Update.class分组的参数字段

组排序:

 @GroupSequence({First.class,Second.class})  
public interface Group {  
  
}  

@RequestMapping("/addPeople")  
    //不需验证ID  
    public @ResponseBody String addPeople(@Validated({Group.class}) People p,BindingResult result)  {  
        if(result.hasErrors())  {  
            return "0";  
        }  
        return "1";  
    }  

5.校验多个对象

@RequestMapping("/addPeople")  
    public @ResponseBody String addPeople(@Validated People p,BindingResult result,@Validated Person p2,BindingResult result2)  {  
        if(result.hasErrors())  {  
            return "0";  
        }  
        if(result2.hasErrors())  {  
            return "-1";  
        }  
        return "1";  
    }  

6.自定义校验

Spring Validation允许用户自定义校验,实现很简单,分两步:

  1. 自定义校验注解
  2. 编写校验者类

校验枚举类型的数据

package com.mye.cloudboxdcim.framework.validator.anno;

import com.mye.cloudboxdcim.framework.validator.method.ContainsDataValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @ClassName ContainsValidata
 * @Description 自定义注解,判断枚举是否包含
 * @Author hl
 * @Date 2022/11/3 15:52
 * @Version 1.0
 */
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {ContainsDataValidator.class})// 标明由哪个类执行校验逻辑
public @interface ContainsDataValid {

    // 校验出错时默认返回的消息
    String message() default "字段值不正确";

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

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

    String[] values() default {}; // 指定值

    /**
     * 同一个元素上指定多个该注解时使用
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        ContainsDataValid[] value();
    }

}

package com.mye.cloudboxdcim.framework.validator.method;

import com.mye.cloudboxdcim.framework.validator.anno.ContainsDataValid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ContainsDataValidator implements ConstraintValidator<ContainsDataValid,String> {
	// 全局变量存放值集合
    private List<String> values = new ArrayList<>();
	
	// 注解初始化时执行
    @Override
    public void initialize(ContainsDataValid constraintAnnotation) {
		// 获取注解中的值
        String[] strList = constraintAnnotation.values();
		// 赋值给全局变量
        values = Arrays.stream(strList).collect(Collectors.toList());
    }

	// 自定义的校验规则
    @Override
    public boolean isValid(String o, ConstraintValidatorContext constraintValidatorContext) {
    	// o 为实体属性的值
    	// 判断值是否属于集合中的元素,true 检验通过,false校验不通过
        return values.contains(o);
    }
}

校验字段非空时候的校验

package com.mye.cloudboxdcim.framework.validator.anno;

import com.mye.cloudboxdcim.framework.validator.method.HaveNoBlankValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.NotBlank;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
public @interface HaveNoBlankValid {
    
    // 校验出错时默认返回的消息
    String message() default "字符串不合法";

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

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

    String value() default ""; // 指定值

    /**
     * 同一个元素上指定多个该注解时使用
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        NotBlank[] value();
    }
}

package com.mye.cloudboxdcim.framework.validator.method;

import cn.hutool.core.util.StrUtil;
import com.mye.cloudboxdcim.framework.validator.anno.HaveNoBlankValid;
import com.mye.cloudboxdcim.util.StringUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashMap;
import java.util.Map;


public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlankValid, String> {

    private String initValue = "";

    /**
       * @MethodName initialize
       * @Description  注解初始化时执行
       * @param haveNoBlankValid  自定义注解
       * @Author hl
       * @Date 17:05 17:05
       */
    @Override
    public void initialize(HaveNoBlankValid haveNoBlankValid) {
        // 获取注解中的值
        initValue = haveNoBlankValid.value();
    }

    /**
       * @MethodName isValid
       * @Description  自定义校验规则
       * @param value 具体值
       * @param context
       * @return: boolean true 通过校验 false 没有通过校验
       * @Author hl
       * @Date 17:04 17:04
       */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // null 不做检验
        if (StrUtil.isBlank(value)) {
            //true 通过校验
            return true;
        }

        if (StrUtil.isNotBlank(initValue)){
            if (initValue.equals("description")){
                //检查描述长度
                return checkDescription(value);
            } else if (initValue.equals("unit")) {
                //检查信号单位长度
                return checkUnit(value);
            } 
        }

        return true;
    }

  
    private boolean checkUnit(String value) {
        return value.length() <= 10;
    }


    private Boolean checkDescription(String value) {
        return value.length() <= 200;
    }
}

五、@Validator和@Valid的区别

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

  1. 分组:

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

  2. 注解使用地方:

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

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

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

六、hibernate的校验模式

1、普通模式(默认是这个模式)

普通模式(会校验完所有的属性,然后返回所有的验证失败信息)

2、快速失败返回模式

快速失败返回模式(只要有一个验证失败,则返回)

failFast:true 快速失败返回模式 false 普通模式

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .failFast( true )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

和 (hibernate.validator.fail_fast:true 快速失败返回模式 false 普通模式)

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
        .configure()
        .addProperty( "hibernate.validator.fail_fast", "true" )
        .buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

3.配置hibernate Validator为快速失败返回模式

@Configuration
public class ValidatorConfiguration {
    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
                .configure()
                .addProperty( "hibernate.validator.fail_fast", "true" )
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();

        return validator;
    }
}
  • 3
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值