入职新公司,大家都在 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)
价值是什么
- 提高代码质量:减少样板代码,提高代码可读性,集中管理校验逻辑
- 增强系统健壮性:防止非法数据进入系统,及早发现并处理异常
- 提升开发效率:声明式校验,复用校验规则,统一异常处理
快速搭建项目
快速搭建项目,验证一下
w
基于: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.
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",即两端的空格被去除,中间的连续空格被压缩为单个空格
- 去除字符串两端的空白字符:这包括空格、制表符等
- 将中间的连续空白字符替换为单个空格:这包括连续的空格、制表符、换行符等
- ......
其他注解
想 @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% 的场景。有需要再学习!
本文到此结束,感谢阅读!下期再见。