1、Hibernatr-Validator
1.1、简介
在 RESTful 的接口服务中,会有各种各样的入参,我们不可能完全不做任何校验就直接进入到业务处理的环节,通常我们会有一个基础的数据验证的机制,待这些验证过程完毕,结果无误后,参数才会进入到正式的业务处理中。而数据验证又分为两种,一种是无业务关联的规则性验证,一种是根据现有数据进行的联动性数据验证(简单来说,参数的合理性,需要查数据库)。而Hibernate-Validator则适合做无业务关联的规则性验证,而这类验证的代码大多是可复用的。
- JSR 303和JSR 349
简单来说,就是Java规定了**一套关于验证器的接口
**。开始的版本是Bean Validation 1.0(JSR-303),然后是Bean Validation 1.1(JSR-349),目前最新版本是Bean Validation 2.0(JSR-380),大概是这样。
从上可以看出Bean Validation并不是一项技术而是一种规范,需要对其实现。hibernate团队提供了参考实现,Hibernate validator 5是Bean Validation 1.1的实现,Hibernate Validator 6.0是Bean Validation 2.0规范的参考实现。新特性可以到官网查看,笔者最喜欢的两个特性是:跨参数验证(比如密码和确认密码的验证)和在消息中使用EL表达式,其他的还有如方法参数/返回值验证、CDI和依赖注入、分组转换等。对于方法参数/返回值验证,大家可以参阅《hibernate官方文档)》
如果项目的框架是spring boot的话,在依赖**spring-boot-starter-web
中已经包含了Hibernate-validator
**的依赖。Hibernate-Validator的主要使用的方式就是注解的形式,并且是“零配置”的,无需配置也可以使用。下面展示一个最简单的案例。
1.2、添加到Spring Boot
<!-- Spring Boot 新版本取消了 Hibernatr-Validator 的默认配置,所以需要手动添加 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
1.3、校验注解与异常捕获
- 在需要校验的POJO前面加
@Validated
注解代表校验该参数。
- @Validated:Spring提供的数据校验
- @Valid:JSR303数据校验
- 在需要校验的POJO后面加
BindingResult参数
,用来接收校验出错误时的提示信息。
@Validated注解和BindingResult参数必须配对使用,并且位置顺序固定。 如果要校验的参数有多个,入参写法:(@Validated Foo foo, BindingResult fooBindingResult, @Validated Bar bar, BindingResult barBindingResult);
1.4、常用注解
注意:
- 除了@Empty要求字符串不能全是空格,其他的字符串校验都是允许空格的。
- message是可以引用常量的,但是如@Size里max不允许引用对象常量,基本类型常量是可以的。
- 注意大部分规则校验都是允许参数为null,即当不存在这个值时,就不进行校验了。
注解 | 作用类型 | 释义 |
---|---|---|
@CreditCardNumber | 字符串 | 验证信用卡号码 |
@DateFromatCheckPatterm | 字符串 | 验证日期格式是否满足正则表达式 |
@DateValidator | 字符串 | 验证日期格式是否满足正则表达式 |
字符串 | 必须是邮箱格式 | |
@Length(min=, max=) | 字符串 | 长度范围 |
@NotBlank(message=) | 字符串 | 不能为null,长度大于0 |
@NotEmpty | 字符串 | 不能为null,长度大于0 |
@NotEmptyPatterm | 字符串 | 不能为null,长度大于0,是否匹配正则表达式 |
@Pattern(regex=, flag=) | 正则表达式 | 必须符合正则表达式 |
@Max(value) | 数字,不支持小数 | 必须为数字,其值小于或等于指定的最大值 |
@Min(value) | 数字,不支持小数 | 必须为数字,其值大于或等于指定的最小值 |
@DecimalMax(value) | 数字 | 必须为数字,其值小于或等于指定的最大值 |
@DecimalMin(value) | 数字 | 必须为数字,其值大于或等于指定的最小值 |
@Digits(integer, fraction) | 数字 | 必须为数字,其值必须再可接受的范围内 |
@Range(min=, max=, message=) | 数值类型、字符串、字节等 | 元素的大小范围 |
@Future | 日期 | 必须是将来的日期 |
@Past | 日期 | 必须是过去的日期 |
@Size(max=, min=) | 任意 | 集合的长度 |
@NotNull | 任意 | 不能为null |
@Null | 任意 | 必须为null |
@AssertFalse | 布尔值 | 必须为false |
@AssertTrue | 布尔值 | 必须为true |
@ListStringPattern | List | 验证集合中的字符串是否满足正则表达式 |
2、案例
2.1、Controller中直接校验
package com.example.test1.controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.Min;
@Validated
@RestController
@RequestMapping("Validator")
public class ValidatorController {
/**
* 1、id数不能小于10,@RequestParam类型的参数需要在Controller上增加@Validated
* 2、BindingResult 用来捕获验证异常信息
*/
@RequestMapping(value = "/Min")
public String test(@Min(value = 10, message = "id最小只能是10") @RequestParam("id") Integer id, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return getValidateError(bindingResult);
}
return "输入参数符合要求:" + id.toString();
}
}
2.2、分组校验、顺序校验、级联校验
同一个校验规则,不可能适用于所有的业务场景,对每一个业务场景去编写一个校验规则,又显得特别冗余。实际上我们可以用到Hibernate-Validator的分组功能。
使用分组能极大的复用需要验证的类信息。而不是按业务重复编写冗余的类。其中@GroupSequence提供组序列的形式进行顺序式校验,即先校验@Save分组的,如果校验不通过就不进行后续的校验多了。我认为顺序化的校验,场景更多的是在业务处理类,例如联动的属性验证,值的有效性很大程度上不能从代码的枚举或常量类中来校验。
2.2.1、案例
com/example/test1/bean/Update.java
package com.example.test1.bean;
public interface Update {
}
com/example/test1/bean/Save.java
package com.example.test1.bean;
public interface Save {
}
com/example/test1/bean/UserDto.java
package com.example.test1.bean;
import lombok.Data;
import javax.validation.GroupSequence;
import javax.validation.Valid;
import javax.validation.constraints.*;
import java.time.LocalDate;
import java.util.List;
/**
* 注解@GroupSequence指定分组校验的顺序,即先校验Save分组的,如果不通过就不会去做后面分组的校验了
*/
@Data
@GroupSequence({Save.class, Update.class, UserDto.class})
public class UserDto {
@NotEmpty(message = "缺少参数", groups = Update.class)
private String id;
@NotEmpty(message = "缺少参数", groups = Save.class)
@Size(min = 1, max = 20, message = "用户名输入过长")
@Pattern(regexp = "^[\\u4E00-\\u9FA5\\uf900-\\ufa2d·s]{2,50}$", message = "用户名输入错误")
private String name;
@NotNull(message = "缺少参数")
@Min(message = "缺少参数", value = 0, groups = Save.class)
private Integer age;
@Pattern(message = "手机号输入格式错误", regexp = "^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$")
private String phone;
@Past(message = "出生不能早于当前时间")
private LocalDate birthday;
// 自定义验证,validation没有验证列表内容的注解,需要自己添加
@EnumCheck(message = "只能选:1(男)或2(女)", enumClass = SexEnum.class)
private Integer sex;
/**
* 级联校验只需要添加@Valid
* 注解@ConvertGroup用于分组的转换,只能和@Valid一起使用。(一般用不到)
*/
@Size(message = "列表数量超出范围", max = 50)
@Valid
//@ConvertGroup(from = Search.class, to = Update.class)
private List<UserModel> list;
}
2.2.2、校验的接口
package com.example.test1.controller;
import com.example.test1.bean.Save;
import com.example.test1.bean.Update;
import com.example.test1.bean.UserDto;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Min;
import javax.validation.groups.Default;
import java.util.HashMap;
import java.util.Map;
@Validated
@RestController
@RequestMapping("Validator")
public class ValidatorController {
/**
* 这里的@Validated({Save.class, Default.class}) 其中Default.class是校验注解默认的分组,
* (也就说明自定义校验注解时要加上)
*/
@RequestMapping(value = "/addUser")
public String addUser(@Validated({Save.class, Default.class}) @RequestBody UserDto addDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return getValidateError(bindingResult);
}
return "添加成功:" + addDto.toString();
}
@PostMapping(value = "/updatedUser")
public String updatedUser(@Validated({Update.class, Default.class}) @RequestBody UserDto updateDto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return getValidateError(bindingResult);
}
return "修改成功:" + updateDto.toString();
}
/**
* 校验错误信息方法可封装成工具类
*/
static public String getValidateError(BindingResult bindingResult) {
if (!bindingResult.hasErrors()) {
return null;
}
Map<String, String> fieldErrors = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
fieldErrors.put(error.getField(), error.getCode() + " | " + error.getDefaultMessage());
}
HashMap<String, Object> result = new HashMap<>();
result.put("fieldErrors", fieldErrors);
return HttpStatus.UNPROCESSABLE_ENTITY.value() + "参数错误" + result;
}
}
2.3、自定义校验注解(枚举)、组合校验注解
上面这些注解能适应我们绝大多数的验证场景,但是为了应对更多的可能性,我们可以自定义注解来满足验证的需求。
我们一定会用到这么一个业务场景,vo中的属性必须符合枚举类中的枚举。Hibernate-Validator中还没有关于枚举的验证规则,那么,我们则需要自定义一个枚举的验证注解。
2.3.1、编写注解
com/example/test1/bean/EnumCheck.java
package com.example.test1.bean;
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;
// 限定使用范围--方法、字段
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
// 表明注解的生命周期,它在运行代码时可以通过反射获取到注解
@Retention(RetentionPolicy.RUNTIME)
// validatedBy:以指定该注解的校验逻辑
@Constraint(validatedBy = EnumCheckValidator.class)
public @interface EnumCheck {
// 是否必填 默认是必填的
boolean required() default true;
// 验证失败的消息
String message() default "枚举的验证失败";
//分组的内容
Class<?>[] groups() default {};
// 错误验证的级别
Class<? extends Payload>[] payload() default {};
// 枚举的Class
Class<? extends Enum<?>> enumClass();
// 枚举中的验证方法
String enumMethod() default "validation";
}
2.3.2、编写注解的校验类
com/example/test1/bean/EnumCheckValidator.java
package com.example.test1.bean;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
public class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {
private EnumCheck enumCheck;
/**
* 初始化验证消息,可以得到配置的注解内容
*
* @param enumCheck 需要验证的数据
*/
@Override
public void initialize(EnumCheck enumCheck) {
this.enumCheck = enumCheck;
}
/**
* 执行验证方法,用来验证业务逻辑,需要继承 ConstraintValidator 接口
*
* @param value 注解表明
* @param constraintValidatorContext 继承 ConstraintValidator 接口
* @return 验证结果
*/
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
// 注解表明为必选项 则不允许为空,否则可以为空
if (value == null) {
return !this.enumCheck.required();
}
Boolean result = Boolean.FALSE;
Class<?> valueClass = value.getClass();
try {
// 通过反射执行枚举类中validation方法
Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
result = (Boolean) method.invoke(null, value);
if (result == null) {
return false;
}
} catch (Exception e) {
System.out.println("custom EnumCheckValidator error\n" + e);
}
return result;
}
}
2.3.3、编写枚举类
com/example/test1/bean/SexEnum.java
package com.example.test1.bean;
import java.util.Objects;
public enum SexEnum {
MAN("男", 1), WOMAN("女", 2);
private String label;
private Integer value;
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
SexEnum(String label, int value) {
this.label = label;
this.value = value;
}
/**
* 判断值是否满足枚举中的value
*
* @param value 映射值
* @return 判断结果:布尔值
*/
public static boolean validation(Integer value) {
for (SexEnum s : SexEnum.values()) {
if (Objects.equals(s.getValue(), value)) {
return true;
}
}
return false;
}
}
2.3.4、使用方法
// 自定义验证,validation没有验证列表内容的注解,需要自己添加
@EnumCheck(enumClass = SexEnum.class, message = "用户性別输入错误")
private Integer sex;
2.4、验证
2.4.1、正常情况
2.4.2、 缺少必填参数
2.4.3、缺少不同组的必填参数
核查优先级高的组:优先级高的组(Save),校验报错,优先级低的组(Update)将不再校验;
核查优先级低的组:优先级低的组(Update),校验报错,优先级高的组(Save)也校验;