一、背景
在 Java mvc 分层架构的实际应用中,从视图层到数据访问层,每一层都会对表单参数信息进行校验,如下图所示:
校验方式普遍采用“抽象工具类”+“逻辑if...else判断”的形式。其中,抽象工具类主要封装了业务常用的校验方法,该方法使用正则表达式对参数进行校验,并返回 boolean 类型的返回值;
// 验证输入用户名:只包含数字和小写字母,且不允许为空
public static boolean isUserName(String name) {
if(StringUtils.isBlank(name)){
// 不允许为空
return false;
}
String regEx = "[a-z0-9\\-]+";
Matcher m = Pattern.compile(regEx).matcher(name);
return m.matches();
}
在业务中进行逻辑判断返回值的真伪,从而控制业务流程走向。
// 新增用户(Controller层)
@PostMapping("/add/user")
public MethodResult<String> addUser(User user) {
// 表单参数校验
if(user == null){
return MethodResult.errorResult("用户信息不能为空");
}
// 调用抽象工具类中的校验
if(!Utils.isUserName(user.getName())){
return MethodResult.errorResult("用户名只能包含数字和小写字母且不能为空");
}
//.... 省略剩余校验逻辑 ....
}
这种验证方式有如下弊端:
- 虽然封装了抽象的校验工具类,但是在业务代码中仍然需要采用大量的if..else..编码。
- 若业务校验规则变更,这些散落在业务代码中的校验语句均需要修改,耗时且容易诱发错误。
二、Bean Validation 规范定义
Bean Validation 规范(JSR303规范)提供了对 Java EE 和 Java SE 中的 Java Bean 进行验证的方式。该规范主要使用注解的方式来实现对 Java Bean 的验证功能,从而使验证逻辑从业务代码中分离出来,如下图所示:
Bean Validation 规范倾向于将验证规则直接放到 Java Bean 本身,使用注解的方式进行规则校验,Bean Validation API为我们提供了可拓展的四大接口(Bootstrapping、Validator、ConstraintViolation、MessageInterpolator)以及约束注解。
Hibernate Validator 对 Bean Validation 规范进行了实现,后续示例代码均基于该实现进行,以下是简要版本清单:
spring-boot-starter-parent-2.1.0.RELEASE;
hibernate-validator-6.0.13.Final;
validation-api-2.1.0.Final;
三、Bean Validation 规范应用
Bean Validation 规范对约束的定义包括两部分:一是约束注解;二是约束验证器。每一个约束注解都存在对应的约束验证器,约束验证器用来验证具体的 Java Bean 是否满足该约束注解声明的条件。
1.约束注解
Bean Validation 规范默认提供了几种约束注解的定义,如下:
约束注解名称 | 约束注解说明 |
---|---|
@Null | 验证元素是否为空 |
@NotNull | 验证元素是否非空 |
@NotBlank | 验证元素是否非空,且必须包含至少一个非空白字符 |
@NotEmpty | 验证元素是否非空,支持Array, Collection, Map, Charsequence |
@AssertTrue | 验证 Boolean 对象是否为 true |
@AssertFalse | 验证 Boolean 对象是否为 false |
@Min | 验证 Number 和 String 对象是否大等于指定的值 |
@Max | 验证 Number 和 String 对象是否小等于指定的值 |
@DecimalMin | 验证 Number 和 String 对象是否大等于指定的值,小数存在精度 |
@DecimalMax | 验证 Number 和 String 对象是否小等于指定的值,小数存在精度 |
@Size | 验证元素(Array,Collection,Map,String)长度是否在给定的范围之内 |
@Digits | 验证 Number 和 String 的构成是否合法 |
@Past | 验证 Date 和 Calendar 对象是否在当前时间之前 |
@Future | 验证 Date 和 Calendar 对象是否在当前时间之后 |
验证元素是否是格式正确的电子邮件地址 | |
@Negative | 验证元素是否是一个严格的负数(0被视为无效值) |
@NegativeOrZero | 验证元素是否是一个负数或0 |
@Pattern | 验证元素是否满足指定的正则表达式 |
@Positive | 验证元素是否是严格的正数(即0被视为无效值) |
@PositiveOrZero | 验证元素是否是正数或0 |
约束注解的使用示例:
// 示例代码 : 视图层领域模型
public class FaceInfoVO extends FaceInfoBaseVO {
@NotEmpty(message = "缩略图不能为空")
@Size(max = 128, message = "缩略图存储路径最大长度不能超过128")
private String thumbnailUrl;
// .... 省略其他字段以及getter setter ....
}
// 示例代码 : Controller层使用, 这里@Validated是Spring提供的注解, Controller接收参数时即可进行校验。
@PostMapping("/add/faceinfo")
public MethodResult<?> addFaceInfo(@Validated FaceInfoVO faceInfoVO {
// .... ....
}
2.分组验证
很多时候,一个视图层领域模型可以用于不同场景,每一个场景对模型中的属性校验各不相同。于是Bean Validation 规范中引入了一个重要的概念,就是 “组”。
对于一个给定的Java Bean,有了组的概念,则无需对该 Java Bean 中所有的约束进行验证,只需要对该组定义的一个子集进行验证即可。完成组别验证需要在约束声明时进行组别的声明,否则使用默认的组 Default.class。
// 定义组
public interface GroupUpdateInfo {
}
public interface GroupInsertInfo {
}
// 示例代码 : 视图层领域模型
public class FaceInfoVO extends FaceInfoBaseVO {
@NotEmpty(message = "缩略图不能为空", groups = {GroupUpdateInfo.class})
private String thumbnailUrl;
@NotEmpty(message = "名称不能为空", groups = {GroupInsertInfo.class})
private String name;
// .... 省略其他字段以及getter setter ....
}
// 示例代码: Controller层, 此处校验满足GroupUpdateInfo组的属性
public MethodResult<?> updateInfo(@Validated(GroupUpdateInfo.class) FaceInfoVO vo) {
// .... 省略 ....
}
3.组序验证
一个组可以定义为其他组的序列,示例代码如下:
// 定义组序列
@GroupSequence({GroupA.class, GroupB.class})
public intreface Group {
}
在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。不过遗憾的是,由于底层代码使用的HashSet的缘故,在同一个 Java Bean 中当多个字段属性对应同一个组时,这些同组的字段是没有先后顺序之分的。
public class FaceInfoVO extends FaceInfoBaseVO {
@NotEmpty(message = "缩略图不能为空", groups = {GroupB.class})
private String thumbnailUrl;
@NotEmpty(message = "名称不能为空", groups = {GroupB.class})
private String name;
@NotEmpty(message = "ID不能为空", groups = {GroupA.class})
private String id;
// .... 省略其他字段以及getter setter ....
}
在上段示例中,当Controller层使用@Validated(Group.class)进行校验,那么人员 id 肯定会优先于 name 和 thumbnailUrl 进行校验。 当 id 校验合法时,对GroupA的两个元素校验是无序的,可能是 name 也可能是 thumbnailUrl。
4.级联验证
在实际业务中,领域模型可能存在很多“对象套对象”或者“对象套集合”的情况,对于这些场景可以使用级联验证进行解决,示例代码如下:
// 对象套数组, 集合中又套了一层对象
public class FaceComparAlarmResultDTO extends FaceAlarmResultDTO {
@NotEmpty(message = "结果信息不能为空")
private List<@NotNull @Valid FaceComparAlarmResultBO> alarmResult;
// .... 省略 ....
}
// 对象套对象
public class FacesBO {
@Valid
@NotNull(message = "年龄不能为空")
private FaceAgeBO age;
// .... 省略 ....
}
这里值得注意的是,当使用“对象套集合,集合又套对象”这种形式的时候,若想对List集合内部的每个对象以及对象中的每个属性进行校验,务必进行@NotNull非空验证,且将该验证写在泛型之中。
5.自定义约束注解以及验证器
我们也可以实现符合自身业务需求的约束注解,约束注解和普通的注解一样,约束注解的定义至少包括如下内容:
// 约束注解应用的目标元素类型
@Target({...})
// 约束注解应用的时机
@Retention(...)
// 多值约束
@Repeatable(List.class)
// 与约束注解关联的验证器
@Constraint(validatedBy = {...})
public @interface NotEmpty {
// 约束注解验证匹配时输出的消息
String message() default "";
// 约束注解在验证分组
Class<?>[] groups() default { };
@Target({...})
@Retention(...)
@Documented
public @interface List {
NotEmpty[] value();
}
}
-
目标元素类型:@Target表示注解可以用于什么地方,可以使用ElementType进行指定。
ElementType的类型包括:
- ElementType.CONSTRUCTOR: 用于描述构造器
- ElementType.FIELD: 成员变量、对象、属性(包括enum实例)
- ElementType.LOCAL_VARIABLE: 用于描述局部变量
- ElementType.METHOD: 用于描述方法
- ElementType.PACKAGE: 用于描述包
- ElementType.PARAMETER: 用于描述参数
- ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明
-
注解生效时机:@Retention表示该自定义注解的生命周期,可以使用RetentionPolicy指定。
RetentionPolicy的类型包括:
- RetentionPolicy.SOURCE : 在编译阶段丢弃。标志着编译结束之后该注解就不再有任何意义,不会被写入字节码,例如:@Override、@SuppressWarnings等。
- RetentionPolicy.CLASS : 在类加载的时候丢弃。在字节码文件的处理中有用,lombok提供的注解,例如:@Data、@Getter、@Setter;mapstruct提供的注解,例如:@Mapper、@Mapping等。
- RetentionPolicy.RUNTIME : 始终不会丢弃,运行期也保留该注解,可以使用反射机制读取该注解的信息。
关联注解验证器:@Constraint的值用于指定该注解的注解验证器,可以指定一个或多个。
多值约束:@Repeatable在需要在同一元素上进行多值约束时可以进行标注,并指定List
自定义约束注解的完整示例,如下:
@Target(FIELD)
@Retention(RUNTIME)
@Repeatable(List.class)
@Constraint(validatedBy = TextValidator.class)
public @interface ValidateText {
String message() default "文本只支持中文,且长度小于8";
Class<?>[] group() default ();
@Target(FIELD)
@Retention(RUNTIME)
@interface List {
ValidateText[] value();
}
}
约束注解定义完成后,需要同时实现与该约束注解关联的验证器。约束验证器的实现需要实现 JSR303 规范提供的接口 javax.validation.ConstraintValidator。
public interface ConstraintValidator<A extends Annotation, T> {
default void initialize(A constraintAnnotation) {}
boolean isValid(T value, ConstraintValidatorContext context);
}
该接口有两个方法,方法 initialize 对验证器进行实例化,必须在验证器的实例使用之前被调用,并保证正确初始化验证器,它的参数是约束注解;方法 isValid 是进行约束验证的主体方法,其中 value 参数代表需要验证的实例,context 参数代表约束执行的上下文环境,这里定义@ValidateText对应的验证器TextValidator:
public class isValid(String value, ConstraintValidatorContext context) {
if(StringUtils.isBlank(value)) {
return false;
}
String regex = "[\u4e00-\u9fa5]+";
return value.matches(regex) && value.length() <= 8;
}
四、总结
使用 Bean Validation 规范进行表单验证,可以减少业务代码中的if…else的使用,将验证统一到了 Java Bean 中方便了业务变更后的校验规则修改。当然,该验证方式仍然不能完全适用于所有业务场景,遇到复杂的业务验证场景可以自定义处理器或配合if…else使用。