Validation框架的应⽤
Validation框架的应⽤
⼀,前⾔
这篇博客只说⼀下Validation框架的应⽤,不涉及相关JSR,相关理论,以及源码的解析。
如果之后需要的话,会再开博客描写,这样会显得主题突出⼀些。
后续扩展部分会解释message,groups,payload三个核⼼属性等。
⾃定义注解部分,会给出蚂蚁⾦服内部真实采⽤的⾃定义校验注解。
⼆,简介
简单来说,就是通过Validation框架,进⾏数据的各类校验。从Java的基本数据类型到⾃定义封装数据类型,从⾮空判断到正则表达式判断,都是Validation框架所⽀持的。
在Validation之前,层次架构中,开发者总是采⽤分层验证模型。就是分别在控制层,服务层,数据层等分别对⽬标对象的⽬标属性进⾏校验。很明显,这是⾮常不优雅的,⽽且开发效率低,因为存在⼤量重
复校验逻辑。
⽽Validation则提出⼀个元数据验证模型,⽽在Spring体系中,则表现为Java Bean验证模型。站在Spring⾓度来说,⽆论是在哪个层次,都是针对Java Bean进⾏验证的。所以,Validation则通过在⽬标Bean上添加约束注解,以及背后的验证程序,实现了⼀个对业务代码⽆侵⼊的校验功能。
三,使⽤⽅法
1.添加依赖
<!-- Validation 相关依赖 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
这是Validation框架的核⼼依赖。跨越自己
该依赖是包含在SpringBoot的spring-boot-web-starter中的。所以如果使⽤了前⾯Spring-boot-web-starter依赖,则不需要再次引⼊Validation框架的依赖。
⾄于EL等依赖,常⽤于⾃定义注解,具体可以根据需要进⾏依赖引⼊。
2.添加约束注解
针对⽬标Bean,针对不同属性的验证需求,添加不同的约束注解。
如UrVo的urId,添加@NotNull注解,表⽰这个属性在验证框架中不可为空。
有关约束注解,后⾯有详尽描述。
3.开启验证
即使对元数据模型添加了约束注解,但是还没有明确开启验证流程。站在Validation框架的⾓度,它并不知道应该在什么时候进⾏校验。因为除了控制层,我们还可能在服务层验证。即使是在服务层,⼀个调⽤链路,可能涉及多个⽅法,也需要确定在哪个⽅法进⾏验证。
那么,开启验证的⽅法有两种(也许还有别的⽅法,欢迎补充):
验证注解:@Validated或者@Valid
初始化验证器:Validation.buildDefaultValidatorFactory().getValidator();
验证注解
@Validated注解的效果与@Valid是⼀样的,毕竟@Validated是SpringBoot对@Valid注解的封装(@Valid是Java的⾃带的注解)。⽽@Validated注解是包含在SpringBoot的spring-boot-web-starter中的。
在对应位置添加@Validated注解(当程序执⾏到这⾥,就会执⾏对应的校验逻辑):
⾃定义对象(启动注解在⾃定义对象前)
@PostMapping("save.do")
@ResponBody
public ServerRespon saveConfig(@Validated(InclinationConfig.ConfigCommitGroup.class) Inclinat
ionConfig inclinationConfig) {
// 业务逻辑
}
基本数据类型()
@Validated
public class demo {
@PostMapping("get.do")
@ResponBody
public ServerRespon getConfig(int configId) {
// 业务逻辑
}
}
针对Java基本数据类型的@NotNull,则需要将对应类上添加@Validated注解。
验证器
初始化,建⽴验证器对象(Validator对象):
// 验证器对象
private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
揭开的近义词
获取验证结果集合(这⾥也就是开启验证的时间位置):
// 验证结果集合
private Set<ConstraintViolation<UrInfo>> t = validator.validate(urInfo);
// 验证过程可以添加分组信息
private Set<ConstraintViolation<UrInfo>> t = validator.validate(urInfo,UrInfo.RegisterGroup.class);
处理验证结果集合:
t.forEach(item -> {
// 输出验证错误信息
王阳明格物致知System.out.Message());
});
当然啦。更多情况下,我们是直接抛出异常的:
// 判断验证结果集是否为空(验证结果集放的都是验证失败时的message)
if(!CollectionUtils.isEmpty(t)) {
// 循环时,采⽤StringBuilder可以有效提⾼效率(详见String,StringBuilder,StringBuffer三者区别)
StringBuilder exceptionMessage = new StringBuilder();
t.forEach(validationItem -> {
exceptionMessage.Message());
});
// 直接抛出异常(其实这也就是@Valid注解的默认校验器的做法)
throw new Strring());
}
四,约束注解
1.初级应⽤:常⽤注解
这⾥给出了Validation框架(validation-api-2.0.1.Final)中constraints下全部的注解说明:
空值校验:
@Null:⽬标值为null。⽐如,注册时的urId当然是null(即使不为null,系统也不会采⽤的)。
@NotNull:⽬标值不为null。⽐如,登录时的urId当然不为null(当然也可能是通过了外部鉴权,然后内部裸奔)。
@NotEmpty:⽬标值不为empty。相较于上者,增加了对空值的判断(就是""⽆法通过@NotEmpty的校验)
@NotBlank:⽬标值不为blank。相较于上者,增加了对空格的判断(就是空格⽆法通过@NotBlank校验的)
范围校验:
@Min:针对数值类型,⽬标值不能低于该注解设定的值。
@Max:针对数值类型,⽬标值不能⾼于该注解设定的值。
@Size:针对集合类型,⽬标集合的元素数量不可以⾼于max参数,不可以低于min参数。
@Digits:针对数值类型,⽬标值的整数位数必须等于integer参数设定的值,⼩数位数必须等于fraction参数设定的值。
@DecimalMax:针对数值类型,⽬标值必须⼩于该注解设定的值。捺菜
@DecimalMin:针对数值类型,⽬标值必须⼤于该注解设定的值。
@Past:针对于⽇期类型,⽬标值必须是⼀个过去的时间。
@PastOrPrent:针对于⽇期类型,⽬标值必须是⼀个过去或现在的时间。
@Future:针对于⽇期类型,⽬标值必须是未来的时间。
@FutureOrPrent:针对于⽇期类型,⽬标值必须是未来或未来的时间。
@Negative:针对数值类型,⽬标值必须是负数。
NegativeOrZero:针对数值类型,⽬标值必须是⾮正数。
@Positive:针对数值类型,⽬标值必须是正数。
@PositiveOrZero:针对数值类型,⽬标值必须是⾮负数。
其他校验:
@AsrtTrue:针对布尔类型,⽬标值必须为true。
@AsrtFal:针对布尔类型,⽬标值必须为fal。
老婆的称呼@Email:针对字符串类型,⽬标值必须是Email格式。
@URL:针对字符串类型,⽬标值必须是URL格式。
@Pattern:针对字符串类型,⽬标值必须通过注解设定的正则表达式。
上⾯有关NotNull,NotEmpty,NotBlank,可以参考StringUtils的类似API。
另外,就是上述的@Pattern注解,可以说是最为灵活的注解。许多⾃定义注解,其实都可以通过@Pattern注解实现。
2.中级应⽤:级联,分组,序列
我认为Validation框架的中级应⽤有三个:
级联验证:通过@Valid注解实现级联校验。举个例⼦,我的ScriptionBO中有⼀个List属性。我希望Validation框架在校验ScriptionBO的时候,不仅仅校验ScriptionBO的属性,还要验证其中List涉及的Ur们。那么在List上添加@Valid注解,就可以实现了。
分组校验:通过分组Interface与校验注解的group参数,就可以实现分组校验。举个例⼦,同样是U
r实体类,既需要满⾜登录验证(有urId这样的属性),也需要满⾜注册验证(不需要urId这样的属性)。那么可以在Ur实体类中,建⽴⽤于登录场景的interface LoginGroup {}接⼝,与⽤于注册场景的interface RegisterGroup {}。在urId属性上,增加⾮空校验的@NotNull(groups = LoginGroup.class),就可以实现了。
分组序列:通过分组校验,再加上@GroupSequence({xxxGroup.class,xxxGroup.class}),就可以实现分组序列了。举个例⼦,登录场景下,Ur连urId的⾮空校验都没有通过,那么就更不需要校验⼿机号码,邮箱等。
3.⾼级应⽤:⾃定义校验注解
⾸先强调⼀点,正常情况下,常⽤约束注解配合Validation框架的中级应⽤,⾜以应付⼤多数情况。尤其是@Pattern注解采⽤了灵活的正则表达式,可以解决⼤部分复杂问题。
举个例⼦,正常的Email地址校验,可以通过@Email注解进⾏校验,更可以通过@Pattern实现更为精准的校验。⾄于⾃定义校验注解,则可以实现根据配置,动态验证Email地址的功能。
⾃定义校验注解,其实就类似于配合⾃定义注解的切⾯编程,只不过利⽤了Validation框架的⼀些基础⽅法。
⾃定义校验注解分为以下三步:
约束注解的定义。
约束验证规则(即⾃定义约束校验器)
关联约束注解与约束规则
为了更直观的感受,这⾥给出⼀个简单的demo。
另外,这⾥的依赖,需要单独引⼊,能只依靠springboot⾃带的validation依赖。
约束注解定义
package tech.jarry.anno;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/
**
* @author jarry
* @description ⾃定义动态属性校验约束注解
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
// 关联约束注解与约束规则
@Constraint(validatedBy = DynamicPropertyVerificationValidator.class)
public @interface DynamicPropertyVerification {
// 约束注解校验失败时的输出信息
String message() default "property verification fail";
百万葵园// 约束注解在验证时所属的组别
Class<?>[] groups() default {};
// 约束注解的负载(可⽤来保存⼀些数据)
Class<? extends Payload>[] payload() default {};
}
约束验证规则
package tech.jarry.anno;
import com.alibaba.fastjson.JSON;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.ArrayList;
import java.util.List;
/**
* @author jarry
* @description 动态属性的⾃定义约束校验器
*/
尊重的反义词
public class DynamicPropertyVerificationValidator implements ConstraintValidator<DynamicPropertyVerification, String> {
// 为了便于进⾏测试,这⾥先放⼊⼀些本地数据
private static final List<String> REX_LIST = new ArrayList<String>() {
{
add("auth_1");
add("auth_2");
add("auth_3");
add("auth_4");
}
};
@Override
public void initialize(DynamicPropertyVerification dynamicPropertyVerification) {
// 通过zk等获取远程配置,或加载本地配置(这个看情况了)
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
/
/ 判断需要校验的属性属于单个属性值,还是集合属性值
// 这⾥只针对"Admin"与["auth_1","auth_3","auth_2"]这样的格式进⾏校验
if (JSON.isValidArray(value)) {
// 需要校验的属性,是⼀个集合类型(如权限列表)
List<String> requestValueList = JSON.parArray(value, String.class);
boolean result = requestValueList.stream()
.allMatch(requestValue -> isValidRequestValue(requestValue));
return result;
} el {
// 需要校验的属性,是⼀个单⼀属性字符串(如gender)
boolean result = isValidRequestValue(value);
return result;
}
}
private boolean isValidRequestValue(final String value) {
return REX_LIST.stream()
.anyMatch(legalValue ->legalValue.equals(value));
}
}
⾸先这个注解是真实项⽬的代码,是我参与的蚂蚁⾦服某项⽬的商业平台代码。
为了实现商业化SDK,便需要后端⾃⾏负责数据校验。正好当时这块的负责⼈希望规范代码,所以就交给我,通过统⼀的Validation框架进⾏数据校验。
不过这个代码很快就增加禁⽌字段等,并通过接⼝实现了逻辑上的关注点分离。
之所以没有引⼊完整版,⼀⽅⾯完整代码,代码量较多,放在这⾥会造成主题的偏移。另⼀⽅⾯,完整代码涉及内部的⼀些配置服务,不⽅便泄露。
五,扩展
1.核⼼属性解释
message:异常消息。在校验失败时,返回的message。通常会将校验失败时的异常消息,甚⾄是异常类型等放在这⾥(异常堆栈,是可以通过校验失败时抛出的BindException获取)。
groups:分组信息。通过该属性,进⾏分组校验。详见中级应⽤:分组信息部分。
payload:有效负载。⽤于保存⼀些关键信息。
其实上述三个核⼼属性,最为神秘的,就是payload属性。⼀⽅⾯,这个属性⽤得最少,绝⼤部分⼈都不会使⽤。另⼀⽅⾯,国内的百度很难找到这⽅⾯资料。
我在百度的前两页,都看不到⼏个相关的解释。即使有解释,也只是⼀句⼲巴巴的有效负载(其实就是翻译过来,具体功能和这个没太⼤关系)。百度中只有两条博客,提到payload可以作为⽤户校验,以及元数据。⽽⼀些Validation框架的教学视频,也⼤多⼀笔带过。最后还是在⾕歌上找到较为全⾯的解释。。。
小学生科学小发明
2.payload的实践应⽤
我之前使⽤Validation框架,也没有使⽤这个注解。直到在蚂蚁某项⽬推进数据校验规范时,才去深⼊了解它。还有⼀个⽐较重要的原因,当时⼀⽅⾯需要在message中保存⾃定义的异常信息,另⼀⽅⾯需要保存错误类型的Code(系统有⼀个专门的异常Enum),从⽽对接阿⾥内部的国际化⽂案平台-美杜莎(特意查了⼀些,外⽹是有资料的。囧)。
那么需要保存的信息就不⽌两处。如果通过Json配合BO的⽅式,就有些复杂化了,⽽且显得⽐较重(尤其是有更好的⽅案)。前期不了解payload的情况下,就通过BindExcpetion的解析,获取所需的核⼼信息,放弃⾮核⼼的信息。那么在了解payload后,问题就简单了。直接通过payload配合对应Payload接⼝的⼦接⼝,可以保存所需的信息。
之后有机会,可以考虑写⼀篇博客,来谈谈有关payload的实践应⽤。
3.BindException的解析
先上图,可以看到BindException继承Exception,实现了BindingResult接⼝。
Exception,相信⼤家都熟悉,那么就直接上BindingResult接⼝吧。
⾄于最终效果如何,可以看下图。
从上图的红框,我都不⽤展⽰具体注解应⽤,⼤家就懂了。很明显是⼀个inclinaionOrigin的对象上,有⼀个属性dataId没有通过@NotNull注解的校验。并且还可以从上图中找到@NotNull注解的message等信息,以及异常堆栈的追踪信息。
并且由于返回异常信息的格式固定,所以可以直接通过对BindException的解析,来获取所需的绝⼤部分异常信息。
六,总结
简单来说,就五点:
1. 尽量使⽤Validation框架⾃带的注解。
2. 使⽤⾃定义注解前,想想是否可以通过@Pattern解决问题。
3. payload其实类似groups,不过对应的接⼝需要继承Payload接⼝。
4. Validation框架校验失败时,抛出的BindException,包含绝⼤部分所需的异常信息。
5. Validation框架是优秀的数据校验规范的落实⽅案,配合全局异常处理等,更棒。
最后,愿与诸君共进步。
七,附录
参考