新公司在使用的 Hibernate Validator 框架

入职新公司,大家都在 Hibernate Validator 做参数校验。这一期就来详细聊一下这个校验框架。

Hibernate Validator 介绍

是什么

Hibernate Validator 是 Jakarta Validation API 的具体实现。 Jakarta Validation API 的前身 Bean Validation API, 随着Java EE向Jakarta EE的过渡,Bean Validation 2.0也随之更名为 Jakarta Bean Validation 2.0,强大的Java平台验证框架,但 Jakarta Validation API 只是接口规范,Hibernate Validator 才是实现。(实现不止 hibernate)

价值是什么

  1. 提高代码质量:减少样板代码,提高代码可读性,集中管理校验逻辑
  2. 增强系统健壮性:防止非法数据进入系统,及早发现并处理异常
  3. 提升开发效率:声明式校验,复用校验规则,统一异常处理

快速搭建项目

快速搭建项目,验证一下

w

仓库地址:gitee.com/uzongn/vali...

基于:hibernate-validator 实现版本。(6.2.3.Final)

基础教程

归零心态,认认真真学一遍基础教程。在项目真正做到,看得懂,能会用。

validation-api 定义的注解

validation-api 定义的规范

具体包路径:javax.validation.constraints

@Null

描述 支持类型
注解元素必须为null 任意类型
kotlin 复制代码
@Null(message = "ID必须为空")
private Long id;

@NotNull

描述 支持类型
注解元素必须不为null 任意类型
kotlin 复制代码
@NotNull(message = "ID不能为空")
private Long id;

@NotEmpty

描述 支持类型
不能为 null 或者不能没有值。集合判断size 大小;数组/CharSequence 类型判断长度 - map
  • collection
  • array
  • CharSequence |
ini 复制代码
@NotEmpty(message = "用户名不能为空")
private String username;

@NotBlank

描述 支持类型
不能为 null 并且 至少有一个非空字符 CharSequence
ini 复制代码
@NotBlank(message = "密码不能为空")
private String password;

@Size

描述 支持类型
支持大小。[min, max], 支持的最大值 Integer.MAX_VALUE - map
  • collection
  • array
  • CharSequence |
arduino 复制代码
@Size(min = 1, max = 5, message = "兴趣爱好必须在1-5个之间")
private List<String> hobbies;

特别注意:如果元素为null,则不参与校验。比如 hobbies 为 null,不校验

null elements are considered valid.

@Email

since 2.0 (从2.0开始才有)

描述 支持类型
校验邮箱;该字符串必须是格式正确的电子邮件地址 CharSequence
ini 复制代码
@Email(message = "邮箱格式不正确")
private String email;

特别注意:如果元素为null,则不参与校验。比如 email 为 null,不校验

null elements are considered valid.

@Min && @Max

描述 支持类型
@Min带注解的元素必须是一个数字,其值必须大于或等于指定的最小值。 BigDecimal BigInteger byte、short、int、long 及其各自的包装器请注意,不支持 double 和 float
@Max带注解的元素必须是一个数字,其值必须小于或等于指定的最大值。 BigDecimal BigInteger byte、short、int、long 及其各自的包装器请注意,不支持 double 和 float
less 复制代码
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;

特别注意:如果元素为null,则不参与校验。比如 age 为 null,不校验

null elements are considered valid.

@DecimalMin && @DecimalMax

与 @Min && @Max 非常类似

描述 支持类型
@DecimalMin带注解的元素必须是一个数字,其值必须大于或等于指定的最小值。 BigDecimal BigInteger byte、short、int、long 及其各自的包装器请注意,不支持 double 和 float
@DecimalMax带注解的元素必须是一个数字,其值必须小于或等于指定的最大值。 BigDecimal BigInteger byte、short、int、long 及其各自的包装器请注意,不支持 double 和 float
less 复制代码
@DecimalMin(value = "0.0", inclusive = true)
@DecimalMax(value = "100.0", inclusive = true)
private Double percentage;

inclusive 是否包含当前值。例如:@DecimalMin(value = "0.0", inclusive = false)。 percentage > 0.0

@Future && @FutureOrPresent && @Past && @PastOrPresent

描述 支持类型
对时间的校验。- 未来
  • 未来或者现在
  • 过去
  • 过去或者现在 | |
ini 复制代码
@Past(message = "生日必须是过去的日期")
private LocalDate birthday;

@Future(message = "计划日期必须是将来的日期")
private LocalDate planDate;

@FutureOrPresent(message = "时间必须是现在或将来")
private LocalDateTime appointmentTime;

@ Positive && PositiveOrZero && @Negative && @NegativeOrZero

描述 支持类型
判断值是负数、0、正数 - BigDecimal
  • BigInteger
  • byte, short, int, long, float, double 以及它们的包装类 |
kotlin 复制代码
@Positive
private Long orderId;

特别注意:如果元素为null,则不参与校验。比如 age 为 null,不校验

null elements are considered valid.

@ Digits

描述 支持类型
有两个关键参数integer:此数字接受的最大整数位数fraction:此数字接受的最大小数位数 - BigDecimal
  • BigInteger
  • byte, short, int, long, float, double 以及它们的包装类 |
ini 复制代码
@Digits(integer = 10, fraction = 2, message = "金额格式无效")
private BigDecimal amount;

@AssertTrue && AssertFalse

描述 支持类型
带注解的元素必须为 true / false。 支持的类型包括 boolean 和 Boolean。
ini 复制代码
@AssertTrue(message = "必须同意用户协议")
private boolean agreementAccepted;

@Pattern

正则表达式。

描述 支持类型
带注解的 CharSequence 必须与指定的正则表达式匹配。 CharSequence,正则表达式遵循 Java 正则表达式约定

null elements are considered valid.

less 复制代码
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$", 
        message = "密码必须至少8个字符,至少包含一个字母和一个数字")
private String password;

注解可以使用的位置

这些注解可以使用在哪些地方呢? 从来源中得到描述

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })

注解位置 说明
METHOD 可以被应用在任何方法上,包括类的方法和接口的方法。
FIELD 可以被应用在类的任何字段上,包括类的属性。
ANNOTATION_TYPE 可以被应用在其他注解类型上,用于定义元注解。
CONSTRUCTOR 可以被应用在类的构造器上。
PARAMETER 可以被应用在方法或构造器的参数上。
TYPE_USE 可以被应用在任何类型使用的地方,比如类实例化、类型转换、泛型参数等。

定义在方法上(@AssertTrue举例)

java 复制代码
@AssertTrue(message = "总价必须等于数量乘以单价")
private boolean isValidTotalPrice() {
    if (quantity > 0 && price != null) {
        BigDecimal expectedTotal = price.multiply(BigDecimal.valueOf(quantity));
        return totalPrice != null && totalPrice.compareTo(expectedTotal) == 0;
    }
    return true;
}

定义在注解上(@Min举例)

less 复制代码
@Min(0)
@Max(Long.MAX_VALUE)
@ReportAsSingleViolation
public @interface Range {
    ......
}

在字段上(@Email举例)

ini 复制代码
@Email(message = "邮箱格式不正确")
private String email;

应用于参数上(@Min举例)

less 复制代码
// 使用 @Min 注解校验方法参数的值是否满足最小值约束
public void deposit(@Min(value = 100, message = "存款金额不能少于100") double amount) {
    // 存款逻辑
}

type 类型 (@NotNull举例)

typescript 复制代码
// 使用 @NotNull 注解校验泛型集合中的元素是否为 null
public List<@NotNull String> getNames() {
    return new ArrayList<>();
}

构造器(@NotNull举例)

less 复制代码
public class User {

    private String username;
    private String password;

    // 构造器,使用 @NotNull 和 @Size 注解来校验传入的参数
    public User(@NotNull(message = "用户名不能为空")  String username,
                @NotNull(message = "密码不能为空")  String password) {
        this.username = username;
        this.password = password;
    }
    ......
}

上面是 validation-api 定义的规范。接下来看一些 hibernate-validator 的扩展。

hibernate-validator

包路径:org.hibernate.validator.constraints

hibernate-validator 对 validation-api 做了扩展。

@Range

python 复制代码
@Range(min = 1, max = 5, message = "等级必须在1-5之间")
private Integer level;
描述 支持类型
带注解的元素必须在适当的范围内。 应用于数值或数值的字符串表示形式。 由@Min 和 @Max 组合

@length

描述 支持类型
验证字符串是否介于 min 和 max including 之间。 字符串
arduino 复制代码
@Length(min = 2, max = 2, message = "州代码必须是两个字母")
private String stateCode;

@ParameterScriptAssert

参数脚步判断

描述 支持类型
用于根据带注解的方法或构造函数计算脚本表达式。此约束可用于实现依赖于带注解的可执行文件的多个参数的验证 脚本表达式可以用任何脚本或表达式语言编写

使用 JDK 附带的 JavaScript 引擎的案例:

typescript 复制代码
@ParameterScriptAssert(script = "start. before(end)", lang = "javascript")
public void createEvent(Date start, Date end) { 
    ... 
}

@CreditCardNumber

描述
带注释的元素必须表示有效的信用卡号。这是 Luhn 算法的实现,旨在检查用户错误,而不是信用卡有效性!
ini 复制代码
@CreditCardNumber(message = "无效的信用卡号")
private String creditCardNumber;

@URL

校验网络地址格式

ini 复制代码
@URL(message = "网址格式不正确")
private String url;

@UniqueElements

描述 支持类型
校验集合中的元素唯一 collection 集合
less 复制代码
public class EventSchedule {

// 使用 @UniqueElements 注解来校验集合中的元素是否唯一
@NotNull(message = "事件列表不能为空")
@Size(min = 1, message = "至少需要一个事件")
@UniqueElements(message = "事件列表中的事件不能重复")
private List<String> eventNames;

public List<String> getEventNames() {
    return eventNames;
}

public void setEventNames(List<String> eventNames) {
    this.eventNames = eventNames;
}
}

@Normalized

@Normalized注解是Hibernate Validator提供的一个注解,用于确保一个字符串被"规范化"处理,即去除字符串两端的空白字符,并且将中间的连续空白字符替换为单个空格。这个注解特别适用于处理用户输入的字符串。

ini 复制代码
@Normalized(message = "输入内容必须规范化")
private String input;

user.setInput(" Hello World "); // 用户输入的字符串两端和中间包含多余的空格

input属性的值将被规范化为"Hello World",即两端的空格被去除,中间的连续空格被压缩为单个空格

  1. 去除字符串两端的空白字符:这包括空格、制表符等
  2. 将中间的连续空白字符替换为单个空格:这包括连续的空格、制表符、换行符等
  3. ......

其他注解

想 @NIP、CPF 与国家相关,EAN、ISBN 用得比较少的注解就不展开讲解了,感兴趣可以自行研究。

进阶教程

分组校验-groups(好用的功能)

在校验规则中,groups 允许将校验注解应用到不同的校验分组中,这样可以根据业务需求对同一个对象执行不同的校验逻辑。通过定义不同的分组接口,可以为不同的业务场景指定不同的校验规则。

这种方式提供了一种灵活的方式来应对不同的校验场景,使得代码更加清晰和易于维护

举个例子:用不同的分组,来进行注册和更新的校验!

less 复制代码
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.groups.Default;

// 定义两个校验分组接口
public interface RegisterGroup {}
public interface UpdateGroup {}

public class UserDto {

    // 在注册和更新时都需要校验的字段
    @NotBlank(message = "用户名不能为空", groups = {RegisterGroup.class, UpdateGroup.class})
    private String username;

    // 只在注册时需要校验的字段
    @NotBlank(message = "密码不能为空", groups = RegisterGroup.class)
    private String password;

    // 在注册和更新时都需要校验的字段
    @Email(message = "邮箱格式不正确", groups = {RegisterGroup.class, UpdateGroup.class})
    private String email;
    
    // Getters and Setters
}

@RestController
public class UserController {

    @PostMapping("/register")
    public String register(@Validated(RegisterGroup.class) @RequestBody UserDto user) {
        // 注册逻辑
        return "注册成功";
    }

    @PutMapping("/update")
    public String update(@Validated(UpdateGroup.class) @RequestBody UserDto user) {
        // 更新逻辑
        return "更新成功";
    }
}

UserDto 类包含了三个字段:username、password 和 email。username 和 email 字段在注册和更新时都需要校验,因此它们被标记为属于 RegisterGroup 和 UpdateGroup 分组。而 password 字段只在注册时需要校验,因此它只属于 RegisterGroup 分组.

register 方法使用 @Validated(RegisterGroup.class) 来指定只对 UserDto 执行注册相关的校验,而 update 方法使用 @Validated(UpdateGroup.class) 来指定只对 UserDto 执行更新相关的校验


分组是比较实用的功能,当创建对象和更新对象,对于字段的约束是不一样的。如果不想创建 UpdateRequest、CreateRequest 两个对象,则可以使用一个对象,使用 group 做分组校验。

自定义校验注解

如何自定义一个校验注解。像@NotNull 一样

第一步:定义注解

less 复制代码
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ContainAConstraintValidator.class)
public @interface ContainAConstraint {
    String message() default "需要包含A";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

第二步:校验器

typescript 复制代码
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class ContainAConstraintValidator implements ConstraintValidator<ContainAConstraint, String> {
    @Override
    public void initialize(ContainAConstraint constraintAnnotation) {
        // 初始化逻辑
        System.out.println("初始化逻辑");
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value != null && value.contains("A")) {
            return true;
        }
        return false;
    }
}

第三步:使用

less 复制代码
@ContainAConstraint
@NotEmpty(message = "用户名不能为空")
private String username;

统一异常处理

在日常使用中,经常需要使用统一的拦截器,对错误进行统一处理。

在异常的统一切面进行处理。(返回对象,可以按照业务上自己去定义)

typescript 复制代码
@ControllerAdvice
public class ValidationExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> 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);
        });
        System.out.println("advice:" + errors);
        return ResponseEntity.badRequest().body(errors);
    }
}

处理方法参数校验异常@ExceptionHandler(MethodArgumentNotValidException.class)

其他校验异常:ExceptionHandler(ConstraintViolationException.class)

级联校验

如果用的是组合对象。需要使用 @Valid 做修饰

kotlin 复制代码
public class Department {
    @Valid
    private Manager manager;

    @Valid
    private List<Employee> employees;
}

最后

到这里,对于 Hibernate-validator 的使用就基本完成,详细可以应对工作中 90% 的场景。有需要再学习!

本文到此结束,感谢阅读!下期再见。

相关推荐
小王不爱笑13236 分钟前
Java项目基本流程(三)
java·开发语言
David爱编程1 小时前
Java 三目运算符完全指南:写法、坑点与最佳实践
java·后端
遇见尚硅谷1 小时前
C语言:单链表学习
java·c语言·学习
学习编程的小羊1 小时前
Spring Boot 全局异常处理与日志监控实战
java·spring boot·后端
YA3332 小时前
java基础(六)jvm
java·开发语言
Moonbit3 小时前
MoonBit 作者寄语 2025 级清华深圳新生
前端·后端·程序员
前端的阶梯3 小时前
开发一个支持支付功能的微信小程序的注意事项,含泪送上
前端·后端·全栈
咕噜分发企业签名APP加固彭于晏3 小时前
腾讯元器的优点是什么
前端·后端
JavaArchJourney3 小时前
Java 集合框架
java
尘民10243 小时前
面试官笑了:线程start() 为什么不能再来一次?
java