一 Hibernate validator是什么
验证数据是贯穿整个应用层(从表示层到持久层)的常见任务。通常在每一层中都需要实现相同的验证逻辑,这样既耗时又容易出错。为了避免这些验证的重复,开发认原经常将验证逻辑直接捆绑到Model域中,将域类与验证代码(实际上是关于类本身的元数据)混在一起。
Jakarta Bean Validation 3.0定义了用于实体和方法验证的元数据模型和API。默认的元数据源是注释,能够通过使用XML覆盖和扩展元数据。API没有绑定到特定的应用程序层或编程模型。它特别不局限于web层或持久性层,并且可用于服务器端应用程序编程以及大量的客户端Swing应用程序开发人员。
Hibernate Validator是Jakarta Bean Validation的实现。解决了业务代码中多次出现if校验使得代码臃肿的问题,可以让业务代码和小样逻辑分开,不在编写重复的校验逻辑。
二 常用的内置注解
下面是Jakarta Bean Validation API中指定的所有校验的列表。所有这些校验都应用于字段/属性级别,Jakarta Bean验证规范中没有定义类级别的校验。如果您正在使用Hibernate对象-关系映射器,那么在为模型创建DDL时需要考虑一些校验(请参阅“Hibernate元数据的影响”)。
注解 | 数据类型 | 说明 |
@AssertFalse | Boolean, boolean | 验证注解的元素值是false |
@AssertTrue | Boolean, boolean | 验证注解的元素值是true |
@DecimalMax(value=x) | BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@DecimalMin(value=x) | BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 | 验证注解的元素值小于等于@ DecimalMin指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 | 验证注解的元素值的整数位数和小数位数上限 |
@Future | java.util.Date ,java.util.Calendar , java.time.Instant , java.time.LocalDate , java.time.LocalDateTime , java.time.LocalTime , java.time.MonthDay , java.time.OffsetDateTime , java.time.OffsetTime , java.time.Year , java.time.YearMonth , java.time.ZonedDateTime , java.time.chrono.HijrahDate , java.time.chrono.JapaneseDate , java.time.chrono.MinguoDate , java.time.chrono.ThaiBuddhistDate ; Additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant | 验证注解的元素值(日期类型)比当前时间晚 |
@Max(value=x) | BigDecimal,BigInteger,byte,short,int,long和原始类型的相应包装。HV额外支持:CharSequence的任何子类型(评估字符序列表示的数字值),Number的任何子类型。 | 验证注解的元素值小于等于@Max指定的value值 |
@Min(value=x) | BigDecimal,BigInteger,byte,short,int,long和原始类型的相应包装。HV额外支持:CharSequence的任何子类型(评估char序列表示的数值),Number的任何子类型。 | 验证注解的元素值大于等于@Min指定的value值 |
@NotNull | 所有类型 | 验证注解的元素值不是null |
@Null | 所有类型 | 验证注解的元素值是null |
@Past | java.util.Date ,java.util.Calendar , java.time.Instant , java.time.LocalDate , java.time.LocalDateTime , java.time.LocalTime , java.time.MonthDay , java.time.OffsetDateTime , java.time.OffsetTime , java.time.Year , java.time.YearMonth , java.time.ZonedDateTime , java.time.chrono.HijrahDate , java.time.chrono.JapaneseDate , java.time.chrono.MinguoDate , java.time.chrono.ThaiBuddhistDate ; ,则由HV附加支持:ReadablePartial和ReadableInstant的任何实现。 | 验证注解的元素值(日期类型)比当前时间早 |
@Pattern(regex=正则表达式, flag=) | CharSequence | 验证注解的元素值与指定的正则表达式匹配 |
@Size(min=最小值, max=最大值) | 字符串,集合,映射和数组。HV额外支持:CharSequence的任何子类型。 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Valid | Any non-primitive type(引用类型) | 验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象 |
@NotEmpty | CharSequence,Collection, Map and Arrays | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | CharSequence, Collection, Map and Arrays, BigDecimal, BigInteger, CharSequece, byte, short, int, long以及原始类型各自的包装 | 验证注解的元素值在最小值和最大值之间 |
@NotBlank | CharSequence | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@Length(min=下限, max=上限) | CharSequence | 验证注解的元素值长度在min和max区间内 |
CharSequence | 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
三,自定义Hibernate Validator校验注解
自定义Hibernate Validator校验注解,首先它是注解,所以我们先来看看Spring Boot如何自定义注解。
3.1 Spring Boot自定义注解
3.1.1 元注解
自定义注解,需要用到元注解,元注解就是注解注解的注解。常用的元注解包括:
- Target:描述了注解的对象范围,取值在java.lang.annotation.ElementType定义,常用的包括:
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述方法变量
- TYPE:用于描述类、接口或enum类型
- FIELD:用来描述字段
- Retention:表示注解保留时间长短。取值在java.lang.annotation.RetentionPolicy中,取值为:
- SOURCE:在源文件中有效,编译过程中会被忽略
- CLASS:随源文件一起编译在class文件中,运行时忽略
- RUNTIME:在运行时有效,可以通过反射获取到
- Documented:是java在生成文档时,是否显示注解的开关。
只有定义为RetentionPolicy.RUNTIME时,我们才能通过注释反射获取到注释。
3.1.2 反射获取注解
可以通过反射来获取注解。假设我们要自定义一个注解,它用在字段上,并且可以通过反射获取到,功能是用来描述字段的长度和作用,可以定义如下:
- 定义注解:
@Target(ElementType.FIELD) //注解用于字段上
@Retention(RetentionPolicy.RUNTIME) //保留到运行时,可通过注解获取
public @interface MyField{
String description();
int length();
}
- 反射获取注解
public class MyFieldTest{
//使用我们的自定义注解
@MyField(description = "用户名", length = 12)
private String username;
@Test
public void testMyField(){
//获取类模板
Class c = MyFieldTest.class;
//获取所有字段
for(Field f : c.getDeclaredFields()){
//判断这个字段是否有MyField注解
if(f.isAnnotationPresent(MyField.class)){
MyFielld annotation = f.getAnnotation(MyFiled.class);
System.out.println("字段:[" + f.getName() + "], 描述:[" + annotation.description() + "], 长度:[" + annotation.length() +"]");
}
}
}
}
3.1.3 AOP使用自定义注解
自定义注解一般都是配合AOP拦截器使用,因此使用步骤为
- 定义注解
package com.****.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
- 实现切面类,用@Aspect来实现。定义切面并实现相关功能
package com.****(项目名称).demo.annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class MyAnnotationImpl {
@Pointcut("@annotation(com.****.demo.annotation.MyAnnotation)")
public void myPointcut(){}
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("......this is before around.....");
Object ret = joinPoint.proceed();
System.out.println("......this is after around.....");
return ret;
}
@Before(value = "myPointcut()")
public void before() throws Throwable {
System.out.println("******this is before function******");
}
@After(value = "myPointcut()")
public void after() throws Throwable {
System.out.println("******this is after function******");
}
}
- Controller查看效果
package com.****.demo.controller;
import com.****.demo.annotation.MyAnnotation;
import com.****.demo.common.CallResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/Annotation")
public class AnnotationController {
@PostMapping("/AnnotationAop")
@MyAnnotation
public CallResult AnnotationAop(){
System.out.println("注解测试成功");
return CallResult.ok();
}
}
- 运行结果:
注解切面已加载成功。
3.2 自定义Hibernate Validator注解校验
Jakarta Bean Validation API定义了如@NotNull, @Size等一整套标准校验注解。如果这些内置校验还不够,您可以根据特定的验证需求轻松创建定制校验。
自定义注解校验需要义下步骤:
- 创建一个校验注解(creating a constraint annotation)
- 实现一个校验器(Implement a validator)
- 定义一个默认的错误信息(Define a default error message)
本例定义一个自定义校验注释,来判断所注释的字符串是否全为大写或全为小写。
3.2.1 步骤一,首先需要的是一种表达两种case模式的方法。虽然你可以使用String常量,但更好的方法是使用枚举:
package com.****.demo.common;
public enum CaseMode {
UPPER,
LOWER;
}
3.2.2 步骤二,定义校验注解
package com.****.demo.annotation;
import com.****.demo.common.CaseMode;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
@Repeatable(List.class)
public @interface CheckCase {
String message() default "这里是默认错误消息,默认的!默认的!默认的!";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
CaseMode value();
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
CheckCase[] value();
}
}
注释类型使用@interface关键字定义。注释类型的所有属性都以类似方法(method-like manner)的方式声明。Jakarta Bean Validation API的规范要求所有校验注解定义:
- 一个默认的message属性,用于在校验被违反时返回错误消息
- 一个groups属性,允许定义分组校验,它必须默认为Class<?>类型的空数组。
- 一个payload属性,用户可以将用户自定义的对象分配给校验。这个属性不是API自己用的。一个用户自定义的payload例子如下,定义一个Severity类:
public class Severity {
public interface Info extends Payload {
}
public interface 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;
// ...
}
现在,客户端可以在经过ContactDetails的实例验证之后,通过ConstraintViolation.getConstraintDescriptor().getPayload()进入一个校验的severity,并通过severity来调整其行为。
除了这三个强制属性外,还有一个value属性,允许指定所需的case模式。value值是一个特殊的值,如果它是唯一指定的属性,当使用注解时可以省略,例如:@CheckCase(CaseMode.UPPER)。
此外,校验注解还使用了一些元注解:
- @Target:用于指定注解的作用范围,包括类、方法、字段等
- @Retention:指定改元注解的保留策略,包括source,class和runtime
- Constraint(validatedBy = CheckCaseValidator.class):将注释类型标记为校验注释,并指定用于验证用@CheckCase注解的元素的验证器。如果校验可以用于多个数据类型,则可以指定多个验证器,每个验证器对应一个数据类型。通过校验器返回的结果(true/false)来判断是否抛出异常信息。
- @Repeatable(List.class):指示注释可以在同一位置重复多次,通常使用不同的配置。List是包含注解类型(containing annotation type)。
示例中也显示了名为List的包含注释类型。它允许在同一个元素上指定多个@CheckCase注释,例如使用不同的验证组和消息。虽然可以使用其他名称,但Jakarta Bean Validation规范建议使用名称List,并使注释成为相应校验类型的内部注释。
3.2.3 校验器
定义注解之后,需要创建一个实现ConstraintValidator接口的校验器用来校验带有@CheckCase注解的元素:
package com.****.demo.common;
import com.****.demo.annotation.CheckCase;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
@Override
public void initialize(CheckCase constraintAnnotation) {
this.caseMode = constraintAnnotation.value();
}
// value:待检验的字符串
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if ( value == null ) {
return true;
}
if ( caseMode == CaseMode.UPPER ){
return value.equals( value.toUpperCase() );
} else {
return value.equals( value.toLowerCase() );
}
}
}
试一下:
在User.demoName上加上注释@CheckCase(CaseMode.LOWER)
@Data
@Component
@ConfigurationProperties(prefix = "user")
public class User implements Serializable {
@ApiModelProperty(value = "名称")
@NotBlank(message = "name不能为空(哈哈,自定义哒)")
@CheckCase(CaseMode.LOWER)
private String demoName;
@ApiModelProperty(value = "年龄")
@NotNull(message = "age 不能为空")
private Integer age;
@ApiModelProperty(value = "id")
@NotNull(message = "id 不能为空")
private Integer id;
}
demoName大写输入"UPPER”
错误显示:
ConstraintValidator接口定义了输入两个参数类型。第一个定义了需要校验的注解的类型(这里是ChecCase),第二个定义了是校验器能处理的参数类型(这里是String)。如果一个校验器可以支持多种数据类型,则每种数据类型都需要一个ConstraintValidator并像上面的方法进行实现和注册。
isValid()方法则包含了实际的校验逻辑。@CheckCase是用来校验相应的字符串是否为全大写或全小写的字符串,根据initialize()中定义的caseMode来判断。注意,Jakarata Bean Validation 建议null是有效的(即返回true)。如果null不是有效数据,则需要加上注解@NotNull。
ConstraintValidatorContext: 上例CheckCaseValidator校验器的实现中,isValid()方法只返回了true和false,使用了默认的error message。可以通过传入的ConstraintValidatorContext对象来增加客户自定义的error message或完全禁用默认错误消息,只定义自定义错误消息,如下例:
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
@Override
public void initialize(CheckCase constraintAnnotation) {
this.caseMode = constraintAnnotation.value();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintContext) {
if ( value == null ) {
return true;
}
boolean isValid;
if ( caseMode == CaseMode.UPPER ){
isValid = value.equals( value.toUpperCase() );
} else {
isValid = value.equals( value.toLowerCase() );
}
if ( !isValid ) {
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(
"这里是我新定义的错误消息!!!!"
)
.addConstraintViolation();
}
return isValid;
}
}
试一下,输入给成大写的“UPPER”
错误显示:
Nice~完美!