地方网站怎么做挣钱,优化法治化营商环境,北京专业做网站公司,沈阳微网站建设十五分钟#xff0c;学会Validation框架的全面应用
一#xff0c;前言
这篇博客只说一下Validation框架的应用#xff0c;不涉及相关JSR#xff0c;相关理论#xff0c;以及源码的解析。
如果之后需要的话#xff0c;会再开博客描写#xff0c;这样会显得主题突出一些…十五分钟学会Validation框架的全面应用
一前言
这篇博客只说一下Validation框架的应用不涉及相关JSR相关理论以及源码的解析。
如果之后需要的话会再开博客描写这样会显得主题突出一些。
后续扩展部分会解释messagegroupspayload三个核心属性等。
自定义注解部分会给出蚂蚁金服内部真实采用的自定义校验注解。
二简介
简单来说就是通过Validation框架进行数据的各类校验。从Java的基本数据类型到自定义封装数据类型从非空判断到正则表达式判断都是Validation框架所支持的。
在Validation之前层次架构中开发者总是采用分层验证模型。就是分别在控制层服务层数据层等分别对目标对象的目标属性进行校验。很明显这是非常不优雅的而且开发效率低因为存在大量重复校验逻辑。
而Validation则提出一个元数据验证模型而在Spring体系中则表现为Java Bean验证模型。站在Spring角度来说无论是在哪个层次都是针对Java Bean进行验证的。所以Validation则通过在目标Bean上添加约束注解以及背后的验证程序实现了一个对业务代码无侵入的校验功能。
三使用方法
1.添加依赖
!-- Validation 相关依赖 --dependencygroupIdjavax.validation/groupIdartifactIdvalidation-api/artifactIdversion2.0.1.Final/version/dependency
这是Validation框架的核心依赖。
该依赖是包含在SpringBoot的spring-boot-web-starter中的。所以如果使用了前面Spring-boot-web-starter依赖则不需要再次引入Validation框架的依赖。
至于EL等依赖常用于自定义注解具体可以根据需要进行依赖引入。
2.添加约束注解
针对目标Bean针对不同属性的验证需求添加不同的约束注解。
如UserVo的userId添加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)ResponseBodypublic ServerResponse saveConfig(Validated(InclinationConfig.ConfigCommitGroup.class) InclinationConfig inclinationConfig) {// 业务逻辑}
基本数据类型 Validatedpublic class demo {PostMapping(get.do)ResponseBodypublic ServerResponse getConfig(int configId) {// 业务逻辑}}
针对Java基本数据类型的NotNull则需要将对应类上添加Validated注解。
验证器
初始化建立验证器对象Validator对象
// 验证器对象private Validator validator Validation.buildDefaultValidatorFactory().getValidator();
获取验证结果集合这里也就是开启验证的时间位置
// 验证结果集合private SetConstraintViolationUserInfo set validator.validate(userInfo);// 验证过程可以添加分组信息private SetConstraintViolationUserInfo set validator.validate(userInfo,UserInfo.RegisterGroup.class);
处理验证结果集合
set.forEach(item - {// 输出验证错误信息System.out.println(item.getMessage());});
当然啦。更多情况下我们是直接抛出异常的
// 判断验证结果集是否为空验证结果集放的都是验证失败时的messageif(!CollectionUtils.isEmpty(set)) {// 循环时采用StringBuilder可以有效提高效率详见String,StringBuilder,StringBuffer三者区别StringBuilder exceptionMessage new StringBuilder();set.forEach(validationItem - {exceptionMessage.append(validationItem.getMessage());});// 直接抛出异常其实这也就是Valid注解的默认校验器的做法throw new Exception(exceptionMessage.toStrring());}
四约束注解
1.初级应用常用注解
这里给出了Validation框架validation-api-2.0.1.Final中constraints下全部的注解说明 空值校验 Null目标值为null。比如注册时的userId当然是null即使不为null系统也不会采用的。NotNull目标值不为null。比如登录时的userId当然不为null当然也可能是通过了外部鉴权然后内部裸奔。NotEmpty目标值不为empty。相较于上者增加了对空值的判断就是无法通过NotEmpty的校验NotBlank目标值不为blank。相较于上者增加了对空格的判断就是空格无法通过NotBlank校验的 范围校验 Min针对数值类型目标值不能低于该注解设定的值。Max针对数值类型目标值不能高于该注解设定的值。Size针对集合类型目标集合的元素数量不可以高于max参数不可以低于min参数。Digits针对数值类型目标值的整数位数必须等于integer参数设定的值小数位数必须等于fraction参数设定的值。DecimalMax针对数值类型目标值必须小于该注解设定的值。DecimalMin针对数值类型目标值必须大于该注解设定的值。Past针对于日期类型目标值必须是一个过去的时间。PastOrPresent针对于日期类型目标值必须是一个过去或现在的时间。Future针对于日期类型目标值必须是未来的时间。FutureOrPresent:针对于日期类型目标值必须是未来或未来的时间。Negative针对数值类型目标值必须是负数。NegativeOrZero针对数值类型目标值必须是非正数。Positive针对数值类型目标值必须是正数。PositiveOrZero针对数值类型目标值必须是非负数。 其他校验 AssertTrue针对布尔类型目标值必须为true。AssertFalse针对布尔类型目标值必须为false。Email针对字符串类型目标值必须是Email格式。URL针对字符串类型目标值必须是URL格式。Pattern针对字符串类型目标值必须通过注解设定的正则表达式。
上面有关NotNull,NotEmpty,NotBlank可以参考StringUtils的类似API。 另外就是上述的Pattern注解可以说是最为灵活的注解。许多自定义注解其实都可以通过Pattern注解实现。
2.中级应用级联分组序列
我认为Validation框架的中级应用有三个
级联验证通过Valid注解实现级联校验。举个例子我的ScriptionBO中有一个List属性。我希望Validation框架在校验ScriptionBO的时候不仅仅校验ScriptionBO的属性还要验证其中List涉及的User们。那么在List上添加Valid注解就可以实现了。分组校验通过分组Interface与校验注解的group参数就可以实现分组校验。举个例子同样是User实体类既需要满足登录验证有userId这样的属性也需要满足注册验证不需要userId这样的属性。那么可以在User实体类中建立用于登录场景的interface LoginGroup {}接口与用于注册场景的interface RegisterGroup {}。在userId属性上增加非空校验的NotNull(groups LoginGroup.class)就可以实现了。分组序列通过分组校验再加上GroupSequence({xxxGroup.class,xxxGroup.class})就可以实现分组序列了。举个例子登录场景下User连userId的非空校验都没有通过那么就更不需要校验手机号码邮箱等。
3.高级应用自定义校验注解
首先强调一点正常情况下常用约束注解配合Validation框架的中级应用足以应付大多数情况。尤其是Pattern注解采用了灵活的正则表达式可以解决大部分复杂问题。
举个例子正常的Email地址校验可以通过Email注解进行校验更可以通过Pattern实现更为精准的校验。至于自定义校验注解则可以实现根据配置动态验证Email地址的功能。
自定义校验注解其实就类似于配合自定义注解的切面编程只不过利用了Validation框架的一些基础方法。
自定义校验注解分为以下三步
约束注解的定义。约束验证规则即自定义约束校验器关联约束注解与约束规则
为了更直观的感受这里给出一个简单的demo。
另外这里的依赖需要单独引入能只依靠springboot自带的validation依赖。
约束注解定义
package tech.jarry.learning.demo.common.anno;import javax.validation.Constraint;import javax.validation.Payload;import java.lang.annotation.*;/*** author jarry* description 自定义动态属性校验约束注解*/DocumentedTarget(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.learning.demo.common.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 ConstraintValidatorDynamicPropertyVerification, String {// 为了便于进行测试这里先放入一些本地数据private static final ListString REX_LIST new ArrayListString() {{add(auth_1);add(auth_2);add(auth_3);add(auth_4);}};Overridepublic void initialize(DynamicPropertyVerification dynamicPropertyVerification) {// 通过zk等获取远程配置或加载本地配置这个看情况了}Overridepublic boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {// 判断需要校验的属性属于单个属性值还是集合属性值// 这里只针对Admin与[auth_1,auth_3,auth_2]这样的格式进行校验if (JSON.isValidArray(value)) {// 需要校验的属性是一个集合类型如权限列表ListString requestValueList JSON.parseArray(value, String.class);boolean result requestValueList.stream().allMatch(requestValue - isValidRequestValue(requestValue));return result;} else {// 需要校验的属性是一个单一属性字符串如genderboolean 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的解析来获取所需的绝大部分异常信息。
六总结
简单来说就五点
尽量使用Validation框架自带的注解。使用自定义注解前想想是否可以通过Pattern解决问题。payload其实类似groups不过对应的接口需要继承Payload接口。Validation框架校验失败时抛出的BindException包含绝大部分所需的异常信息。Validation框架是优秀的数据校验规范的落实方案配合全局异常处理等更棒。
最后愿与诸君共进步。
七附录
参考
告别996 实现高效编程 减少开发压力Bean Validation specificationValid与Validated注解Validated和Valid区别…JavaBean Validation - javax.validation.Payload ExamplesJavaBean Validation - Constraint payloadsChapter 3. Creating custom constraints