咱们在业务中常常会遇到参数校验问题,比方前端参数校验、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
本文最先公布至微信公众号,版权所有,禁止转载!
发表回复