简介
数据校验(Bean Validation)
- Bean Validation是将校验逻辑与相应的Bean域模型进行绑,是基于Bean的校验。
- 契约式编程:避免了if else 等硬编码校验(能通过契约约定解决的就不要去硬编码)。
- 面向接口编程:Bean Validation属于Java EE标准规范,是JSR(Java Specification Requests)抽象的具体实现,实际使用过程中仅需要面向标准使用即可。
JSR标准
JSR(Java Specification Requests),为需要校验的JavaBean,定义了源数据模型 和 API。
版本变迁:
JSR303,2009年,Java Bean Validation 1.0
- 规定了Java数据校验的模型和API
- 只支持对Java Bean进行校验
JSR349,2013年,Bean Validation 1.1,伴随着Java EE 7一起发布
- 支持方法级验证(入参或返回值的验证)
- Bean验证组件的依赖注入
JSR380,2019,Java Bean Validation 2.0和Jakarta Bean Validation 2.0(两者只有名字的区别),当前主流。
- JDK最低版本要求:JDK 8
- 一些新增的标准
依赖包
由Java Bean Validation变为Jakarta Bean Validation。(雅加达)
2018年03月, Oracle 把 JavaEE (Java Enterprise Edition)移交给开源组织 Eclipse 基金会,并把Java EE(包名javax)改为Jakarta EE。Eclipse接手后发布的首个Enterprise Java将是 Jakarta EE 9,该版本将以Java EE 8作为其基准版本(最低版本要求是Java8)
由于上述版本迁移问题,maven包由javax
和jakarta
下的两种坐标,但目前2.0.1版本下,内容完全一样:
- javax.validation到2.0.1.Final版本就彻底结束了,不再升级。
- jakarta.validation从2.0.1版本开始。
1 | <!-- https://mvnrepository.com/artifact/javax.validation/validation-api --> |
目前最新版本是Jakarta Bean Validation 3.0,它唯一的变化,只是包名。
1 | <!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api --> |
不只是校验包,其他包名也有更改,如”javax.servlet.”改成了”jakarta.servlet.”
java.*
:java SE的标准库,是对外承诺的java开发接口。会保持向后兼容,不会轻易修改。
javax.*
: java的扩展包,如servelet/xml/validation等。属于某特定领域,不是普遍的api。被官方支持,兼容所有java支持的平台。
com.sun.*
:不被官方支持,不推荐使用。是sun的hotspot虚拟机中java.*
和javax.*
的实现类。因包含在rt中,故也可调用。但因为不是sun对外公开承诺的接口,所以根据实现的需要随时增减,不保证api跨平台(linux和windows的api可能不一样)、兼容版本(在不同版本的hotspot中可能不同)。
org.omg.*
:大部分不是sun公司提供的,不具备向后兼容性,会根据需要随时增减。其中比较常用的是w3c提供的对XML、网页、服务器的类和接口。HotSpot 虚拟机: 是Sun JDK和OpenJDK中所带的虚拟机,是目前使用范围最广的Java虚拟机。
起步案例
1 | <dependency> |
1 | public class Person { |
常见约束注解
多个可以组合发挥作用,如 @Min(value = 10,message = “年龄最小为10”)@Max(value = 100,message = “年龄最大为100”)。
除了内置校验,也可以自定义校验注解:
首先必须定义一个注解
然后才实现自定义校验类(具体逻辑)
BeanValidation内置
注解 | 说明 |
---|---|
@Valid | 被注释的元素是一个对象,需要检查此对象的所有字段值。适用于嵌套。@Valid注解用于验证级联的属性、方法参数或方法返回类型。比如你的属性仍旧是个Java Bean,你想深入进入校验它里面的约束,那就在此属性头上标注此注解即可。另外,通过使用@Valid可以实现递归验证,因此可以标注在List上,对它里面的每个对象都执行校验 |
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内@Size(min = 1,max = 10,message = “姓名长度必须为1到10”) |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期。必须是相对当前时间来讲“未来的”某个时间 @Future@JSONField(format=”yyyy-MM-dd HH:mm:ss”)private Date birth; |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
@GroupSequence | 控制校验顺序。 |
HibernateValidator附加
也内置在springboot中了
注解 | 作用 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length(min=, max=) | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range(min=, max=) | 被注释的元素必须在合适的范围内 |
@GroupSequenceProvider | 比@GroupSequence更强大的控制group调用注解(字段之间校验依赖) |
@NotBlank | 被注释的字符串的必须非空 |
@URL(protocol=,host=, port=, regexp=, flags=) | 被注释的字符串必须是一个有效的url |
@CreditCardNumber | 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 |
@ScriptAssert(lang=, script=, alias=) | 要有Java Scripting API 即JSR 223 (“Scripting for the JavaTM Platform”)的实现 |
@SafeHtml(whitelistType=, additionalTags=) | classpath中要有jsoup包 |
非空判断注解的区别
- @NotNull 用在基本类型上,不能为null,但可以为空字符串
- @NotBlank 用在String上面,只能作用在String上,不能为null,而且调用trim()后,长度必须大于0(不能是空字符串)
- @NotEmpty用在集合类上面,不能为null,并且长度必须大于0、
kotlin的写法略有不同
1 | For a field @field:NotBlank |
groups
1 | // 定义 |
@GroupSequence
- 在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。
- 控制group校验顺序,实现参数的顺序校验(默认情况下,不同group的约束校验无序)
- Bean Validation 中内置的标准注解
顺序只能控制在分组级别,无法控制在约束注解级别。因为一个类内的约束(同一分组内),它的顺序是Set<MetaConstraint<?>> metaConstraints来保证的,所以可以认为同一分组内的校验器是没有有执行先后顺序的(不管是类、属性、方法、构造器…)
约束验证的顺序和短路能力,在某些场景十分重要:
- 第一个约束正确,第二个约束才有意义。
- 对于性能消耗非常大的约束,应该把它放到最后
实现GroupIpInfoBean中,先校验IAreaCkeckSequence,再校验GroupIpInfoBean自己。而IAreaCkeckSequence中,又先校验省IProvinceCheck,地市ICityCheck,再区县ICountyCheck。
1 | public interface IProvinceCheck {} |
@GroupSequenceProvider
多字段组合逻辑校验,若想借助@GroupSequence完成,相对来说还是比较困难的。
- 比 @GroupSequence更强大,能够控制group校验,从而实现多种校验场景。
- 比如多字段联合逻辑校验场景:当且仅当属性a值满足条件时,属性b的校验逻辑才生效
- Hibernate Validator 附加的 constraint,非标准
1 | // 定义 |
Bean Validation四种约束级别
- 字段约束(Field)
- 属性约束(Property)
- 容器元素约束(Container Element)
- 类约束(Class)
Bean Validation标准的约束全部支持1/2/3级别,全部不支持第4级。
作为补充的Hibernate-Validator提供了一些专门用于类级别的约束注解。
字段约束Field
最常见。
不支持对静态字段static的约束。
直接反射访问字段的值 -> Field#get(不会执行get方法体)。不会调用任何方法来进行校验,比如对应的get/set方法。
1 | public class User { |
自定义字段注解
1 |
|
枚举自定义
1 | // 使用 |
属性约束Property
若一个Bean遵循Java Bean规范(getter/setter),则可使用属性约束来代替字段约束。
会调用属性get方法 -> getXXX(会执行get方法体)来获取待校验的值。
- 约束放在get方法上优于放在set方法上,这样只读属性(没有set方法)依然可以执行约束逻辑
- 不要在属性和字段上都标注注解,否则会重复执行约束逻辑(有多少个注解就执行多少次)
- 不要既在属性的get方法上又在set方法上标注约束注解。
如果希望执行了验证就输出一句日志,又或者POJO被字节码增强了,建议使用属性约束。
字节码增强:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
1 | public class User { |
容器元素级别约束Container Element
容器,List、Set、Map等。
验证容器内(每个)元素,形如List<User>
希望验证List容器内部元素User。
必须基于Java Bean,验证才会生效,所以需要将容器(List等),封装成Bean来使用。
1 | // 定义 |
自定义容器元素
某些容器不被支持校验,如Result
1 | // 首先注册一个值提取器,ValueExtractor |
SpringBoot实现
需要自行提供一个验证器来覆盖掉自动装配进去的,可参考ValidationAutoConfiguration
。
1 |
类约束Class
所有的约束注解都会执行,不存在短路效果
@ScriptAssert
@ScriptAssert对null值并不免疫,都会执行,因此书写脚本时注意判空
语义上不够明显,需要阅读脚本才知,推荐使用在验证逻辑只用一次(只一个地方使用)且简单(比如只是简单判断而已)的情况。
1 | "javascript", alias = "_", script = "_.maxStuNum >= _.studentNames.length") (lang = |
自定义类注解
1 | // 类注解 |
SpringBoot校验应用场景
上述已经定义好的四种约束,如何使用呢?
备注:java代码的侵入性强。可以通过两种AOP方式减少侵入:
- 基于Java EE的@Inteceptors实现
- 基于Spring Framework实现,此处spring boot为例。
Springboot默认集成了BeanValidation和HibernateValidator校验。
1 | <dependency> |
https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/spring-bean-validation.md
@Validated
在Controller 中 请求参数上添加@Validated 标签开启验证
与@Valid异同: @Validated是Spring对@Valid(javax.validation.Valid)的一层封装.
场景 | @Validated | @Valid | |
---|---|---|---|
controller验证Request Body | @Validated | @Valid | 抛出MethodArgumentNotValidException,Spring默认转为400(Bad Request)请求 |
controller验证PathVariable/RequestParam | 类上标注@Validated,并方法参数声明需要的约束注解 | 抛出ConstraintViolationException | |
Service验证,需要组合两个注解使用 | 类上标注@Validated | 并方法参数@Valid | |
Bean字段属性上,嵌套校验 | @Valid | ||
构造器 | @Valid | ||
类 | @Validated | ||
分组 | @Validated | ||
1 | // @validated和@valid只能在controller层的参数前面 |
1 |
|
路径校验(PathVariable)
可以对url地址路径进行正则表达式的校验
- 语法:
路径变量:正则表达式
- 当URI不满足正则表达式时,客户端将收到404错误码。
- 而不是通过捕获异常的方式,向前端返回统一的、自定义格式的响应参数。
1 | "detail/{domain:[a-zA-Z]+}/{userId}") ( |
方法参数校验
- 基本类型参数校验(RequestParam):需要所在类上增加注解@Validated
1 |
|
- Bean参数校验
1 | //Java Bean作为入参校验 |
- Bean返回值校验
1 | //校验方法返回值 |
注解应该写在接口的方法内上还是实现的方法内?
- 如果该方法是接口方法的实现,那么可存在如下两种case:
- 保持和接口方法一毛一样的约束条件(极限情况:接口没约束注解,那你也不能有)
- 实现类一个都不写约束条件,结果就是接口里有约束就有,没约束就没有
@Validated注解应该放在接口(方法)上,还是实现类(方法)上?
1 | @NotNull加到接口,@Validated加到[实现]或者接口都可以 |
对象校验(bean)
详见Bean Validation四种约束级别。
1 | // 字段约束示例 |
分组校验(Group)
1 | public class User { |
全局异常拦截器
全局异常处理器,将Controller层异常统一封装,并向前端返回校验失败信息。
- 优点:
- 将 Controller 层的异常和数据校验的异常进行统一处理,减少模板代码,减少编码量,提升扩展性和可维护性。
- 去掉了try catch高耦合代码
- 统一封装,返回给前端一个友好的界面
- 避免了层层向上抛出异常
- 缺点:只能处理 Controller 层未捕获(往外抛)的异常,对于 Interceptor(拦截器)层的异常,Spring 框架层的异常,就无能为力了
实现方式:
- Spring的AOP(复杂)
@ControllerAdvice
结合@ExceptionHandler
(简单)
@ControllerAdvice
@ControllerAdvice注解源码中被@Component标记,所以可以被组件扫描到Spring容器。
1 | // controller通知器 |
@ControllerAdvice使用形式
1 | // 全局使用 |
@RestControllerAdvice
1 | @RestControllerAdvice = @ControllerAdvice + @ResponseBody |
顺序:如果有多个@ControllerAdvice
注解类,当第一个加载的注解类里有对需要捕获异常的相同类/父类有方法处理,就会使用第一个处理方法。可@Order指定顺序。
@ExceptionHandler
所有异常
1 |
ConstraintViolationException
- 指定参数为方法的基本参数异常
- 参数前有@RequestParam
- 如http://localhost/user?age=3&name=nice中的age和name校验异常
1 | .class) (value = ConstraintViolationException |
MethodArgumentNotValidException
- 指定参数为实体类时异常
- 实体类前必须有@RequestBody,才能捕获
- Content-Type为application/json
BindException
- 指定参数为实体类时异常
- 实体类前没有有@RequestBody,但的确是个实体类
- Content-Type为application/x-www-form-urlencoded,即表单请求
这两者一般会合起来处理。
1 | public String getAllUser(@Validated User user) {} |
自定义异常
1 | // 捕获 |
返回值
封装返给前端的内容。
包括如下内容:
- BaseResponseEnum.java 编码常量
- ResponseUtil.java 包装类
- 其他关于返回值的特定封装类:
- 自定义ParameterException类,封装参数异常返回
- 自定义UserAppException类,封装用户程序应用异常类
BaseResponseEnum.java 编码常量
1 | public interface IResponse { |
ResponseUtil.java 包装类
1 | public final class ResponseUtil { |
UserAppException自定义异常类(用户程序应用异常类)
1 | public class UserAppException extends RuntimeException implements IResponse { |
ParameterException类
1 | public class ParameterException extends UserAppException { |
拦截案例
1 |
|
工具方法
因为Validator等校验器是线程安全的,因此一般来说一个应用全局仅需一份、只初始化一次。可以封装这些方法作为工具方法。
1 | public abstract class ValidatorUtil { |
一些核心思想
Validator
校验器,可实现对Java Bean、某个属性、方法、构造器等完成校验。
方法 | 描述 |
---|---|
validate | 校验Java Bean上的所有约束,包括属性约束 + 类的约束。.validate(user) |
validateProperty | 校验指定的属性。.validateProperty(user, “fullName”) |
validateValue | 校验指定value值,是否符合指定属性上的所有约束。不需要存在对象实例,直接校验某个值是否满足某个属性的所有约束,可以做事前的校验判断。 .validateValue(User.class, “fullName”, “want”) |
getConstraintsForClass | 获取Class类型描述。.getConstraintsForClass(User.class) |
forExecutables | 获得Executable校验器,只能校验Java Bean |
使用案例:
1 | public class Network { |
ConstraintViolation
约束校验失败的详情信息。违反一个约束,就生成一个实例。
1 | Set<ConstraintViolation<Network>> networkViolation = validator.validate(network); |
ValidatorContext
可以定制设置校验器的核心组件(提供了Validator校验器的五大核心组件可以定制)。
1 | public interface ValidatorContext { |
ValueExtractor
值提取器,2.0版本新增,用于把值从容器内提取出来参与校验(容器包括数组、集合、Map、Optional等)
所以从Bean Validation2.0开始就支持验证容器内的元素,形如:
List<@NotNull @Valid Person>、Optional<@NotNull @Valid Person>
可以自定义ValueExtractor来实现自定义容器值提取器。
- 定义
- 添加
ValidatorContext#addValueExtractor()
1 | // 自定义容器Result |
自定义的Validator
利用ValidatorContext,要想定制设置核心组件,实现自定义的Validator,两种方式:
1 | // new实例的方式,不够抽象,强耦合了Hibernate Validator的API(ValidatorContextImpl) |
获得Validator实例的两种办法
- 工厂直接获取,默认
1 | Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); |
- 从上下文获取,可以任意指定核心组件实现,实现定制。
1 | Validator validator = Validation.buildDefaultValidatorFactory().usingContext() |
ValidationMessages.properties
可以把校验消息都配置到.properties文件中。默认目录是
resources/ValidationMessages.properties
(spring boot自动读取classpath中的ValidationMessages.properties)
当验证不通过时会抛出
ValidationMessages.properties
中配置的提示信息。若想自定义文件名,如
messages.properties
,可以做路径配置即可。
1 | <resource> |
内容:
key=value
(value即消息,默认是ASCII编码),需要修改为UTF-8格式编码
使用示例
1 | // Bean中使用注解 |
参考
不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知
https://blog.csdn.net/f641385712/article/details/108184782
Bean Validation声明式校验方法的参数、返回值
https://blog.csdn.net/f641385712/article/details/108256131
站在使用层面,Bean Validation这些标准接口你需要烂熟于胸
https://blog.csdn.net/f641385712/article/details/108342248
Validator校验器的五大核心组件,一个都不能少
https://blog.csdn.net/f641385712/article/details/108358583
Bean Validation声明式验证四大级别:字段、属性、容器元素、类
https://blog.csdn.net/f641385712/article/details/109234678
自定义容器类型元素验证,类级别验证(多字段联合验证)
https://blog.csdn.net/f641385712/article/details/109270066
SpringBoot里参数校验/参数验证
https://blog.csdn.net/jinjiankang/article/details/89711493
【springboot全局异常处理】— 请求参数异常+自定义异常+其他异常
https://blog.csdn.net/nrsc272420199/article/details/102645938/