前言
对数据做校验是一个程序员的基本素质,它不难但发生在我们程序的几乎每个角落,就像下面这幅图所示:每一层都需要做校验。
如果你真的这么去写代码的话(每一层都写一份),肯定是不太合适的,良好的状态应该如下图所示:
作为一个 Java 开发者,在 Spring 大行其道的今天,很多小伙伴了解数据校验来自于 Spring MVC 场景,甚至止步于此。殊不知,Java EE 早已把它抽象成了 JSR 标准技术,并且 Spring 还是借助整合它完成了自我救赎呢。
在我看来,按 Spring 的 3C 战略标准来比,Bean Validation 数据校验这块是没有能够完成对传统 Java EE 的超越,自身设计存在过重、过度设计等特点。
本专栏命名为 Bean Validation(数据校验),将先从 JSR 标准开始,再逐渐深入到具体实现 Hibernate Validation、整合 Spring 使用场景等等,因此本专栏将让你将得到一份系统数据校验的知识。
参考官网:
- Jakarta Bean Validation
- JSR Bean Validation 标准
- Hibernate Validator
- Hibernate Validator Reference Guide
正文
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的,比如生日必须是过去时,年龄必须 >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");
...
}
从这个简单的方法入参校验至少能发现如下问题:
- 需要写大量的代码来进行参数基本验证(这种代码多了就算垃圾代码);
- 需要通过文字注释来知道每个入参的约束是什么(否则别人咋看得懂);
- 每个程序员做参数验证的方式可能不一样,参数验证抛出的异常也不一样,导致后期几乎没法维护。
如上会导致代码冗余和一些管理的问题(代码量越大,管理起来维护起来就越困难),比如说语义的一致性问题。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定,这就是本文将要提供的一个新思路:Bean Validation。
关于 Jakarta EE
2018 年 03 月, Oracle 决定把 Java EE 移交给开源组织 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 作为其基准版本(最低版本要求是 Java 8)。
有个意思的现象是: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 只是冠了个名,那么这个就名正言顺了。
这次企业平台的升级最大的亮点是:
- 把旗下 30 于种技术的大版本号全部 +1(Jakarta RESTful Web Services 除外);
- 包名全部去 javax. 化,全部改为 jakarta.;
- Java SE 基准版本要求依旧保持为 Java 8(而并非 Java 9 哦)。
可以发现本次升级的主要目的并着眼于功能点,仍旧是名字的替换。虽然大家对 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 空间所以非常轻量,有些选手还是忠爱的(此项目还在发展中,并未停更哦,有兴趣你可以自己使用试试)。
JSR 303: Bean Validation 1.0
这个 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 | - boolean - Boolean |
被注释的元素必须为 False。 | 否 |
@AssertTrue | - boolean - Boolean |
被注释的元素必须为 True。 | 否 |
@DecimalMax | - BigDecimal - BigInteger - CharSequence - byte、short、int、long 和它们各自的包装类 - 不支持 float 和 double |
被注释的元素必须是一个数字,其值必须小于或等于指定的最大值。 | 否 |
@DecimalMin | - BigDecimal - BigInteger - CharSequence - byte、short、int、long 和它们各自的包装类 - 不支持 float 和 double |
被注释的元素必须是一个数字,其值必须大于或等于指定的最小值。 | 否 |
@Max | - BigDecimal - BigInteger - CharSequence - byte、short、int、long 和它们各自的包装类 - 不支持 float 和 double |
被注释的元素必须是一个数字,其值必须小于或等于指定的最大值。 | 否 |
@Min | - BigDecimal - BigInteger - CharSequence - byte、short、int、long 和它们各自的包装类 - 不支持 float 和 double |
被注释的元素必须是一个数字,其值必须大于或等于指定的最小值。 | 否 |
@Digits | - BigDecimal - BigInteger - CharSequence - byte、short、int、long 和它们各自的包装类 |
被注释的元素必须是一个在接受范围内的数字。 - 该数字接受的最大整数位数; - 该数字接受的最大小数位数。 |
否 |
@Future | 时间类型,支持的类型比较多,具体参考 API Doc | 被注释的元素必须是将来的一个瞬间、日期或时间(不包含相等,比较精确到毫秒)。 | 否 |
@Past | 时间类型,支持的类型比较多,具体参考 API Doc | 被注释的元素必须是过去的一个瞬间、日期或时间(不包含相等,比较精确到毫秒)。 | 否 |
@NotNull | 任何类型 | 被注释的元素不能为 NULL | 是 |
@Null | 任何类型 | 被注释的元素必须为 NULL | 是 |
@Pattern | CharSequence | 被注释的字符串必须与指定的正则表达式匹配。 | 否 |
@Size | - CharSequence(字符串长度) - Collection(集合大小) - Map(Map 的大小) - Array(数组的长度) |
被注释的元素大小必须在指定的边界(包括在内)之间。 | 否 |
所有注解均可标注在:方法、字段、注解、构造器、入参等几乎任何地方。
可以看到这些注解均为平时开发中比较常用的注解,但是在使用过程中有如下事项你仍旧需要注意:
- 以上所有注解对 NULL 是免疫的,也就是说如果你的值是 NULL,是不会触发对应的校验逻辑的(也就说 NULL 是合法的),当然 @NotNull 和 @Null 除外;
- 对于时间类型的校验注解(@Future 和 @Past),是开区间(不包含相等)。也就是说:如果相等就是不合法的,必须是大于或者小于;
- 这种 case 比较容易出现在 LocalDate 这种只有日期上面,必须是将来 / 过去日期,当天属于非法日期;
- @Digits 它并不规定数字的范围,只规定了数字的结构。如:整数位最多多少位,小数位最多多少位;
- @Size 规定了集合类型的范围(包括字符串),这个范围是闭区间;
- @DecimalMax 和 @Max 作用基本类似,大部分情况下可通用。不同点在于:@DecimalMax 设置最大值是用字符串形式表示(只要合法都行,比如科学计数法),而 @Max 最大值设置是个 Long 值。
另外可能有人会问:为毛没看见 @NotEmpty、@Email、@Positive 等常用注解?那么带着兴趣和疑问,继续往下看吧。
JSR 349: Bean Validation 1.1
该规范是 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 版本,它主要的改进有如下几点:
- 标准化了 Java 平台的约束定义、描述、和验证;
- 支持方法级验证(入参和返回值的验证);
- Bean 验证组件的依赖注入;
- 与上下文和 DI 依赖注入集成;
- 使用 EL 表达式的错误消息插值,让错误消息动态化起来(强依赖于 ElManager);
- 跨参数验证,比如密码和验证密码必须相同。
注解个数上,相较于 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 Boot 1.5.x 默认集成的还是 Bean Validation 1.1 哦,但到了 Boot 2.x 后就彻底摒弃了老旧版本。
JSR 380: Bean Validation 2.0
当下主流版本,也就是我们所说的 Java Bean Validation 2.0 和 Jakarta 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 了:
此版本具有很重要的现实意义,它主要提供如下亮点:
- 支持通过注解参数化类型(泛型类型)参数来验证容器内的元素,如:List<@Positive Integer> positiveNumbers;
- 更灵活的集合类型级联验证,例如,现在可以验证 Map 的值和键,如:Map<@Valid CustomerType, @Valid Customer> customersByType
- 支持 java.util.Optional 类型,并且支持通过插入额外的值提取器来支持自定义容器类型;
- @Past 和 @Future 注解支持注解在 JSR 310 时间上;
- 新增内建的注解类型(共9个):@Email、@NotEmpty、@NotBlank、@Positive、@PositiveOrZero、@Negative、@NegativeOrZero、@PastOrPresent 和 @FutureOrPresent;
- 所有内置的约束现在都支持重复标记;
- 使用反射检索参数名称,也就是入参名,详见这个 API:ParameterNameProvider;
- 很明显这是需要 Java 8 的启动参数支持的;
- Bean 验证 XML 描述符的名称空间已更改为:
- META-INF/validation.xml -> http://xmlns.jcp.org/xml/ns/validation/configuration
- mapping files -> http://xmlns.jcp.org/xml/ns/validation/mapping
- JDK 最低版本要求:JDK 8。
Hibernate Validator 自 6.x 版本开始对 JSR 380 规范提供完整支持,除了支持标准外,自己也做了相应的优化,比如性能改进、减少内存占用等等,因此用最新的版本肯定是没错的,毕竟只会越来越好嘛。
相较于 1.x 版本,2.0 版本在其基础上新增了 9 个实用注解,总数到了 22 个。现对新增的 9 个注解解释如下:
注解 | 支持类型 | 含义 | NULL 值是否校验 |
---|---|---|---|
CharSequence | 被注释的元素必须是格式正确的电子邮件地址。 | 否 | |
@NotEmpty | - CharSequence(字符串长度) - Collection(集合大小) - Map(Map 的大小) - Array(数组的长度) |
被注释的元素不能为 NULL,也不能为空。 | 是 |
@NotBlank | CharSequence | 被注释的元素不能为 NULL,并且必须至少包含一个非空白字符。 | 是 |
@Positive | - BigDecimal - BigInteger - byte, short, int, long, float, double 和它们各自的包装类 |
被注释的元素必须为正数(不包括 0)。 | 否 |
@PositiveOrZero | - BigDecimal - BigInteger - byte, short, int, long, float, double 和它们各自的包装类 |
被注释的元素必须为正数或 0。 | 否 |
@Negative | - BigDecimal - BigInteger - byte, short, int, long, float, double 和它们各自的包装类 |
被注释的元素必须为负数(不包括 0)。 | 否 |
@NegativeOrZero | - BigDecimal - BigInteger - byte, short, int, long, float, double 和它们各自的包装类 |
被注释的元素必须为负数或 0。 | 否 |
@PastOrPresent | 时间类型,支持的类型比较多,具体参考 API Doc | 在 @Past 基础上包括相等 | 否 |
@FutureOrPresent | 时间类型,支持的类型比较多,具体参考 API Doc | 在 @Futrue 基础上包括相等 | 否 |
像 @Email、@NotEmpty、@NotBlank 之前是 Hibernate 额外提供的,2.0 标准后 Hibernate 自动退位让贤并且标注为过期了。Bean Validation 2.0 的 JSR 规范制定负责人就职于 Hibernate,所以这么做就很自然了。就是他:
除了 JSR 标准提供的这 22 个注解外,Hibernate Validator 还提供了一些非常实用的注解,这在后面讲述 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.* 化了。因为实际的类并没有改变,因此仍旧可以认为它是 JSR 380 的实现(虽然不再由 JCP 组织制定标准了)。
参考实现
毫无疑问,参考实现那必然是 Hibernate Validator。它的步伐也跟得非常的紧,推出了 7.x 版本用于支持 Jakarta Bean Validation 3.0。虽然是大版本号的升级,但是在新特性方面你可认为是无:
总结
本文着眼于讲解 JSR 规范、Bean Validation 校验标准、官方参考实现 Hibernate Validator,把它们之间的关系进行了关联,并且对差异进行了鉴别。我认为这篇文章对一般读者来说是能够刷新对数据校验的认知的。
数据校验是日常工组中接触非常非常频繁的一块知识点,我认为掌握它并且熟练运用于实际工作中,能起到事半功倍的效果,让代码更加的优雅,甚至还能实现别人加班你加薪呢。所以又是一个投出产出比颇高的小而美专栏在路上……
作为本专栏的第一篇文章以 JSR 标准作为切入点进行讲解,是希望理论和实践能结合起来学习,毕竟理论的指导作用不可或缺。有了理论铺垫的基石,后面实践将更加流畅,正所谓着地走路更加踏实嘛。
转载
打个广告,方便的话,可以关注一下 A哥(YourBatman) 的公众号。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/buorlh 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。