bean validate -- 转载
系列博客: https://www.yourbatman.cn/categories/A哥学数据校验/
https://www.cnblogs.com/summerday152/p/13984576.html
https://www.yourbatman.cn/x2y/55d56c0b.html
对数据做校验是一个程序员的基本素质,它不难但发生在我们程序的几乎每个角落,就像下面这幅图所示:每一层都需要做校验。
如果你真的这么去写代码的话(每一层都写一份),肯定是不太合适的,良好的状态应该如下图所示:
作为一个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");
...
}
从这个简单的方法入参校验至少能发现如下问题:
- 需要写大量的代码来进行参数基本验证(这种代码多了就算垃圾代码)
- 需要通过文字注释来知道每个入参的约束是什么(否则别人咋看得懂)
- 每个程序员做参数验证的方式可能不一样,参数验证抛出的异常也不一样,导致后期几乎没法维护
如上会导致代码冗余和一些管理的问题(代码量越大,管理起来维护起来就越困难),比如说语义的一致性问题。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定,这就是本文将要提供的一个新思路: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呢,这个时间是我在官网趴来的,因此肯定准确
这次企业平台的升级最大的亮点是:
- 把旗下30于种技术的大版本号全部+1(Jakarta RESTful Web Services除外)
- 包名全部去
javax.*
化,全部改为jakarta.*
- 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 | 元素大小需在指定范围中 | 否
所有注解均可标注在:方法、字段、注解、构造器、入参等几乎任何地方
可以看到这些注解均为平时开发中比较常用的注解,但是在使用过程中有如下事项你仍旧需要注意:
-
以上所有注解对null是免疫的,也就是说如果你的值是null,是不会触发对应的校验逻辑的(也就说null是合法的),当然喽@NotNull / @Null除外
-
对于时间类型的校验注解(@Future/@Past),是开区间(不包含相等)。也就是说:如果相等就是不合法的,必须是大于或者小于
- 这种case比较容易出现在LocalDate这种只有日期上面,必须是将来/过去日期,当天属于非法日期
-
@Digits
它并不规定数字的范围,只规定了数字的结构。如:整数位最多多少位,小数位最多多少位 -
@Size
规定了集合类型的范围(包括字符串),这个范围是闭区间 -
@DecimalMax和@Max
作用基本类似,大部分情况下可通用。不同点在于:@DecimalMax设置最大值是用字符串形式表示(只要合法都行,比如科学计数法),而@Max最大值设置是个long值
- 我个人一般用@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版本,它主要的改进/优化有如下几点:
- 标准化了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 Boot1.5.x默认集成的还是Bean Validation 1.1哦,但到了Boot 2.x后就彻底摒弃了老旧版本
JSR380
当下主流版本,也就是我们所说的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<@Valid CustomerType, @Valid Customer> customersByType
- 支持java.util.Optional类型,并且支持通过插入额外的值提取器来支持自定义容器类型
- 更灵活的集合类型级联验证;例如,现在可以验证映射的值和键,如:
-
让@Past/@Future注解支持注解在JSR310时间上
-
新增内建的注解类型(共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/configurationmapping 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值是否校验
——– | —– | —– | —–
@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标准作为切入点进行讲解,是希望理论和实践能结合起来学习,毕竟理论的指导作用不可或缺。有了理论铺垫的基石,后面实践将更加流畅,正所谓着地走路更加踏实嘛。
JDK1.8、SpringBoot2.3.4release
- 说明后端参数校验的必要性。
- 介绍如何使用validator进行参数校验。
- 介绍@Valid和@Validated的区别。
- 介绍如何自定义约束注解。
- 关于Bean Validation的前世今生,建议阅读文章: 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知,介绍十分详细。
后端参数校验的必要性
在开发中,从表现层到持久化层,数据校验都是一项逻辑差不多,但容易出错的任务,
前端框架往往会采取一些检查参数的手段,比如校验并提示信息,那么,既然前端已经存在校验手段,后端的校验是否还有必要,是否多余了呢?
并不是,正常情况下,参数确实会经过前端校验传向后端,但如果后端不做校验,一旦通过特殊手段越过前端的检测,系统就会出现安全漏洞。
不使用Validator的参数处理逻辑
既然是参数校验,很简单呀,用几个if/else
直接搞定:
@PostMapping("/form")
public String form(@RequestBody Person person) {
if (person.getName() == null) {
return "姓名不能为null";
}
if (person.getName().length() < 6 || person.getName().length() > 12) {
return "姓名长度必须在6 - 12之间";
}
if (person.getAge() == null) {
return "年龄不能为null";
}
if (person.getAge() < 20) {
return "年龄最小需要20";
}
// service ..
return "注册成功!";
}
写法干脆,但if/else
太多,过于臃肿,更何况这只是区区一个接口的两个参数而已,要是需要更多参数校验,甚至更多方法都需要这要的校验,这代码量可想而知。于是,这种做法显然是不可取的,我们可以利用下面这种更加优雅的参数处理方式。
Validator框架提供的便利
Validating data is a common task that occurs throughout all application layers, from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone.
如果依照下图的架构,对每个层级都进行类似的校验,未免过于冗杂。
Jakarta Bean Validation 2.0 - defines a metadata model and API for entity and method validation. The default metadata source are annotations, with the ability to override and extend the meta-data through the use of XML.
The API is not tied to a specific application tier nor programming model. It is specifically not tied to either web or persistence tier, and is available for both server-side application programming, as well as rich client Swing application developers.
Jakarta Bean Validation2.0
定义了一个元数据模型,为实体和方法提供了数据验证的API,默认将注解作为源,可以通过XML扩展源。
SpringBoot自动配置ValidationAutoConfiguration
Hibernate Validator
是Jakarta Bean Validation
的参考实现。
在SpringBoot中,只要类路径上存在JSR-303的实现,如Hibernate Validator
,就会自动开启Bean Validation验证功能,这里我们只要引入spring-boot-starter-validation
的依赖,就能完成所需。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
目的其实是为了引入如下依赖:
<!-- Unified EL 获取动态表达式-->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>3.0.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
<scope>compile</scope>
</dependency>
SpringBoot对BeanValidation的支持的自动装配定义在org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
类中,提供了默认的LocalValidatorFactoryBean
和支持方法级别的拦截器MethodValidationPostProcessor
。
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
//ValidatorFactory
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
// 支持Aop,MethodValidationInterceptor方法级别的拦截器
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
// factory.getValidator(); 通过factoryBean获取了Validator实例,并设置
processor.setValidator(validator);
return processor;
}
}
Validator+BindingResult优雅处理
默认已经引入相关依赖。
为实体类定义约束注解
/**
* 实体类字段加上javax.validation.constraints定义的注解
* @author Summerday
*/
@Data
@ToString
public class Person {
private Integer id;
@NotNull
@Size(min = 6,max = 12)
private String name;
@NotNull
@Min(20)
private Integer age;
}
使用@Valid或@Validated注解
@Valid和@Validated在Controller层做方法参数校验时功能相近,具体区别可以往后面看。
@RestController
public class ValidateController {
@PostMapping("/person")
public Map<String, Object> validatePerson(@Validated @RequestBody Person person, BindingResult result) {
Map<String, Object> map = new HashMap<>();
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
if (result.hasErrors()) {
List<String> res = new ArrayList<>();
result.getFieldErrors().forEach(error -> {
String field = error.getField();
Object value = error.getRejectedValue();
String msg = error.getDefaultMessage();
res.add(String.format("错误字段 -> %s 错误值 -> %s 原因 -> %s", field, value, msg));
});
map.put("msg", res);
return map;
}
map.put("msg", "success");
System.out.println(person);
return map;
}
}
发送Post请求,伪造不合法数据
这里使用IDEA提供的HTTP Client工具发送请求。
POST http://localhost:8081/person
Content-Type: application/json
{
"name": "天乔巴夏",
"age": 10
}
响应信息如下:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 14 Nov 2020 15:58:17 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"msg": [
"错误字段 -> name 错误值 -> 天乔巴夏 原因 -> 个数必须在6和12之间",
"错误字段 -> age 错误值 -> 10 原因 -> 最小不能小于20"
]
}
Response code: 200; Time: 393ms; Content length: 92 bytes
Validator + 全局异常处理
在接口方法中利用BindingResult处理校验数据过程中的信息是一个可行方案,但在接口众多的情况下,就显得有些冗余,我们可以利用全局异常处理,捕捉抛出的MethodArgumentNotValidException
异常,并进行相应的处理。
定义全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* If the bean validation is failed, it will trigger a MethodArgumentNotValidException.
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpStatus status) {
BindingResult result = ex.getBindingResult();
Map<String, Object> map = new HashMap<>();
List<String> list = new LinkedList<>();
result.getFieldErrors().forEach(error -> {
String field = error.getField();
Object value = error.getRejectedValue();
String msg = error.getDefaultMessage();
list.add(String.format("错误字段 -> %s 错误值 -> %s 原因 -> %s", field, value, msg));
});
map.put("msg", list);
return new ResponseEntity<>(map, status);
}
}
定义接口
@RestController
public class ValidateController {
@PostMapping("/person")
public Map<String, Object> validatePerson(@Valid @RequestBody Person person) {
Map<String, Object> map = new HashMap<>();
map.put("msg", "success");
System.out.println(person);
return map;
}
}
@Validated精确校验到参数字段
有时候,我们只想校验某个参数字段,并不想校验整个pojo对象,我们可以利用@Validated精确校验到某个字段。
定义接口
@RestController
@Validated
public class OnlyParamsController {
@GetMapping("/{id}/{name}")
public String test(@PathVariable("id") @Min(1) Long id,
@PathVariable("name") @Size(min = 5, max = 10) String name) {
return "success";
}
}
发送GET请求,伪造不合法信息
GET http://localhost:8081/0/hyh
Content-Type: application/json
未作任何处理,响应结果如下:
{
"timestamp": "2020-11-15T15:23:29.734+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "javax.validation.ConstraintViolationException: test.id: 最小不能小于1, test.name: 个数必须在5和10之间...省略",
"message": "test.id: 最小不能小于1, test.name: 个数必须在5和10之间",
"path": "/0/hyh"
}
可以看到,校验已经生效,但状态和响应错误信息不太正确,我们可以通过捕获ConstraintViolationException
修改状态。
捕获异常,处理结果
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(CustomGlobalExceptionHandler.class);
/**
* If the @Validated is failed, it will trigger a ConstraintViolationException
*/
@ExceptionHandler(ConstraintViolationException.class)
public void constraintViolationException(ConstraintViolationException ex, HttpServletResponse response) throws IOException {
ex.getConstraintViolations().forEach(x -> {
String message = x.getMessage();
Path propertyPath = x.getPropertyPath();
Object invalidValue = x.getInvalidValue();
log.error("错误字段 -> {} 错误值 -> {} 原因 -> {}", propertyPath, invalidValue, message);
});
response.sendError(HttpStatus.BAD_REQUEST.value());
}
}
@Validated和@Valid的不同
参考:@Validated和@Valid的区别?教你使用它完成Controller参数校验(含级联属性校验)以及原理分析【享学Spring】
@Valid
是标准JSR-303规范的标记型注解,用来标记验证属性和方法返回值,进行级联和递归校验。@Validated
:是Spring提供的注解,是标准JSR-303
的一个变种(补充),提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。- 在
Controller
中校验方法参数时,使用@Valid和@Validated并无特殊差异(若不需要分组校验的话)。 @Validated
注解可以用于类级别,用于支持Spring进行方法级别的参数校验。@Valid
可以用在属性级别约束,用来表示级联校验。@Validated
只能用在类、方法和参数上,而@Valid
可用于方法、字段、构造器和参数上。
如何自定义注解
Jakarta Bean Validation API
定义了一套标准约束注解,如@NotNull,@Size等,但是这些内置的约束注解难免会不能满足我们的需求,这时我们就可以自定义注解,创建自定义注解需要三步:
- 创建一个constraint annotation。
- 实现一个validator。
- 定义一个default error message。
创建一个constraint annotation
/**
* 自定义注解
* @author Summerday
*/
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class) //需要定义CheckCaseValidator
@Documented
@Repeatable(CheckCase.List.class)
public @interface CheckCase {
String message() default "{CheckCase.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CaseMode value();
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CheckCase[] value();
}
}
实现一个validator
/**
* 实现ConstraintValidator
*
* @author Summerday
*/
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
/**
* 初始化获取注解中的值
*/
@Override
public void initialize(CheckCase constraintAnnotation) {
this.caseMode = constraintAnnotation.value();
}
/**
* 校验
*/
@Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
if (object == null) {
return true;
}
boolean isValid;
if (caseMode == CaseMode.UPPER) {
isValid = object.equals(object.toUpperCase());
} else {
isValid = object.equals(object.toLowerCase());
}
if (!isValid) {
// 如果定义了message值,就用定义的,没有则去
// ValidationMessages.properties中找CheckCase.message的值
if(constraintContext.getDefaultConstraintMessageTemplate().isEmpty()){
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(
"{CheckCase.message}"
).addConstraintViolation();
}
}
return isValid;
}
}
定义一个default error message
在ValidationMessages.properties
文件中定义:
CheckCase.message=Case mode must be {value}.
这样,自定义的注解就完成了,如果感兴趣可以自行测试一下,在某个字段上加上注解:@CheckCase(value = CaseMode.UPPER)
。
源码下载
本文内容均为对优秀博客及官方文档总结而得,原文地址均已在文中参考阅读处标注。最后,文中的代码样例已经全部上传至Gitee:https://gitee.com/tqbx/springboot-samples-learn,另有其他SpringBoot的整合哦