一文弄懂spring validate(上)

简介: 一文弄懂spring validate(上)

前言:



校验参数在以前基本都是使用大量的if/else,稍微方便一点的可以使用反射+自定义注解的形式,但是复用性不是很好,并且每个人对于的自定义注解有着自己的使用习惯,不过好在spring开发了validated框架用于注解校验,可以节省很多的校验ifelse代码,这篇文章通篇介绍了如何使用spring validated。


文章目的:


  1. 了解 validate 校验,快速的集成和使用 spring validate,以及使用注解对于参数进行快速校验
  2. 关于统一全局异常处理,以及一些踩坑问题。
  3. 讨论list<Object>校验的问题,分析如何使用list对象内容校验


简单介绍


spring Validation 是一种参数检验工具,集成在spring-context包中, 常用于spring mvcController的参数处理,主要针对整个实体类的多个可选域进行判定,对于不合格的数据信息springMVC会把它保存在错误对象中,这些错误信息我们也可以通过SpringMVC提供的标签或者前端的脚本等在前端页面上进行展示。

实现方式和使用方式:一般使用较多的是两个注解:@Validated@Valid

  1. 第一种使用方式:使用Validator,利用BindingResult获取Errors信息
  2. 第二种使用方式:采用@Valid 以及 JSR-303(@validated)中的参数判定注解


@Valid@Validated区别


区别 @Valid @Validated
提供者 JSR-303规范 Spring
是否支持分组 不支持 支持
标注位置 METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
嵌套校验 支持 不支持


常用注解


网上有很多类似表格,这里直接COPY了一份,对于不同的api版本可能出现部分注解过时等情况,注意!


meta-data comment version
@Null 对象,为空 Bean Validation 1.0
@NotNull 对象,不为空 Bean Validation 1.0
@AssertTrue 布尔,为True Bean Validation 1.0
@AssertFalse 布尔,为False Bean Validation 1.0
@Min(value) 数字,最小为value Bean Validation 1.0
@Max(value) 数字,最大为value Bean Validation 1.0
@DecimalMin(value) 数字,最小为value Bean Validation 1.0
@DecimalMax(value) 数字,最大为value Bean Validation 1.0
@Size(max, min) min<=value<=max Bean Validation 1.0
@Digits (integer, fraction) 数字,某个范围内 Bean Validation 1.0
@Past 日期,过去的日期 Bean Validation 1.0
@Future 日期,将来的日期 Bean Validation 1.0
@Pattern(value) 字符串,正则校验 Bean Validation 1.0
@Email 字符串,邮箱类型 Bean Validation 2.0
@NotEmpty 集合,不为空 Bean Validation 2.0
@NotBlank 字符串,不为空字符串 Bean Validation 2.0
@Positive 数字,正数 Bean Validation 2.0
@PositiveOrZero 数字,正数或0 Bean Validation 2.0
@Negative 数字,负数 Bean Validation 2.0
@NegativeOrZero 数字,负数或0 Bean Validation 2.0
@PastOrPresent(时间) 过去或者现在 Bean Validation 2.0
@FutureOrPresent(时间) 将来或者现在 Bean Validation 2.0


快速入门


spring validate入门使用都十分的简单,基本十分钟不到就能快速的集成,目前springboot的项目已经越来越多,所以本文基本都是基于springboot构建的,spring mvc集成和本文类似。


第一步:pom.xml 加入注解

这里为了方便版本控制增加了版本控制配置:

注意:hibernate-validate 的版本到本文为止已经出现了7.0.0,这个版本的校验做了不少的改动。

docs.jboss.org/hibernate/s…


<properties>
    <hibernate-validate.version>5.2.0.Final</hibernate-validate.version>
</properties>
复制代码


增加完配置之后,增加对应的maven依赖,需要引入如下两个依赖配置


<!--jsr 303-->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>
<!-- hibernate validator-->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <!-- 注意使用了版本控制 -->
    <version>${hibernate-validate.version}</version>
</dependency>
复制代码


加完这两个注解之后,不要急着先进行其他的编写,可以现在任意的class类里面敲击上面提到的注解,看下是不是有对应的注解了,在做下一步。


第二步:增加注解

这里给出一个案例进行参考,验证对象增加注解:

@NotBlank:字符串不能为Null以及不能为空字符串,建议String都使用这种类型

@Length(max = 7)maxmin表示字符的最大长度,这里多提一嘴的是接口一定要加上这种注解,注意是一定,因为接口给参数不加以长度限制就等着数据库截断二进制吧。


@Pattern:根据案例就可以看到,就是JAVA对应的Pattern,可以编写正则进行校验,很棒,因为Pattern对象比较占用空间。目前个人没有验证过这个注解的校验性能问题,有想法可以自己测试一下。


public class TestProduct {
    @NotBlank
    @Length(max = 7)
    @Pattern(regexp = "^(([1-9]{1}\\d*)|([0]{1}))(\\.(\\d){0,2})?$")
    private String tranAmount;
    @NotBlank
    @Length(max = 3,min = 3)
    @Pattern(regexp = "^[A-Z]{3}$")
    private String currency;
    @NotBlank
    @Length(max = 100)
    private String tranReason;
    @NotBlank
    @Length(max = 100)
    private String gatherName;
    @NotBlank
    @Length(max = 10)
    private String business_type;
    /
    @NotBlank
    @Length(max = 10)
    private String pay_channel;
    // .......其他校验
    // 过滤 get/set 方法
}
复制代码


controller层增加@Validated注解,效果如下:

个人比较喜欢写在参数的前面,也可以写在方法的上面,比较自由


@Controller
@RequestMapping("/test")
public class TestController{
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    @ResponseBody
    public Object create(@Validated @RequestBody Product requestString, BindingResult bindResult) {
        // 统一处理校验注解的错误信息
        Result stringBuilder = dealWithError(bindResult);
        if (stringBuilder != null){
            return stringBuilder;
        }
       // 自己的业务处理...
        return ....;
    }
}
复制代码


第三步:验证注解是否生效


到这一步就可以直接请求接口,在接口处进行断点,如果请求正确会直接进入对应的断点,否则会抛出如下案例所示的异常信息,如果校验不通过,会抛出MethodArgumentNotValidException或者ConstraintViolationException异常,下面是案例:


{
    "timestamp": "2021-01-01T12:08:49.859+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object 'product' on field 'name': rejected value [null]; codes [NotBlank.product.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.name,name]; arguments []; default message ......",
    "message": "Validation failed for object='product'. Error count: 1",
    "errors": [
        {
            "codes": [
                "NotBlank.product.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "product.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "不能为空",
            "objectName": "product",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        }
    ],
    "path": "//test/valid"
}
复制代码


到此,一个对象的注解校验基本实现了,但是我们发现注解校验的方式抛出的异常信息不是十分友好,基本都会配合统一的异常处理来处理请求参数的问题,后文会单独讲如何使用全局异常处理来统一的处理异常信息。下面先了解一下如何自定义注解校验。


自定义注解校验:


如果默认的注解规则无法满足业务需求,这时候validator提供了自定义注解的形式帮助开发者可以进行自定的规则校验。


第一步:定义自定义注解:

首先第一步是确定自己需要自定义的注解:比如我这里定义了一个检查时间格式的注解


/**
 * 日期格式校验注解
 */
import org.hibernate.validator.constraints.EAN;
import org.hibernate.validator.constraints.NotBlank;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
// 注意这里有静态导入
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
// javadoc 文档标识
@Documented
// 可以注入的类型,字段和参数类型
@Target({PARAMETER, FIELD})
// 运行时生效
@Retention(RUNTIME)
// 触发校验的对象
@Constraint(validatedBy = {TimeValidator.class})
public @interface Time {
    // 必须
    String message() default "时间格式校验失败";
    // 必须
    Class<?>[] groups() default {};
    // 必须
    Class<? extends Payload>[] payload() default {};
    String value = "";
    // 下面部分可以忽略
    @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        Time[] value();
    }
}
复制代码


对于注解的解释:


  • @Retention(RUNTIME):指定此类型的注释将在运行时通过反射方式可用
  • @Constraint:指定用于验证元素的验证器
  • @Target:注解的标识范围,比如这里注解可以是参数或者字段

对应的三个固定参数含义:


  • message 定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制
  • groups这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
  • payload 主要是针对bean的,使用不多。

@Constraint注解声明约束及其可配置属性,同时在对应的真实注解处理类TimeValidator里面,在真实注解类里面可以随意的注入需要的beanAutoWired等)

注意上面注意除开value这个属性之外,其他三个属性messagegroupspayload都是必须定义的,否则进行校验的时候,会抛出如下的错误:


HV000074: com.xxx.xxx.valid.annotation.Time contains Constraint annotation, but does not contain a groups parameter.
复制代码


扩展:HV000074这个错误是如何来的?:

首先我们需要明确一点:javax.validator - JSR303的规范是由Hibernate validate作为标准实现的,也就是说虽然Spring已经为我们进行了适配,但是在校验的时候依然使用的Hibernate Validator,所以我们定义自定义的注解需要按照固定的要求规范:

旧版本的文档:docs.jboss.org/hibernate/v…

较新版本的文档:docs.jboss.org/hibernate/s…

org.hibernate.validator.internal.util.logging下面的几个包中定义了日志以及异常信息:


private static final String getConstraintWithoutMandatoryParameterException = "HV000074: %2$s contains Constraint annotation, but does not contain a %1$s parameter.";
复制代码


具体的提示信息如下图所示:org.hibernate.validator.internal.util.logging.Log_$logger

注解定义了如下的异常信息提示


@Message(
     id = 74,
     value = "%2$s contains Constraint annotation, but does not contain a %1$s parameter."
 )
 ConstraintDefinitionException getConstraintWithoutMandatoryParameterException(String var1, String var2);
复制代码


注意以下几个点:

  • 静态字段和属性无法验证。
  • 建议在一个类中坚持使用字段 属性注释。不建议对字段随附的getter方法进行注释*,*因为这将导致对该字段进行两次验证。


第二步:定义真实注解处理类:

需要实现接口ConstraintValidator,泛型的第一个参数为注解类,第二个参数为具体校验对象的类型


下面定义校验时间格式是否正确的一个案例,写的非常粗浅,仅供参考:


public class TimeValidator implements ConstraintValidator<Time, String> {
    /**
     * 初始化注解的校验内容
     * @param constraintAnnotation
     */
    @Override
    public void initialize(Time constraintAnnotation) {
        System.err.println("test" + constraintAnnotation);
    }
    /**
    具体校验代码
    */
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintContext) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        simpleDateFormat.setLenient(false);
        boolean isValid = true;
        try {
            simpleDateFormat.parse(value);
        } catch (Exception e) {
            isValid = false;
        }
        // 如果校验失败,设置自定义错误信息
        if ( !isValid ) {
            constraintContext.disableDefaultConstraintViolation();
            constraintContext.buildConstraintViolationWithTemplate(
                    "{com.zxd.interview.valid.annotation." +
                            "Time.message}"
            )
            .addConstraintViolation();//很重要的一步,需要将自定义的信息提示模板加入
        }
        return isValid;
    }
}
复制代码


有的同学可能会好奇对于ConstraintValidator接口作用,英文进行机翻之后的内容如下:


public interface ConstraintValidator<A extends Annotation, T> {
  /**
        初始化验证器,为isValid(Object, ConstraintValidatorContext)调用做准备。传递给定约束声明的约束注释。
        保证在使用此实例进行验证之前调用此方法。
        默认的实现是no-op。
   */
  default void initialize(A constraintAnnotation) {
  }
  /**
   * 实现验证逻辑,值的状态不能被改变。
        该方法可以并发访问,实现必须确保线程安全。
        @value:被校验的值
        @ConstraintValidatorContext 校验上下文
   */
  boolean isValid(T value, ConstraintValidatorContext context);
}
复制代码


注意上面提到的线程安全问题,一定要保证实现的结果是线程安全的,否则校验可能会带来并发的问题。


做完上面这些步骤之后,只需要将注解应用到对应的对象上面,在请求的时候就可以进行对应的参数校验了:


public class Product {
    @NotBlank
    private String name;
  // 自己定义的注解
    @Time
    private String time;
    // 省略get/set
}
复制代码


使用 Validator 校验:


有时候我们想在代码里面手动进行注解校验,JSR-303也考虑到了这一点,下面介绍一下使用Validator要如何校验,简单的使用可以使用Validation.buildDefaultValidatorFactory()获取ValidatorFactory,通过factory.getValidator()获取对应的校验器Validator


@Override
public void doSomething(Product product) {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    Set<ConstraintViolation<Product>> validate = validator.validate(product);
    Map<Object, Object> objectObjectMap = new HashMap<>();
    validate.forEach(item -> {
        //            System.err.println("item = "+ item);
        String message = item.getMessage();
        //            System.err.println("message " + message);
        objectObjectMap.put(item.getPropertyPath(), message);
    });
    objectObjectMap.forEach((k, v) -> {
        System.err.println("key = " + k + " value = " + v);
    });
}
复制代码


为了更方便的使用校验,减少重复代码,下面构建一个工具类:


public class ValidateUtils {
    private static Validator validator =
            Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .buildValidatorFactory()
            .getValidator();
    private static final SmartValidator validatorAdapter = new SpringValidatorAdapter(validator);
    public static Validator getValidator() {
        return validator;
    }
    private static SmartValidator getValidatorAdapter(Validator validator) {
        return validatorAdapter;
    }
    /**
     * 校验参数,用于普通参数校验 [未测试!]
     *
     * @param
     */
    public static void validateParams(Object... params) {
        Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(params);
        if (!constraintViolationSet.isEmpty()) {
            throw new ConstraintViolationException(constraintViolationSet);
        }
    }
    /**
     * 校验对象
     *
     * @param object
     * @param groups
     * @param <T>
     */
    public static <T> void validate(T object, Class<?>... groups) {
        Set<ConstraintViolation<T>> constraintViolationSet = validator.validate(object, groups);
        if (!constraintViolationSet.isEmpty()) {
            throw new ConstraintViolationException(constraintViolationSet);
        }
    }
    /**
     * 校验对象
     * 使用与 Spring 集成的校验方式。
     *
     * @param object 待校验对象
     * @param groups 待校验的组
     * @throws BindException
     */
    public static <T> void validateBySpring(T object, Class<?>... groups)
            throws BindException {
        DataBinder dataBinder = getBinder(object);
        dataBinder.validate((Object[]) groups);
        if (dataBinder.getBindingResult().hasErrors()) {
            throw new BindException(dataBinder.getBindingResult());
        }
    }
    private static <T> DataBinder getBinder(T object) {
        DataBinder dataBinder = new DataBinder(object, ClassUtils.getShortName(object.getClass()));
        dataBinder.setValidator(getValidatorAdapter(validator));
        return dataBinder;
    }
}
复制代码


上面的工具类代码来源于文章:www.jianshu.com/p/2432d0f51…

相关文章
|
前端开发 安全 Java
一文弄懂spring validate
校验参数在以前基本都是使用大量的if/else,稍微方便一点的可以使用反射+自定义注解的形式,但是复用性不是很好,并且每个人对于的自定义注解有着自己的使用习惯,不过好在spring开发了validated框架用于注解校验,可以节省很多的校验ifelse代码,这篇文章通篇介绍了如何使用spring validated。
332 0
|
Java Spring
第5章—构建Spring Web应用程序—关于spring中的validate注解后台校验的解析
关于spring中的validate注解后台校验的解析 在后台开发过程中,对参数的校验成为开发环境不可缺少的一个环节。比如参数不能为null,email那么必须符合email的格式,如果手动进行if判断或者写正则表达式判断无意开发效率太慢,在时间、成本、质量的博弈中必然会落后。
1203 0
|
Java Spring
关于spring中的validate注解后台校验的解析
在后台开发过程中,对参数的校验成为开发环境不可缺少的一个环节。比如参数不能为null,email那么必须符合email的格式,如果手动进行if判断或者写正则表达式判断无意开发效率太慢,在时间、成本、质量的博弈中必然会落后。
1215 0
|
安全 Java 数据库连接
Spring Boot ConfigurationProperties validate
使用@Query可以在自定义的查询方法上使用@Query来指定该方法要执行的查询语句,比如:@Query("select o from UserModel o where o.uuid=?1")public List findByUuidOrAge(int uuid);注意:1:方法的参数个数...
1667 0
|
1月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
2月前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。
|
2月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
99 0
|
1天前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1天前
|
XML Java 关系型数据库
springboot 集成 mybatis-plus 代码生成器
本文介绍了如何在Spring Boot项目中集成MyBatis-Plus代码生成器,包括导入相关依赖坐标、配置快速代码生成器以及自定义代码生成器模板的步骤和代码示例,旨在提高开发效率,快速生成Entity、Mapper、Mapper XML、Service、Controller等代码。
springboot 集成 mybatis-plus 代码生成器