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,针对不同属性的验证需求,添加不同的约束注解。
如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")
@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());
}
四,约束注解
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 自定义动态属性校验约束注解
*/
@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框架进行数据校验。
不过这个代码很快就增加禁止字段等,并通过接口实现了逻辑上的关注点分离。
之所以没有引入完整版,一方面完整代码,代码量较多,放在这里会造成主题的偏移。另一方面,完整代码涉及内部的一些配置服务,不方便泄露。
五,扩展
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 specification
- @Valid与@Validated注解
- @Validated和@Valid区别...
- JavaBean Validation - javax.validation.Payload Examples
- JavaBean Validation - Constraint payloads
- Chapter 3. Creating custom constraints
Validation框架的应用的更多相关文章
- Java参数验证Bean Validation 框架
1.为什么要做参数校验? 参数校验和业务逻辑代码分离,参数校验代码复用,统一参数校验方式.校验不太通过时统一异常描述. 2.bean validation规范 JSR303 规范(Bean Valid ...
- Silverlight实例教程 - 自定义扩展Validation类,验证框架的总结和建议(转载)
Silverlight 4 Validation验证实例系列 Silverlight实例教程 - Validation数据验证开篇 Silverlight实例教程 - Validation数据验证基础 ...
- @Validated和@Valid区别:Spring validation验证框架对入参实体进行嵌套验证必须在相应属性(字段)加上@Valid而不是@Validated
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR- ...
- Struts2 Validation学习
Every input is evil! ------------------------------华丽的分割线----------------------------------- 客户端提交的数 ...
- JSR 303 - Bean Validation 介绍及最佳实践
JSR 303 - Bean Validation 介绍及最佳实践 JSR 303 – Bean Validation 是一个数据验证的规范,2009 年 11 月确定最终方案.2009 年 12 月 ...
- Spring3.1 对Bean Validation规范的新支持(方法级别验证)
上接Spring提供的BeanPostProcessor的扩展点-1继续学习. 一.Bean Validation框架简介 写道Bean Validation standardizes constra ...
- 使用spring validation完成数据后端校验
前言 数据的校验是交互式网站一个不可或缺的功能,前端的js校验可以涵盖大部分的校验职责,如用户名唯一性,生日格式,邮箱格式校验等等常用的校验.但是为了避免用户绕过浏览器,使用http工具直接向后端请求 ...
- java bean validation 参数验证
一.前言 二.几种解决方案 三.使用bean validation 自带的注解验证 四.自定义bean validation 注解验证 一.前言 在后台开发过程中,对参数的校验成为开发环境不可缺少的一 ...
- JSR教程1——JSR 303 - Bean Validation介绍
1.Bean Validation 在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情.应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的.在通常的情况下, ...
随机推荐
- elasticsearch 单机安装
一.elasticsearch下载 1.elastic 官网:https://www.elastic.co/cn/ 2.elasticsearch 下载地址:https://www.elastic.c ...
- 网络编程技术-----6、I/O复用实现并发服务器
网络编程技术-----6.I/O复用实现并发服务器 一.实验要求 服务器: 服务器等待接收客户的连接请求,一旦连接成功则显示客户地址,接着接收客户端的名称并显示:然后接收来自该客户的字符串,对 ...
- 操作系统-schedule函数
1. Linux 0.11的调度函数schedule() 也就是找到了counter最大的进程,然后就跳出去执行switch_to,对应上面的优先级算法,而counter本身也是时间片,也作了轮转调度 ...
- DBA_Oracle DBA常用表汇总(概念)--转载
https://www.cnblogs.com/eastsea/p/3799411.html 一.与权限相关的字典 ALL_COL_PRIVS表示列上的授权,用户和PUBLIC是被授予者 ALL_C ...
- vue-cli2.0项目 添加骨架屏
1.创建项目 npm init webpack project 3.下载 vue-skeleton-webpack-plugin 插件 npm install vue-skeleton-webpa ...
- mysql中实现更新数据+1,再次更新数据-1
$sql="update article set comment=comment^1 where a_id=2"; 这条语句你会发现当你更新comment字段时为1,再次更新时为0 ...
- DJango中开启事务的两种方式
目录 Django中开启事务的两种方式 第一种 第二种 Django中开启事务的两种方式 第一种 from django.db import transaction with transaction. ...
- 【vue生命周期】- 详解
这篇文章通俗易懂,写的不错,本文转载至:https://www.cnblogs.com/happ0/p/8075562.html 详解Vue Lifecycle 先来看看vue官网对vue生命周期的介 ...
- Effective Python读书笔记
有些位置可能翻译理解的不到位,各位看官如有疑问,欢迎留言赐教. Pythonic Thinking 大家经常用Pythonic来形容python语法风格的编程方式:简单优美,没有之一:通过import ...
- RestFul API 统一格式返回 + 全局异常处理
一.背景 在分布式.微服务盛行的今天,绝大部分项目都采用的微服务框架,前后端分离方式.前端和后端进行交互,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端. ...