这篇博客只说一下Validation框架的应用,不涉及相关JSR,相关理论,以及源码的解析。html
若是以后须要的话,会再开博客描写,这样会显得主题突出一些。java
后续扩展部分会解释message,groups,payload三个核心属性等。git
自定义注解部分,会给出蚂蚁金服内部真实采用的自定义校验注解。web
简单来讲,就是经过Validation框架,进行数据的各种校验。从Java的基本数据类型到自定义封装数据类型,从非空判断到正则表达式判断,都是Validation框架所支持的。正则表达式
在Validation以前,层次架构中,开发者老是采用分层验证模型。就是分别在控制层,服务层,数据层等分别对目标对象的目标属性进行校验。很明显,这是很是不优雅的,并且开发效率低,由于存在大量重复校验逻辑。spring
而Validation则提出一个元数据验证模型,而在Spring体系中,则表现为Java Bean验证模型。站在Spring角度来讲,不管是在哪一个层次,都是针对Java Bean进行验证的。因此,Validation则经过在目标Bean上添加约束注解,以及背后的验证程序,实现了一个对业务代码无侵入的校验功能。编程
<!-- Validation 相关依赖 --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
这是Validation框架的核心依赖。json
该依赖是包含在SpringBoot的spring-boot-web-starter中的。因此若是使用了前面Spring-boot-web-starter依赖,则不须要再次引入Validation框架的依赖。后端
至于EL等依赖,经常使用于自定义注解,具体能够根据须要进行依赖引入。api
针对目标Bean,针对不一样属性的验证需求,添加不一样的约束注解。
如UserVo的userId,添加@NotNull注解,表示这个属性在验证框架中不可为空。
有关约束注解,后面有详尽描述。
即便对元数据模型添加了约束注解,可是尚未明确开启验证流程。站在Validation框架的角度,它并不知道应该在何时进行校验。由于除了控制层,咱们还可能在服务层验证。即便是在服务层,一个调用链路,可能涉及多个方法,也须要肯定在哪一个方法进行验证。
那么,开启验证的方法有两种(也许还有别的方法,欢迎补充):
@Validated注解的效果与@Valid是同样的,毕竟@Validated是SpringBoot对@Valid注解的封装(@Valid是Java的自带的注解)。而@Validated注解是包含在SpringBoot的spring-boot-web-starter中的。
在对应位置添加@Validated注解(当程序执行到这里,就会执行对应的校验逻辑):
@PostMapping("save.do") @ResponseBody public ServerResponse saveConfig(@Validated(InclinationConfig.ConfigCommitGroup.class) InclinationConfig inclinationConfig) { // 业务逻辑 }
@Validated public class demo { @PostMapping("get.do") @ResponseBody public ServerResponse getConfig(int configId) { // 业务逻辑 } }
针对Java基本数据类型的@NotNull,则须要将对应类上添加@Validated注解。
初始化,创建验证器对象(Validator对象):
// 验证器对象 private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
获取验证结果集合(这里也就是开启验证的时间位置):
// 验证结果集合 private Set<ConstraintViolation<UserInfo>> set = validator.validate(userInfo); // 验证过程能够添加分组信息 private Set<ConstraintViolation<UserInfo>> set = validator.validate(userInfo,UserInfo.RegisterGroup.class);
处理验证结果集合:
set.forEach(item -> { // 输出验证错误信息 System.out.println(item.getMessage()); });
固然啦。更多状况下,咱们是直接抛出异常的:
// 判断验证结果集是否为空(验证结果集放的都是验证失败时的message) if(!CollectionUtils.isEmpty(set)) { // 循环时,采用StringBuilder能够有效提升效率(详见String,StringBuilder,StringBuffer三者区别) StringBuilder exceptionMessage = new StringBuilder(); set.forEach(validationItem -> { exceptionMessage.append(validationItem.getMessage()); }); // 直接抛出异常(其实这也就是@Valid注解的默认校验器的作法) throw new Exception(exceptionMessage.toStrring()); }
这里给出了Validation框架(validation-api-2.0.1.Final)中constraints下所有的注解说明:
空值校验:
范围校验:
其余校验:
上面有关NotNull,NotEmpty,NotBlank,能够参考StringUtils的相似API。
另外,就是上述的@Pattern注解,能够说是最为灵活的注解。许多自定义注解,其实均可以经过@Pattern注解实现。
我认为Validation框架的中级应用有三个:
首先强调一点,正常状况下,经常使用约束注解配合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 自定义动态属性校验约束注解 */ @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.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 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.parseArray(value, String.class); boolean result = requestValueList.stream() .allMatch(requestValue -> isValidRequestValue(requestValue)); return result; } else { // 须要校验的属性,是一个单一属性字符串(如gender) boolean result = isValidRequestValue(value); return result; } } private boolean isValidRequestValue(final String value) { return REX_LIST.stream() .anyMatch(legalValue ->legalValue.equals(value)); } }
首先这个注解是真实项目的代码,是我参与的蚂蚁金服某项目的商业平台代码。
为了实现商业化SDK,便须要后端自行负责数据校验。正好当时这块的负责人但愿规范代码,因此就交给我,经过统一的Validation框架进行数据校验。
不过这个代码很快就增长禁止字段等,并经过接口实现了逻辑上的关注点分离。
之因此没有引入完整版,一方面完整代码,代码量较多,放在这里会形成主题的偏移。另外一方面,完整代码涉及内部的一些配置服务,不方便泄露。
其实上述三个核心属性,最为神秘的,就是payload属性。一方面,这个属性用得最少,绝大部分人都不会使用。另外一方面,国内的百度很难找到这方面资料。
我在百度的前两页,都看不到几个相关的解释。即便有解释,也只是一句干巴巴的有效负载(其实就是翻译过来,具体功能和这个没太大关系)。百度中只有两条博客,提到payload能够做为用户校验,以及元数据。而一些Validation框架的教学视频,也大多一笔带过。最后仍是在谷歌上找到较为全面的解释。。。
我以前使用Validation框架,也没有使用这个注解。直到在蚂蚁某项目推动数据校验规范时,才去深刻了解它。还有一个比较重要的缘由,当时一方面须要在message中保存自定义的异常信息,另外一方面须要保存错误类型的Code(系统有一个专门的异常Enum),从而对接阿里内部的国际化文案平台-美杜莎(特地查了一些,外网是有资料的。囧)。
那么须要保存的信息就不止两处。若是经过Json配合BO的方式,就有些复杂化了,并且显得比较重(尤为是有更好的方案)。前期不了解payload的状况下,就经过BindExcpetion的解析,获取所需的核心信息,放弃非核心的信息。那么在了解payload后,问题就简单了。直接经过payload配合对应Payload接口的子接口,能够保存所需的信息。
以后有机会,能够考虑写一篇博客,来谈谈有关payload的实践应用。
先上图,能够看到BindException继承Exception,实现了BindingResult接口。
Exception,相信你们都熟悉,那么就直接上BindingResult接口吧。
至于最终效果如何,能够看下图。
从上图的红框,我都不用展现具体注解应用,你们就懂了。很明显是一个inclinaionOrigin的对象上,有一个属性dataId没有经过@NotNull注解的校验。而且还能够从上图中找到@NotNull注解的message等信息,以及异常堆栈的追踪信息。
而且因为返回异常信息的格式固定,因此能够直接经过对BindException的解析,来获取所需的绝大部分异常信息。
简单来讲,就五点:
最后,愿与诸君共进步。