新公司在使用的 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% 的场景。有需要再学习!

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

相关推荐
Q_274378510939 分钟前
django基于Python的智能停车管理系统
java·数据库·python·django
编程小筑2 小时前
R语言的数据库编程
开发语言·后端·golang
兩尛2 小时前
maven高级(day15)
java·开发语言·maven
大熊程序猿2 小时前
golang 环境变量配置
开发语言·后端·golang
xiao--xin2 小时前
LeetCode100之搜索二维矩阵(46)--Java
java·算法·leetcode·二分查找
end_SJ3 小时前
c语言 --- 字符串
java·c语言·算法
zzyh1234563 小时前
spring cloud 负载均衡策略
java·spring cloud·负载均衡
涔溪3 小时前
JS二叉树是什么?二叉树的特性
java·javascript·数据结构
拾忆,想起3 小时前
深入浅出负载均衡:理解其原理并选择最适合你的实现方式
分布式·后端·微服务·负载均衡
zzyh1234563 小时前
springcloud负载均衡原理
java·spring cloud·负载均衡