Hibernate数据校验简介
我们在业务中经常会遇到参数校验问题,比如前端参数校验、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; |
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注解包含以下几个内容:
- message:错误消息,示例中的是错误码,可以根据国际化翻译成不同的语言。
- groups: 分组校验,不同的分组可以有不同的校验条件,比如同一个DTO用于create和update时校验条件可能不一样。
- 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
本文最先发布至微信公众号,版权所有,禁止转载!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
破解数据匮乏现状:纵向联邦学习场景下的逻辑回归(LR)
摘要:主要介绍了华为云可信智能计算服务(TICS)采用的纵向联邦逻辑回归(LR)方案。 本文分享自华为云社区《纵向联邦学习场景下的逻辑回归(LR)》,作者: 汽水要加冰。 海量训练数据是人工智能技术在各个领域成功应用的重要条件。例如,计算机视觉和商务经融推荐系统中的 AI 算法都依靠大规模标记良好的数据才能获得较好的推理效果。然而在医疗、银行以及一些政务领域中,行业内对数据隐私的保护越来越强,造成可用数据严重匮乏的现状。针对上述问题,华为云可信智能计算服务( TICS)专为打破银行、政企等行业的数据壁垒,实现数据安全共享,设计了多方联邦学习方案。 一、什么是逻辑回归? 回归是描述自变量和因变量之间相互依赖关系的统计分析方法。线性回归作为一种常见的回归方法,常用作线性模型(或线性关系)的拟合。 逻辑回归(logistic regression)虽然也称为回归,却不是一种模型拟合方法,而是一种简单的“二分类”算法。具有实现简单,算法高效等诸多优点。 1.1 线性回归(linear regression) 图1.1、1.2分别表示二维和三维线性回归模型,图1.1的拟合直接(蓝线)可表示为 y...
- 下一篇
如何利用 JuiceFS 的性能工具做文件系统分析和调优
JuiceFS 是一款面向云原生环境设计的高性能 POSIX 文件系统,在 AGPL v3.0 开源协议下发布。作为一个云上的分布式文件系统,任何存入 JuiceFS 的数据都会按照一定规则拆分成数据块存入对象存储(如 Amazon S3),相对应的元数据则持久化在独立的数据库中。这种结构决定了 JuiceFS 的存储空间可以根据数据量弹性伸缩,可靠地存储大规模的数据,同时支持在多主机之间共享挂载,实现跨云跨地区的数据共享和迁移。 从 v0.13 发布以来, JuiceFS 新增了多项与性能监测和分析相关的功能,从某种程度上说,开发团队希望 JuiceFS 既能提供大规模分布式计算场景下的出色性能,也能广泛地覆盖更多日常的使用场景。 本文我们从单机应用入手,看依赖单机文件系统的应用是否也可以用在 JuiceFS 之上,并分析它们的访问特点进行针对性的调优。 测试环境 接下来的测试我们会在同一台亚马逊云服务器上进行,配置情况如下: 服务器配置:Amazon c5d.xlarge: 4 vCPUs, 8 GiB 内存, 10 Gigabit 网络, 100 GB SSD JuiceFS:使...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Windows10,CentOS7,CentOS8安装Nodejs环境
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长