我们在业务中经常会遇到参数校验问题,比如前端参数校验、Kafka消息参数校验等,如果业务逻辑比较复杂,各种实体比较多的时候,我们通过代码对这些数据一一校验,会出现大量的重复代码以及和主要业务无关的逻辑。Spring MVC提供了参数校验机制,但是其底层还是通过Hibernate进行数据校验,所以有必要去了解一下Hibernate数据校验和JSR数据校验规范。

JSR数据校验规范

Java官方先后发布了JSR303与JSR349提出了数据合法性校验提供的标准框架:BeanValidator,BeanValidator框架中,用户通过在Bean的属性上标注类似于@NotNull、@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。

JSR注解列表

JSR标准中的数据校验注解如下所示:

注解名 注解数据类型 注解作用 示例
AssertFalse boolean/Boolean 被注释的元素必须为False @AssertFalse private boolean success;
AssertTrue boolean/Boolean 被注释的元素必须为True @AssertTrue private boolean success;
DecimalMax BigDecimal/BigInteger/CharSequence/byte/short/int/long及其包装类 被注释的值应该小于等于指定的最大值 @DecimalMax("10") private BigDecimal value;
DecimalMin BigDecimal/BigInteger/CharSequence/byte/short/int/long及其包装类 被注释的值应该大于等于指定的最小值 @DecimalMin("10") private BigDecimal value;
Digits BigDecimal/BigInteger/CharSequence/byte/short/int/long及其包装类 integer指定整数部分最大位数,fraction指定小数部分最大位数 @Digits(integer = 10,fraction = 4) private BigDecimal value;
Email CharSequence 字符串为合法的邮箱格式 @Email private String email;
Future java中的各种日期类型 指定日期应该在当期日期之后 @Future private LocalDateTime future;
FutureOrPresent java中的各种日期类型 指定日期应该为当期日期或当期日期之后 @FutureOrPresent private LocalDateTime futureOrPresent;
Max BigDecimal/BigInteger/byte/short/int/long及包装类 被注释的值应该小于等于指定的最大值 @Max("10") private BigDecimal value;
Min BigDecimal/BigInteger/byte/short/int/long及包装类 被注释的值应该大于等于指定的最小值 @Min("10") private BigDecimal value;
Negative BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 被注释的值应该是负数 @Negative private BigDecimal value;
NegativeOrZero BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 被注释的值应该是0或者负数 @NegativeOrZero private BigDecimal value;
NotBlank CharSequence 被注释的字符串至少包含一个非空字符 @NotBlank private String noBlankString;
NotEmpty CharSequence/Collection/Map/Array 被注释的集合元素个数大于0 @NotEmpty private List<string> values;
NotNull any 被注释的值不为空 @NotEmpty private Object value;
Null any 被注释的值必须空 @Null private Object value;
Past java中的各种日期类型 指定日期应该在当期日期之前 @Past private LocalDateTime past;
PastOrPresent java中的各种日期类型 指定日期应该在当期日期或之前 @PastOrPresent private LocalDateTime pastOrPresent;
Pattern CharSequence 被注释的字符串应该符合给定得到正则表达式 @Pattern(\d*) private String numbers;
Positive BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 被注释的值应该是正数 @Positive private BigDecimal value;
PositiveOrZero BigDecimal/BigInteger/byte/short/int/long/float/double及包装类 被注释的值应该是正数或0 @PositiveOrZero private BigDecimal value;
Size CharSequence/Collection/Map/Array 被注释的集合元素个数在指定范围内 @Size(min=1,max=10) private List<string> values;

JSR注解内容

我们以常用的比较简单的@NotNull注解为例,看看注解中都包含那些内容,如下边的源码所示,可以看到@NotNull注解包含以下几个内容:

  1. message:错误消息,示例中的是错误码,可以根据国际化翻译成不同的语言。
  2. groups: 分组校验,不同的分组可以有不同的校验条件,比如同一个DTO用于create和update时校验条件可能不一样。
  3. payload:BeanValidation API的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用.
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; /**
* Defines several {@link NotNull} annotations on the same element.
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List { NotNull[] value();
}
}

错误消息message、分组group这些功能我们程序中使用比较多,在我介绍Spring Validator数据校验的文章中有详细说明,但是关于payload我们接触的比较少,下面我们举例说明以下payload的使用,下面的示例中,我们用payload来标识数据校验失败的严重性,通过以下代码。在校验完一个ContactDetails的示例之后, 你就可以通过调用ConstraintViolation.getConstraintDescriptor().getPayload()来得到之前指定到错误级别了,并且可以根据这个信息来决定接下来到行为.

public class Severity {
public static class Info extends Payload {};
public static class Error extends Payload {};
} public class ContactDetails {
@NotNull(message="Name is mandatory", payload=Severity.Error.class)
private String name; @NotNull(message="Phone number not specified, but not mandatory", payload=Severity.Info.class)
private String phoneNumber; // ...
}

JSR校验接口

通过前面的JSR校验注解,我们可以给某个类的对应字段添加校验条件,那么怎么去校验这些校验条件呢?JSR进行数据校验的核心接口是Validation,该接口的定义如下所示,我们使用比较多的接口应该是<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);,该方法可以用于校验某个Object是否符合指定分组的校验规则,如果不指定分组,那么只有默认分组的校验规则会生效。

public interface Validator {

	/**
* Validates all constraints on {@code object}.
*/
<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups); /**
* Validates all constraints placed on the property of {@code object}
* named {@code propertyName}.
*/
<T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName,Class<?>... groups); /**
* Validates all constraints placed on the property named {@code propertyName}
* of the class {@code beanType} would the property value be {@code value}.
*/
<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups); /**
* Returns the descriptor object describing bean constraints.
* The returned object (and associated objects including
* {@link ConstraintDescriptor}s) are immutable.
*/
BeanDescriptor getConstraintsForClass(Class<?> clazz); /**
* Returns an instance of the specified type allowing access to
* provider-specific APIs.
* <p>
* If the Jakarta Bean Validation provider implementation does not support
* the specified class, {@link ValidationException} is thrown.call
*/
<T> T unwrap(Class<T> type); /**
* Returns the contract for validating parameters and return values of methods
* and constructors.
*/
ExecutableValidator forExecutables();
}

Hibernate数据校验

基于JSR数据校验规范,Hibernate添加了一些新的注解校验,然后实现了JSR的Validator接口用于数据校验。

Hibernate新增注解

注解名 注解数据类型 注解作用 示例
CNPJ CharSequence 被注释的元素必须为合法的巴西法人国家登记号 @CNPJ private String cnpj;
CPF CharSequence 被注释的元素必须为合法的巴西纳税人注册号 @CPF private String cpf;
TituloEleitoral CharSequence 被注释的元素必须为合法的巴西选民身份证号码 @TituloEleitoral private String tituloEleitoral;
NIP CharSequence 被注释的元素必须为合法的波兰税号 @NIP private String nip;
PESEL CharSequence 被注释的元素必须为合法的波兰身份证号码 @PESEL private String pesel;
REGON CharSequence 被注释的元素必须为合法的波兰区域编号 @REGON private String regon;
DurationMax Duration 被注释的元素Duration的时间长度小于指定的时间长度 @DurationMax(day=1) private Duration duration;
DurationMin Duration 被注释的元素Duration的时间长度大于指定的时间长度 @DurationMin(day=1) private Duration duration;
CodePointLength CharSequence 被注释的元素CodPoint数目在指定范围内,unicode中每一个字符都有一个唯一的识别码,这个码就是CodePoint。比如我们要限制中文字符的数目,就可以使用这个 @CodePointLength(min=1) private String name;
ConstraintComposition 其它数据校验注解 组合注解的组合关系,与或等关系 ---
CreditCardNumber CharSequence 用于判断一个信用卡是不是合法格式的信用卡 @CreditCardNumber private String credictCardNumber;
Currency CharSequence 被注释的元素是指定类型的汇率 @Currency(value = {"USD"}) private String currency;
ISBN CharSequence 被注释的元素是合法的ISBN号码 @ISBN private String isbn;
Length CharSequence 被注释的元素是长度在指定范围内 @Length(min=1) private String name;
LuhnCheck CharSequence 被注释的元素可以通过Luhn算法检查 @LuhnCheck private String luhn;
Mod10Check CharSequence 被注释的元素可以通过模10算法检查 @Mod10Check private String mod10;
ParameterScriptAssert 方法 参数脚本校验 ————
ScriptAssert 类脚本校验 ————
UniqueElements 集合 集合中的每个元素都是唯一的 @UniqueElements private List<String> elements;

Hibiernate数据校验

如何使用Hibernate进行数据校验呢?我们知道JSR规定了数据校验的接口Validator,Hibernate用ValidatorImpl类中实现了Validator接口,我们可以通过Hibernate提供的工厂类HibernateValidator.buildValidatorFactory创建一个ValidatorImpl实例。使用Hibernate创建一个Validator实例的代码如下所示。

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

Hibernate校验源码

通过上面的内容,我们知道Hibernate可以用工厂方法实例化一个Validator接口的实例,这个实例可以用于带有校验注解的校验JavaBean,那么Hibernate底层是如何实现这些校验逻辑的呢?我们以如下JavaBean为例,解析Hibernate校验的源码。

@Data
public class Person { @NotBlank
@Size(max=64)
private String name; @Min(0)
@Max(200)
private int age;
}

ConstraintValidator介绍

ConstraintValidator是Hibernate中数据校验的最细粒度,他可以校验指定注解和类型的数值是否合法。比如上面例子中的@Max(200)private int age;,对于age字段的校验就会使用一个叫MaxValidatorForInteger的ConstraintValidator,这个ConstraintValidator在校验的时候会判断指定的数值是不是大于指定的最大值。

public class MaxValidatorForInteger extends AbstractMaxValidator<Integer> {

	@Override
protected int compare(Integer number) {
return NumberComparatorHelper.compare( number.longValue(), maxValue );
}
} public abstract class AbstractMaxValidator<T> implements ConstraintValidator<Max, T> { protected long maxValue; @Override
public void initialize(Max maxValue) {
this.maxValue = maxValue.value();
} @Override
public boolean isValid(T value, ConstraintValidatorContext constraintValidatorContext) {
// null values are valid
if ( value == null ) {
return true;
} return compare( value ) <= 0;
} protected abstract int compare(T number);
}

ConstraintValidator初始化

我们在前面的内容中说到Hibernate提供了ValidatorImpl用于数据校验,那么ValidatorImpl和ConstraintValidator是什么关系呢,简单来说就是ValidatorImpl在初始化的时候会初始化所有的ConstraintValidator,在校验数据的过程中调用这些内置的ConstraintValidator校验数据。内置ConstraintValidator的对应注解的@Constraint(validatedBy = { })是空的。

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { }) // 这儿是空的
public @interface AssertFalse { String message() default "{javax.validation.constraints.AssertFalse.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; /**
* Defines several {@link AssertFalse} annotations on the same element.
*
* @see javax.validation.constraints.AssertFalse
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List { AssertFalse[] value();
}
}

自定义ConstraintValidator

如果Hibernate和JSR中的注解不够我用,我需要自定义一个注解和约束条件,我们应该怎么实现呢。实现一个自定义校验逻辑一共分两步:1.注解的实现。2.校验逻辑的实现。比如我们需要一个校验字段状态的注解,我们可以使用以下示例定义一个注解:

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = StatusValidator.class)
@Documented
public @interface ValidStatus {
String message() default "状态错误 ";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* 有效的状态值集合,默认{1,2}
*/
int[] value() default {1,2};
}

实现了注解之后,我们需要实现注解中的@Constraint(validatedBy = StatusValidator.class),示例代码如下:

/**
* 校验状态是否属于指定状态集
(ConstraintValidator后指定的泛型对象类型为
注解类和注解注释的字段类型<ValidStatus, Integer>)
*/
public class StatusValidator implements ConstraintValidator<ValidStatus, Integer> {
private Integer[] validStatus; @Override
public void initialize(ValidStatus validStatus) {
int[] ints = validStatus.value();
int n = ints.length;
Integer[] integers = new Integer[n];
for (int i = 0; i < n; i++) {
integers[i] = ints[i];
}
this.validStatus = integers;
} @Override
public boolean isValid(Integer n, ConstraintValidatorContext constraintValidatorContext) {
List<Integer> status = Arrays.asList(validStatus);
if (status.contains(n)) {
return true;
}
return false;
}
}

Validator的特性

四种约束级别

成员变量级别的约束

约束可以通过注解一个类的成员变量来表达。如下代码所示:

@Data
public class Person { @NotBlank
@Size(max=64)
private String name; @Min(0)
@Max(200)
private int age;
}

属性约束

如果你的模型类遵循javabean的标准,它也可能注解这个bean的属性而不是它的成员变量。关于JavaBean的介绍可以看我的另外一篇博客。

@Data
public class Person { private String name; @Min(0)
@Max(200)
private int age; @NotBlank
@Size(max=64)
public String getName(){
return name;
}
}

集合约束

通过在约束注解的@Target注解在约束定义中指定ElementType.TYPE_USE,就可以实现对容器内元素进行约束

类级别约束

一个约束被放到类级别上,在这种情况下,被验证的对象不是简单的一个属性,而是一个完整的对象。使用类级别约束,可以验证对象几个属性之间的相关性,比如不允许所有字段同时为null等。

@Data
@NotAllFieldNull
public class Person { private String name; @Min(0)
@Max(200)
private int age; @NotBlank
@Size(max=64)
public String getName(){
return name;
}
}

校验注解的可继承性

父类中添加了约束的字段,子类在进行校验时也会校验父类中的字段。

递归校验

假设我们上面例子中的Person多了一个Address类型的字段,并且Address也有自己的校验,我们怎么校验Address中的字段呢?可以通过在Address上添加@Valid注解实现递归校验。

@Data
public class Person { private String name; @Min(0)
@Max(200)
private int age; @Valid
public Address address;
} @Data
public class Address{ @NotNull
private string city;
}

方法参数校验

我们可以通过在方法参数中添加校验注解,实现方法级别的参数校验,当然这些注解的生效需要通过一些AOP实现(比如Spring的方法参数校验)。


public void createPerson(@NotNull String name,@NotNull Integer age){ }

方法参数交叉校验

方法也支持参数之间的校验,比如如下注解不允许创建用户时候用户名和年龄同时为空,注解校验逻辑需要自己实现。交叉校验的参数是Object[]类型,不同参数位置对应不同的Obj。

@NotAllPersonFieldNull
public void createPerson( String name,Integer age){ }

方法返回值校验

public @NotNull Person getPerson( String name,Integer age){
return null;
}

分组功能

我在另一篇介绍Spring校验注解的文章中说过,在Spring的校验体系中,@Valid注解不支持分组校验,@Validated注解支持分组校验。 事实上这并不是JSR注解中的@Valid不支持分组校验,而是Spring层面把@Valid注解的分组校验功能屏蔽了。

所以原生的JSR注解和Hibernate校验都支持分组校验功能,具体校验逻辑可以参考我有关Spring数据校验的文章。

分组继承

我们知道JSR分组校验功能是使用注解中的group字段,group字段存储了分组的类别,那么如果分组的类之间有继承关系,分组校验会被继承吗?答案是会的。

分组顺序

如果我们在校验的过程中需要指定校验顺序,那么我们可以给校验条件分组,分组之后就会按照顺序校验对象中的各个属性。

GroupSequence({ Default.class, BaseCheck.class, AdvanceCheck.class })

public interface OrderedChecks {

}

Payload

如果我们需要在不同的情况下有不同的校验方式,比如中英文环境之类的,这种时候用分组就不是很合适了,可以考虑使用PayLoad。用户可以在初始化Validator时候指定当前环境的payload,然后在校验环节拿到环境中的payload走不同的校验流程:

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.constraintValidatorPayload( "US" )
.buildValidatorFactory(); Validator validator = validatorFactory.getValidator(); public class ZipCodeValidator implements ConstraintValidator<ZipCode, String> { public String countryCode; @Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
if ( object == null ) {
return true;
} boolean isValid = false; String countryCode = constraintContext
.unwrap( HibernateConstraintValidatorContext.class )
.getConstraintValidatorPayload( String.class ); if ( "US".equals( countryCode ) ) {
// checks specific to the United States
}
else if ( "FR".equals( countryCode ) ) {
// checks specific to France
}
else {
// ...
} return isValid;
}
}

我是御狐神,欢迎大家关注我的微信公众号:wzm2zsd

本文最先发布至微信公众号,版权所有,禁止转载!

Hibernate数据校验简介的更多相关文章

  1. JSR303的数据校验-Hibernate Validator方式实现

    1.什么是JSR303? JSR303是java为bean数据合法性校验所提供的一个标准规范,叫做Bean Validation. Bean Validation是一个运行时的数据校验框架,在验证之后 ...

  2. 使用Hibernate Validator来帮你做数据校验

    数据校验是贯穿所有应用程序层(从表示层到持久层)的常见任务.通常在每个层中实现相同的验证逻辑,这是耗时且容易出错的.这里我们可以使用Hibernate Validator来帮助我处理这项任务.对此,H ...

  3. 用spring的@Validated注解和org.hibernate.validator.constraints.*的一些注解在后台完成数据校验

    这个demo主要是让spring的@Validated注解和hibernate支持JSR数据校验的一些注解结合起来,完成数据校验.这个demo用的是springboot. 首先domain对象Foo的 ...

  4. SpringBoot 2 快速整合 | Hibernate Validator 数据校验

    概述 在开发RESTFull API 和普通的表单提交都需要对用户提交的数据进行校验,例如:用户姓名不能为空,年龄必须大于0 等等.这里我们主要说的是后台的校验,在 SpringBoot 中我们可以通 ...

  5. Spring MVC数据校验

    在web应用程序中,为了防止客户端传来的数据引发程序异常,常常需要对 数据进行验证.输入验证分为客户端验证与服务器端验证.客户端验证主要通过JavaScript脚本进行,而服务器端验证则主要通过Jav ...

  6. spring mvc 数据校验

    1.需要导入的jar包: slf4j-api-1.7.21.jar validation-api-1.0.0.GA.jar hibernate-validator-4.0.1.GA.jar 2.访问页 ...

  7. SpringMvc中的数据校验

    SpringMvc中的数据校验 Hibernate校验框架中提供了很多注解的校验,如下: 注解 运行时检查 @AssertFalse 被注解的元素必须为false @AssertTrue 被注解的元素 ...

  8. springmvc的数据校验

       springmvc的数据校验 在Web应用程序中,为了防止客户端传来的数据引发程序异常,常常需要对数据进行验证,输入验证分为客户端验证与服务器端验证. 客户端验证主要通过javaScript脚本 ...

  9. Java数据校验(Bean Validation / JSR303)

    文档: http://beanvalidation.org/1.1/spec/ API : http://docs.jboss.org/hibernate/beanvalidation/spec/1. ...

随机推荐

  1. .NET Core 基于Quartz的UI可视化操作组件 GZY.Quartz.MUI 简介

    前言 最近在用Quartz做定时任务.虽然很方便,但是Quartz自己貌似是没有UI界面的..感觉操作起来 就很难受.. 查了一下,貌似有个UI组件 不过看了一下文档..直接给我劝退了..太麻烦了 我 ...

  2. LOJ6469 Magic(trie)

    纪念我菜的真实的一场模拟赛 首先看到这个题目,一开始就很毒瘤.一定是没有办法直接做的. 我们考虑转化问题 假设,我们选择枚举\(x\),其中\(x\)是\(10\)的若干次方,那么我们只需要求有多少对 ...

  3. 【原创】C语言和C++常见误区(一)

    本文仅在博客园发布,认准原文地址:https://www.cnblogs.com/jisuanjizhishizatan/p/15414469.html 问题1:int类型占几个字节? 常见误区:占4 ...

  4. JAVA实现表达式求导运算的分析总结

    1第一次作业 1.1题目描述 对形如4*x+x^2+x的多项式求导. 1.2类图 1.3度量分析 在完成第一次作业时,我的写法没有特别的"面向对象".唯一封装起来的是Node,代表 ...

  5. Noip模拟83 2021.10.26

    T1 树上的数 有手就能在衡中$OJ$上过,但是$WaitingCoders$不行,就是这样 必须使用$O(n)$算法加上大力卡常,思路就是找子树内没更新的更新,更新过了直接$return$ 1 #i ...

  6. Linux下Zabbix5.0 LTS监控基础原理及安装部署(图文教程)

    Zabbix 是什么? zabbix 是一个基于 Web 界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案.通过 C/S 模式采集数据,通过 B/S 模式在 Web 端展示和配置,能监视 ...

  7. 对SQLServer错误使用聚集索引的优化案例(千万级数据量)

    前言: 半个月前发了文章 SQLServer聚集索引导致的插入性能低 终于等到生产环境休整半天,这篇文章是对前文的实际操作. 以下正文开始: 异常:近期发现偶尔有新数据插入超时. 分析:插入条码有多种 ...

  8. NavigationView使用简介

    Android支持直接创建带有NavigationView的Activity,这里主要介绍NavigationView的逻辑. NavigationView通常是跟DrawerLayout一起使用.D ...

  9. MVC +Jqyery+Ajax 实现弹出层提醒

    CSS部分: /*登录提示*/ * {margin: 0; padding: 0; } .layer { width: 350px; padding: 20px; background: #fff; ...

  10. 嵌入式开发板nfs挂载

    板子要开始调试了,第一个头大的问题就是调试过程中更新的文件怎么更新到板子上,以前用sd卡拷贝来来回回太浪费时间了,adb也需要接线各种连接操作. 现在板子有wifi可用,是时候把nfs共享搭起来了. ...