乔丹是我听过的篮球之神,科比是我亲眼见过的篮球之神。本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT的乌托邦】逐个击破,深入掌握,拒绝浅尝辄止。

✍前言

你好,我是YourBatman。

作为一个开发者,聊起数据校验(Bean Validation),不管是前、中、后端都耳熟能详,并且心里暗爽:so easy。

的确,对数据做校验是一个程序员的基本素质,它不难但发生在我们程序的几乎每个角落,就像下面这幅图所示:每一层都需要做校验



如果你真的这么去写代码的话(每一层都写一份),肯定是不太合适的,良好的状态应该如下图所示:

作为一个Java开发者,在Spring大行其道的今天,很多小伙伴了解数据校验来自于Spring MVC场景,甚至止步于此。殊不知,Java EE早已把它抽象成了JSR标准技术,并且Spring还是借助整合它完成了自我救赎呢。

在我看来,按Spring的3C战略标准来比,Bean Validation数据校验这块是没有能够完成对传统Java EE的超越,自身设计存在过重、过度设计等特点。

本专栏命名为Bean Validation(数据校验),将先从JSR标准开始,再逐渐深入到具体实现Hibernate Validation、整合Spring使用场景等等。因此本专栏将让你将得到一份系统数据校验的知识。

✍正文

在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的,比如生日必须是过去时,年龄必须>0等等。

为什么要有数据校验?

数据校验是非常常见的工作,在日常的开发中贯穿于代码的各个层次,从上层的View层到后端业务处理层,甚至底层的数据层。

我们知道通常情况下程序肯定是分层的,不同的层可能由不同的人来开发或者调用。若你是一个有经验的程序员,我相信你肯定见过在不同的层了都出现了相同的校验代码,这就是某种意义上的垃圾代码

public String queryValueByKey(String zhName, String enName, Integer age) {
checkNotNull(zhName, "zhName must be not null");
checkNotNull(enName, "enName must be not null");
checkNotNull(age, "age must be not null");
validAge(age, "age must be positive");
...
}

从这个简单的方法入参校验至少能发现如下问题:

  1. 需要写大量的代码来进行参数基本验证(这种代码多了就算垃圾代码)
  2. 需要通过文字注释来知道每个入参的约束是什么(否则别人咋看得懂)
  3. 每个程序员做参数验证的方式可能不一样,参数验证抛出的异常也不一样,导致后期几乎没法维护

如上会导致代码冗余和一些管理的问题(代码量越大,管理起来维护起来就越困难),比如说语义的一致性问题。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定,这就是本文将要提供的一个新思路:Bean Validation

关于Jakarta EE

2018年03月, Oracle 决定把 JavaEE 移交给开源组织 Eclipse 基金会,并且不再使用Java EE这个名称。这是它的新logo:

对应的名称修改还包括:

旧名称 新名称
Java EE Jakarta EE
Glassfish Eclipse Glassfish
Java Community Process (JCP) Eclipse EE.next Working Group (EE.next)
Oracle development management Eclipse Enterprise for Java (EE4J) 和 Project Management Committee (PMC)

JCP 将继续支持 Java SE社区。 但是,Jakarta EE规范自此将不会在JCP下开发。Jakarta EE标准大概由Eclipse Glassfish、Apache TomEE、Wildfly、Oracle WebLogic、JBoss、IBM、Websphere Liberty等组织来制定

迁移

既然名字都改了,那接下来就是迁移喽,毕竟Java EE这个名称(javax包名)不能再用了嘛。Eclipse接手后发布的首个Enterprise Java将是 Jakarta EE 9,该版本将以Java EE 8作为其基准版本(最低版本要求是Java8)。

有个意思的现象是:Java EE 8是2019.09.10发布的,但实际上官方名称是Jakarta EE 8了。很明显该版本并非由新组织设计和制定的,不是它们的产物。但是,彼时平台已更名为Jakarta有几个月了,因此对于一些Jar你在maven市场上经常能看见两种坐标:

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency> <dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.1</version>
</dependency>

虽然坐标不一样,但是内容是100%一样的(包名均还为javax.*),很明显这是更名的过度期,为后期全面更名做准备呢。

严格来讲:只要大版本号(第一个数字)还一样,包名是不可能变化的,因此一般来说均具有向下兼容性

既然Jakarta释放出了更名信号,那么下一步就是彻彻底底的改变喽。果不其然,这些都在Jakarta EE 9里得到实施。

Jakarta EE 9

2020.08.31,Jakarta后的第一个企业级平台Jakarta EE 9正式发布。如果说Jakarta EE 8只是冠了个名,那么这个就名正言顺了。

小贴士:我写本文时还没到2020.08.31呢,这个时间是我在官网趴来的,因此肯定准确

这次企业平台的升级最大的亮点是:

  1. 把旗下30于种技术的大版本号全部+1(Jakarta RESTful Web Services除外)
  2. 包名全部javax.*化,全部改为jakarta.*
  3. JavaSE基准版本要求依旧保持为Java 8(而并非Java9哦)

可以发现本次升级的主要目的并着眼于功能点,仍旧是名字的替换。虽然大家对Java EE的javax有较深的情节,但旧的不去新的不来。我们以后开发过中遇到jakarta.*这种包名就不用再感到惊讶了,提前准备总是好的。

Jakarta Bean Validation

Jakarta Bean Validation不仅仅是一个规范,它还是一个生态。

之前名为Java Bean Validation,2018年03月之后就得改名叫Jakarta Bean Validation

喽,这不官网早已这么称呼了:

Bean Validation技术隶属于Java EE规范,期间有多个JSR(Java Specification Requests)支持,截止到稿前共有三次JSR标准发布:



说明:JCP这个组织就是来定义Java标准的,在Java行业鼎鼎有名的公司大都是JCP的成员,可以共同参与Java标准的制定,影响着世界。包括掌门人Oracle以及Eclipse、Redhat、JetBrains等等。值得天朝人自豪的是:2018年5月17日阿里巴巴作为一员正式加入JCP组织,成为唯一一家中国公司

Bean Validation是标准,它的参考实现除了有我们熟悉的Hibernate Validator外还有Apache BVal,但是后者使用非常小众,忘了它吧。实际使用中,基本可以认为Hibernate Validator是Bean Validation规范的唯一参考实现,是对等的。

小贴士:Apache BVal胜在轻量级上,只有不到1m空间所以非常轻量,有些选手还是忠爱的(此项目还在发展中,并未停更哦,有兴趣你可以自己使用试试)

JSR303

这个JSR提出很早了(2009年),它为 基于注解的 JavaBean验证定义元数据模型和API,通过使用XML验证描述符覆盖和扩展元数据。JSR-303主要是对JavaBean进行验证,如方法级别(方法参数/返回值)、依赖注入等的验证是没有指定的。

作为开山之作,它规定了Java数据校验的模型和API,这就是Java Bean Validation 1.0版本

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
</dependency>

该版本提供了常见的校验注解(共计13个):

注解 支持类型 含义 null值是否校验
@AssertFalse bool 元素必须是false
@AssertTrue bool 元素必须是true
@DecimalMax Number的子类型(浮点数除外)以及String 元素必须是一个数字,且值必须<=最大值
@DecimalMin 同上 元素必须是一个数字,且值必须>=最大值
@Max 同上 同上
@Min 同上 同上
@Digits 同上 元素构成是否合法(整数部分和小数部分)
@Future 时间类型(包括JSR310) 元素必须为一个将来(不包含相等)的日期(比较精确到毫秒)
@Past 同上 元素必须为一个过去(不包含相等)的日期(比较精确到毫秒)
@NotNull any 元素不能为null
@Null any 元素必须为null
@Pattern 字符串 元素需符合指定的正则表达式
@Size String/Collection/Map/Array 元素大小需在指定范围中

所有注解均可标注在:方法、字段、注解、构造器、入参等几乎任何地方

可以看到这些注解均为平时开发中比较常用的注解,但是在使用过程中有如下事项你仍旧需要注意:

  1. 以上所有注解对null是免疫的,也就是说如果你的值是null,是不会触发对应的校验逻辑的(也就说null是合法的),当然喽@NotNull / @Null除外
  2. 对于时间类型的校验注解(@Future/@Past),是开区间(不包含相等)。也就是说:如果相等就是不合法的,必须是大于或者小于
    1. 这种case比较容易出现在LocalDate这种只有日期上面,必须是将来/过去日期,当天属于非法日期
  3. @Digits它并不规定数字的范围,只规定了数字的结构。如:整数位最多多少位,小数位最多多少位
  4. @Size规定了集合类型的范围(包括字符串),这个范围是闭区间
  5. @DecimalMax和@Max作用基本类似,大部分情况下可通用。不同点在于:@DecimalMax设置最大值是用字符串形式表示(只要合法都行,比如科学计数法),而@Max最大值设置是个long值
    1. 我个人一般用@Max即可,因为够用了~

另外可能有人会问:为毛没看见@NotEmpty、@Email、@Positive等常用注解?那么带着兴趣和疑问,继续往下看吧~

JSR349

该规范是2013年完成的,伴随着Java EE 7一起发布,它就是我们比较熟悉的Bean Validation 1.1。

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>

相较于1.0版本,它主要的改进/优化有如下几点:

  1. 标准化了Java平台的约束定义、描述、和验证
  2. 支持方法级验证(入参或返回值的验证)
  3. Bean验证组件的依赖注入
  4. 与上下文和DI依赖注入集成
  5. 使用EL表达式的错误消息插值,让错误消息动态化起来(强依赖于ElManager)
  6. 跨参数验证。比如密码和验证密码必须相同

小贴士:注解个数上,相较于1.0版本并没新增~

它的官方参考实现如下:



可以看到,Java Bean Validation 1.1版本实现对应的是Hibernate Validator 5.x(1.0版本对应的是4.x)

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.3.Final</version>
</dependency>

当你导入了hibernate-validator后,无需再显示导入javax.validation。hibernate-validator 5.x版本基本已停更,只有严重bug才会修复。因此若非特殊情况,不再建议你使用此版本,也就是不建议再使用Bean Validation 1.1版本,更别谈1.0版本喽。

小贴士:Spring Boot1.5.x默认集成的还是Bean Validation 1.1哦,但到了Boot 2.x后就彻底摒弃了老旧版本

JSR380

当下主流版本,也就是我们所说的Java Bean Validation 2.0Jakarta Bean Validation 2.0版本。关于这两种版本的差异,官方做出了解释:



他俩除了叫法不一样、除了GAV上有变化,其它地方没任何改变。它们各自的GAV如下:

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency> <dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.1</version>
</dependency>

现在应该不能再叫Java EE了,而应该是Jakarta EE。两者是一样的意思,你懂的。Jakarta Bean Validation 2.0是在2019年8月发布的,属于Jakarta EE 8的一部分。它的官方参考实现只有唯一的Hibernate validator了:

此版本具有很重要的现实意义,它主要提供如下亮点:

  1. 支持通过注解参数化类型(泛型类型)参数来验证容器内的元素,如:List<@Positive Integer> positiveNumbers

    1. 更灵活的集合类型级联验证;例如,现在可以验证映射的值和键,如:Map<@Valid CustomerType, @Valid Customer> customersByType
    2. 支持java.util.Optional类型,并且支持通过插入额外的值提取器来支持自定义容器类型
  2. 让@Past/@Future注解支持注解在JSR310时间上
  3. 新增内建的注解类型(共9个):@Email, @NotEmpty, @NotBlank, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent和@FutureOrPresent
  4. 所有内置的约束现在都支持重复标记
  5. 使用反射检索参数名称,也就是入参名,详见这个API:ParameterNameProvider
    1. 很明显这是需要Java 8的启动参数支持的
  6. Bean验证XML描述符的名称空间已更改为:
    1. META-INF/validation.xml -> http://xmlns.jcp.org/xml/ns/validation/configuration
    2. mapping files -> http://xmlns.jcp.org/xml/ns/validation/mapping
  7. JDK最低版本要求:JDK 8

Hibernate Validator自6.x版本开始对JSR 380规范提供完整支持,除了支持标准外,自己也做了相应的优化,比如性能改进、减少内存占用等等,因此用最新的版本肯定是没错的,毕竟只会越来越好嘛。

新增注解

相较于1.x版本,2.0版本在其基础上新增了9个实用注解,总数到了22个。现对新增的9个注解解释如下:

注解 支持类型 含义 null值是否校验
@Email 字符串 元素必须为电子邮箱地址
@NotEmpty 容器类型 集合的Size必须大于0
@NotBlank 字符串 字符串必须包含至少一个非空白的字符
@Positive 数字类型 元素必须为正数(不包括0)
@PositiveOrZero 同上 同上(包括0)
@Negative 同上 元素必须为负数(不包括0)
@NegativeOrZero 同上 同上(包括0)
@PastOrPresent 时间类型 在@Past基础上包括相等
@FutureOrPresent 时间类型 在@Futrue基础上包括相等

@Email、@NotEmpty、@NotBlank之前是Hibernate额外提供的,2.0标准后hibernate自动退位让贤并且标注为过期了。Bean Validation 2.0的JSR规范制定负责人就职于Hibernate,所以这么做就很自然了。就是他:

小贴士:除了JSR标准提供的这22个注解外,Hibernate Validator还提供了一些非常实用的注解,这在后面讲述Hibernate Validator时再解释吧

使用示例

导入实现包:

<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>

校验Java Bean

书写JavaBean和校验程序(全部使用JSR标准API哦):

@ToString
@Setter
@Getter
public class Person { @NotNull
public String name;
@NotNull
@Min(0)
public Integer age;
}
public static void main(String[] args) {
Person person = new Person();
person.setAge(-1); // 1、使用【默认配置】得到一个校验工厂 这个配置可以来自于provider、SPI提供
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
// 2、得到一个校验器
Validator validator = validatorFactory.getValidator();
// 3、校验Java Bean(解析注解) 返回校验结果
Set<ConstraintViolation<Person>> result = validator.validate(person); // 输出校验结果
result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue()).forEach(System.out::println);
}

运行程序,不幸抛错:

Caused by: java.lang.ClassNotFoundException: javax.el.ELManager
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
...

上面说了,从1.1版本起就需要El管理器支持用于错误消息动态插值,因此需要自己额外导入EL的实现。

小贴士:EL也属于Java EE标准技术,可认为是一种表达式语言工具,它并不仅仅是只能用于Web(即使你绝大部分情况下都是用于web的jsp里),可以用于任意地方(类比Spring的SpEL)

这是EL技术规范的API:

<!-- 规范API -->
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>

Expression Language 3.0表达式语言规范发版于2013-4-29发布的,Tomcat 8、Jetty 9、GlasshFish 4都已经支持实现了EL 3.0,因此随意导入一个都可(如果你是web环境,根本就不用自己手动导入这玩意了)。

<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>9.0.22</version>
</dependency>

添加好后,再次运行程序,控制台正常输出校验失败的消息:

age 最小不能小于0: -1
name 不能为null: null

校验方法/校验构造器

请移步下文详解。

加餐:Bean Validation 3.0

伴随着Jakarta EE 9的发布,Jakarta Bean Validation 3.0也正式公诸于世。

<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.0</version>
</dependency>

它最大的改变,甚至可以说唯一的改变就是包名的变化:



至此不仅GAV上实现了更名,对代码执行有重要影响的包名也彻彻底底的去javax.*化了。因为实际的类并没有改变,因此仍旧可以认为它是JSR380的实现(虽然不再由JCP组织制定标准了)。

参考实现

毫无疑问,参考实现那必然是Hibernate Validator。它的步伐也跟得非常的紧,退出了7.x版本用于支持Jakarta Bean Validation 3.0。虽然是大版本号的升级,但是在新特性方面你可认为是

✍总结

本文着眼于讲解JSR规范、Bean Validation校验标准、官方参考实现Hibernate Validator,把它们之间的关系进行了关联,并且对差异进行了鉴别。我认为这篇文章对一般读者来说是能够刷新对数据校验的认知的。

wow,数据校验背后还有这么广阔的天地

数据校验是日常工组中接触非常非常频繁的一块知识点,我认为掌握它并且熟练运用于实际工作中,能起到事半功倍的效果,让代码更加的优雅,甚至还能实现别人加班你加薪呢。所以又是一个投出产出比颇高的小而美专栏在路上......

作为本专栏的第一篇文章以JSR标准作为切入点进行讲解,是希望理论和实践能结合起来学习,毕竟理论的指导作用不可或缺。有了理论铺垫的基石,后面实践将更加流畅,正所谓着地走路更加踏实嘛。

推荐阅读:

1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知的更多相关文章

  1. 服务器文档下载zip格式 SQL Server SQL分页查询 C#过滤html标签 EF 延时加载与死锁 在JS方法中返回多个值的三种方法(转载) IEnumerable,ICollection,IList接口问题 不吹不擂,你想要的Python面试都在这里了【315+道题】 基于mvc三层架构和ajax技术实现最简单的文件上传 事件管理

    服务器文档下载zip格式   刚好这次项目中遇到了这个东西,就来弄一下,挺简单的,但是前台调用的时候弄错了,浪费了大半天的时间,本人也是菜鸟一枚.开始吧.(MVC的) @using Rattan.Co ...

  2. 从0开始搭建SQL Server AlwaysOn 第一篇(配置域控)

    从0开始搭建SQL Server AlwaysOn 第一篇(配置域控) 第一篇http://www.cnblogs.com/lyhabc/p/4678330.html第二篇http://www.cnb ...

  3. Python爬虫小白入门(四)PhatomJS+Selenium第一篇

    一.前言 在上一篇博文中,我们的爬虫面临着一个问题,在爬取Unsplash网站的时候,由于网站是下拉刷新,并没有分页.所以不能够通过页码获取页面的url来分别发送网络请求.我也尝试了其他方式,比如下拉 ...

  4. Three.js 第一篇:绘制一个静态的3D球体

    第一篇就画一个球体吧 首先我们知道Three.js其实是一个3D的JS引擎,其中的强大之处就在于这个JS框架并不是依托于JQUERY来写的.那么,我们在写这一篇绘制3D球体的文章的时候,应该注意哪些地 ...

  5. 深入学习jQuery选择器系列第一篇——基础选择器和层级选择器

    × 目录 [1]id选择器 [2]元素选择器 [3]类选择器[4]通配选择器[5]群组选择器[6]后代选择器[7]兄弟选择器 前面的话 选择器是jQuery的根基,在jQuery中,对事件处理.遍历D ...

  6. 【第一篇】ASP.NET MVC快速入门之数据库操作(MVC5+EF6)

    目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...

  7. Android基础学习第一篇—Project目录结构

    写在前面的话: 1. 最近在自学Android,也是边看书边写一些Demo,由于知识点越来越多,脑子越来越记不清楚,所以打算写成读书笔记,供以后查看,也算是把自己学到所理解的东西写出来,献丑,如有不对 ...

  8. 深入理解ajax系列第一篇——XHR对象

    × 目录 [1]创建对象 [2]发送请求 [3]接收响应[4]异步处理[5]实例演示 前面的话 ajax是asynchronous javascript and XML的简写,中文翻译是异步的java ...

  9. 深入理解javascript对象系列第一篇——初识对象

    × 目录 [1]定义 [2]创建 [3]组成[4]引用[5]方法 前面的话 javascript中的难点是函数.对象和继承,前面已经介绍过函数系列.从本系列开始介绍对象部分,本文是该系列的第一篇——初 ...

随机推荐

  1. bzoj 1515 [POI2006]Lis-The Postman 有向图欧拉回路

    LINK:Lis-The Postman 看完题觉得 虽然容易发现是有向图欧拉回路 但是觉得很难解决这个问题. 先分析一下有向图的欧拉回路:充要条件 图中每个点的入度-出度=0且整张图是一个强连通分量 ...

  2. 探讨Netty获取并检查Websocket握手请求的两种方式

    在使用Netty开发Websocket服务时,通常需要解析来自客户端请求的URL.Headers等等相关内容,并做相关检查或处理.本文将讨论两种实现方法. 方法一:基于HandshakeComplet ...

  3. JS——变量提升和函数提升

    一.引入 在了解这个知识点之前,我们先来看看下面的代码,控制台都会输出什么 var foo = 1; function bar() { if (!foo) { var foo = 10; } aler ...

  4. 10分钟 Castle.Windsor 适配 Asp.Net Core 3.0

    Asp.Net Core 3.0以上,不再能通过修改Starup.ConfigureServices返回值(IServiceProvider),所以只能调用IHostBuilder.UseServic ...

  5. 【BZOJ1471】不相交路径 题解(拓扑排序+动态规划+容斥原理)

    题目描述 在有向无环图上给你两个起点和终点分别为$a,b,c,d$.问有几种路径方案使得能从$a$走到$b$的同时能从$c$走到$d$,且两个路径没有交点. $1\leq n\leq 200,1\le ...

  6. Python要点总结,我使用了100个小例子!

  7. 使用HttpClient 发送 GET、POST(FormData、Raw)、PUT、Delete请求及文件上传

    httpclient4.3.6 package org.caeit.cloud.dev.util; import java.io.File; import java.io.IOException; i ...

  8. XCTF-WEB-高手进阶区-NaNNaNaNNaN-Batman-笔记

    上来直接百度先搜下Batman -_-|| 不存在的传令兵么 本身是下载下来了一个文件web100 打开发现是如下内容 可以看出这个是个脚本语言,因此尝试修改后缀为html,发现是一个OK框 现在是想 ...

  9. C++/C socket编程实例

    目录 基于TCP的socket编程 服务器代码 客户端代码 运行结果 基于UDP的socket编程 服务器代码 客户端代码 运行结果 基于TCP的socket编程 服务器代码 服务器端代码如下 //T ...

  10. C#LeetCode刷题之#561-数组拆分 I(Array Partition I)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/3718 访问. 给定长度为 2n 的数组, 你的任务是将这些数分成 ...