Hibernate Validator:自定义注解与校验器,为枚举数据校验赋能

494 阅读5分钟

引言

数据校验在数据处理中起到保驾护航的作用,它能有效确保数据的准确性、完整性和一致性,从而为后续分析和决策提供可靠依据,避免因错误数据导致的不良后果。

本文将详细介绍一下 Hibernate Validator,并详细说说如何自定义扩展。

一、认识Hibernate Validator

image.png

1.1 什么是Hibernate Validator?

  • Hibernate Validator 是一个实现 Java Bean Validation (JSR 380) 规范的校验框架。

  • 它提供了丰富的内置校验注解,支持自定义校验逻辑,能够无缝集成到 Spring Boot 和其他 Java 框架中。

  • 截止目前为止,Hibernate Validator最新版本已经更新到了9.0.0.CR1,官方文档点这里

1.2 Java Bean Validation 规范

  • JSR 380 是 Java 的标准校验规范,定义了一套统一的校验机制。它是 Java EE 和 Java SE 的一部分,用于通过注解(如 @NotNull、@Min 和 @Max)来验证 Java Bean 的属性是否符合特定条件。JSR 380 是 JSR 303 的升级版本,需要 Java 8 或更高版本,并利用了 Java 8 的特性,例如类型注解和对 Optional 和 LocalDate 等类的支持。

  • Hibernate Validator 是该规范的参考实现,完全兼容规范定义的校验注解。

  • 通过使用 JSR 380,开发者可以更优雅地实现参数校验,避免硬编码校验逻辑,从而提高代码的可维护性和可读性。

image.png

1.3 Hibernate Validator 的优势

  • 丰富的内置注解:提供了如 @NotNull@Size@Email 等常用校验注解。

  • 灵活的自定义校验:支持自定义注解和校验器,满足复杂业务需求。

  • 集成友好:与 Spring Boot、Spring MVC 等框架无缝集成。

  • 性能高效:校验逻辑在编译时生成,运行时性能优异。

image.png

二、Hibernate Validator 的基本使用

2.1 添加依赖

  • 在 Spring Boot 项目中,Hibernate Validator 是默认集成的,无需额外添加依赖。

  • 如果是纯 Java 项目,需要手动添加依赖:

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.1.Final</version>
</dependency>

maven仓库地址:hibernate-validator

image.png

2.2 使用内置校验注解

示例代码:

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

public class User {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 50, message = "用户名长度必须在3到50之间")
    private String username;

    @Email(message = "邮箱格式不正确")
    private String email;

    // 省略 getter 和 setter 方法
}

2.3 校验触发

  • 在 Spring Boot 中,可以通过 @Valid@Validated 注解触发校验。
@RestController
public class UserController {
    @PostMapping("/user")
    public ResponseEntity<?> createUser(@Valid @RequestBody User user) {
        return ResponseEntity.ok("用户创建成功");
    }
}

2.4 校验结果处理

捕获校验异常

  • 使用 @ControllerAdvice 捕获校验异常并返回统一的错误响应。
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.badRequest().body(errors);
    }
}

三、自定义注解与校验器,为枚举数据校验赋能

3.1 为什么需要自定义注解?

  • 内置注解无法满足所有业务需求,例如枚举值校验、复杂逻辑校验等。

  • 自定义注解可以扩展校验功能,提高代码的复用性和可维护性。

3.2 自定义枚举数据类型注解和校验器

3.2.1 枚举值校验注解

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 枚举值校验注解
 *
 * @author bamboo panda
 * @version 1.0
 * @date 2025/5/7 13:54
 */
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumCheckValidator.class)
public @interface EnumCheck {

    String message() default "";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    Class<? extends Enum<?>> enumClass();

    String enumMethod();
}

3.2.2 枚举值校验器

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * 枚举值校验器
 *
 * @author bamboo panda
 * @version 1.0
 * @date 2025/5/7 13:56
 */
public class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {

    private Class<? extends Enum<?>> enumClass;
    private String enumMethod;


    @Override
    public void initialize(EnumCheck enumCheck) {
        enumMethod = enumCheck.enumMethod();
        enumClass = enumCheck.enumClass();

    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (null == value) {
            return Boolean.FALSE;
        }
        if (enumClass == null || enumMethod == null) {
            return Boolean.FALSE;
        }

        Class<?> valueClass = value.getClass();
        try {
            Method method = enumClass.getMethod(enumMethod, valueClass);
            if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
                throw new RuntimeException(String.format("%s method return is not boolean type in the %s class", enumMethod, enumClass));
            }

            if (!Modifier.isStatic(method.getModifiers())) {
                throw new RuntimeException(String.format("%s method is not static method in the %s class", enumMethod, enumClass));
            }

            Boolean result = (Boolean) method.invoke(null, value);
            return result == null ? false : result;
        } catch (NoSuchMethodException | SecurityException e) {
            throw new RuntimeException(String.format("This %s(%s) method does not exist in the %s", enumMethod, valueClass, enumClass), e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

3.2.3 对应的枚举实现

注意:这里主要关注isValid方法,是枚举注解里面使用的方法名称

/**
 * 送达状态枚举
 *
 * @author bamboo panda
 * @version 1.0
 * @date 2025/5/7 14:00
 */
public enum ServiceStatusEnum {

    PEPPERY(0, "待送达"),
    NOT_PEPPERY(1, "已送达"),
    ALL_IS_OK(2, "未送达");

    private Integer value;

    private String name;

    ServiceStatusEnum(Integer value, String name) {
        this.value = value;
        this.name = name;
    }

    public Integer getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    /**
     * 判断参数合法性
     *
     * @param value 值
     * @return true 合法 false 不合法
     */
    public static boolean isValid(Integer value) {
        ServiceStatusEnum[] values = ServiceStatusEnum.values();
        for (ServiceStatusEnum tempEnum : values) {
            if (tempEnum.getValue().compareTo(value) == 0) {
                return true;
            }
        }
        return false;
    }

}

3.2.4 注解使用方法

/**
 * 送达状态 0待送达 1已送达 2未送达
 */
@NotNull(message = "不能为空", groups = {Edit.class})
@EnumCheck(message = "送达状态不合法", enumClass = ServiceStatusEnum.class, enumMethod = "isValid", groups = {Edit.class})
private Integer serviceStatus;

注意:我们的注解校验器里面,为空默认是不行的,所以这里也可以不要@NotNull

四、总结

到这里已经介绍清楚了Hibernate Validator是干嘛的?它给我们提供了丰富的内置注解,方便了我们快速优雅的开发。

虽然内置注解丰富,但无法满足所有业务需求。例如,对于枚举值的校验、复杂逻辑的校验等,内置注解可能无能为力。

这个时候就需要我们自己去扩展,这个扩展也非常简单,俗话说得好,自己动手,丰衣足食。

最后:如果你从本篇文章中学到一点点东西,麻烦发财的小手点一下赞,如有疑问或者错误,欢迎提出和指正。