Hibernate validator注解及Spring Boot自定义Hibernate Validator注解校验(超级详细)

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元数据的影响”)。

注解数据类型说明
@AssertFalseBoolean, boolean验证注解的元素值是false
@AssertTrueBoolean, 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的任何子类型。验证注解的元素值的整数位数和小数位数上限
@Futurejava.util.Date,java.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimejava.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.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
@Pastjava.util.Date,java.util.Calendarjava.time.Instantjava.time.LocalDatejava.time.LocalDateTimejava.time.LocalTimejava.time.MonthDayjava.time.OffsetDateTimejava.time.OffsetTimejava.time.Yearjava.time.YearMonthjava.time.ZonedDateTimejava.time.chrono.HijrahDatejava.time.chrono.JapaneseDatejava.time.chrono.MinguoDatejava.time.chrono.ThaiBuddhistDate; ,则由HV附加支持:ReadablePartial和ReadableInstant的任何实现。验证注解的元素值(日期类型)比当前时间早
@Pattern(regex=正则表达式, flag=)CharSequence验证注解的元素值与指定的正则表达式匹配
@Size(min=最小值, max=最大值)字符串,集合,映射和数组。HV额外支持:CharSequence的任何子类型。验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Valid 
Any non-primitive type(引用类型)
验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象
@NotEmptyCharSequence,Collection, Map and Arrays验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值)CharSequence, Collection, Map and Arrays, BigDecimal, BigInteger, CharSequece, byte, short, int, long以及原始类型各自的包装验证注解的元素值在最小值和最大值之间
@NotBlankCharSequence验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Length(min=下限, max=上限)CharSequence验证注解的元素值长度在min和max区间内
@EmailCharSequence验证注解的元素值是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等一整套标准校验注解。如果这些内置校验还不够,您可以根据特定的验证需求轻松创建定制校验。

自定义注解校验需要义下步骤:

  1. 创建一个校验注解(creating a constraint annotation)
  2. 实现一个校验器(Implement a validator)
  3. 定义一个默认的错误信息(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~完美!

        

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值