文章目录
- 0. 背景
- 1. 定制代码检验
- 2. 通用标准校验
- 2.1. 发展历程
- 2.1.1. JSR303
- 2.1.2. JSR349
- 2.1.3. JSR380
- 2.1.4. 发展总述
- 2.1.5. 引入依赖
- 2.1.6. 常用注解
- 2.2. @Valid 详解
- 2.3. @Validated 详解
- 2.4. 嵌套验证
- 2.5. 自定义校验
- 2.6. 类级别验证(多字段联合验证)
- 2.7. Dubbo RPC参数校验
- 2.7.1. ValidationFilter & JValidator
- 2.7.2. @MethodValidated注解
- 2.7.3. 简单示例
- 2.8. springboot校验
- 2.8.1. 代码示例
- 2.8.2. 注意事项
- 2.8.3. 注解详解
0. 背景
服务端在向外提供接口服务时,不管是对前端提供HTTP接口,还是面向内部其他服务端提供的RPC接口,常常会面对这样一个问题,就是如何优雅的解决各种接口参数校验问题?
早期大家在做面向前端提供的HTTP接口时,对参数的校验可能都会经历这几个阶段:
- 每个接口每个参数都写定制校验代码
- 提炼公共校验逻辑
- 自定义切面进行校验
- 通用标准的校验逻辑。
其中最常见的就是定制检验代码和通用标准的校验逻辑,前者是利用大量的if/else语句,后者指的就是基于JSR303的Java Bean Validation,其中官方指定的具体实现就是 Hibernate Validator,在Web项目中结合Spring可以做到很优雅的去进行参数校验。
1. 定制代码检验
大量的 if / else 使代码非常臃肿
/**
* 员工对象
*
* @author sunnyzyq
* @since 2019/12/13
*/
@Data
public class Employee {
/**
姓名
*/
private String name;
/**
年龄
*/
private Integer age;
/**
邮箱地址
*/
private String email;
/**
手机号
*/
private String phone;
}
@Controller
public class TestController {
@RequestMapping("/add")
@ResponseBody
public String add(Employee employee) {
String name = employee.getName0;
if (name == null || name.trim().length == 0){
return"员工名称不能为空"
}
if (name.trim().length0 > 10){
return"员工名称不能超过10个字符"
}
return "新增员工成功";
}
}
以上代码肯定是可以正常的校验员工名称收为空以及长度是否符合的,但是随着检验条件的增多,我们会需要越来越多的代码,比如我们规定年龄也是必填项,且范围在1到100岁,那么此时,我们需要增加对应判定代码如下:
@Controller
public class TestController {
@RequestMapping("/add")
@ResponseBody
public String add(Employee employee) {
String name = employee.getName0;
if (name == null || name.trim().length == 0){
return"员工名称不能为空"
}
if (name.trim().length0 > 10){
return"员工名称不能超过10个字符"
}
// 新增校验条件
Integer age = employee.getAge();
if(age == null){
return "年龄不能为空";
}
if(age < 1 || age > 10){
return "年龄不能大于10岁或者小于1岁";
}
return "新增员工成功";
}
}
定制检验代码现在就会出现一种情况,每校验一个字段就需要增加6行的代码,此时只校验了两个字段,要是有20个字段,岂不是要写 100 多行代码?通常来说,当一个方法中的无效业务代码量过多时,往往代码设计有问题,当然这不是我们所想看到都结果。
2. 通用标准校验
其实我真的觉得现在作为一个程序员是幸运的,因为有很多的轮子已经造好了,同时,我觉得现在作为程序员是不幸运的,因为很多轮子已经造好了…
没错,java早就帮我们准备好了更方便的参数校验方式。-- Bean Validation
2.1. 发展历程
Bean Validation技术隶属于Java EE规范,期间有多个JSR(Java Specification Requests)支持,目前共有三次相关JSR标准发布:
- JSR303 最早(2009)
- JSR349
- JSR380
2.1.1. JSR303
JSR303提出很早(2009年),它为 基于注解的 JavaBean验证定义元数据模型和API。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 | String | 元素需符合指定的正则表达式 | 否 |
@Size | String/Collection/Map/Array | 元素大小需在指定范围中 | 否 |
它的官方参考实现如下:
2.1.2. JSR349
该规范2013年完成伴随java EE 7一起发布,就是我们比较熟悉的Bean Validation1.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版本并没新增~
它的官方参考实现如下:
注:当你导入了hibernate-validator后,无需再显示导入javax.validation,反之亦同
2.1.3. JSR380
当下主流版本,也就是Java Bean Validation 2.0,它完成于2017年8月,在2019年8月发布,属于Java EE 8的一部分。它的官方参考实现只有唯一的Hibernate validator了:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
此版本具有很重要的现实意义,主要有以下变化:
- 支持通过注解泛型类型来验证容器内的元素,如: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
- 所有内置的约束现在都支持重复标记
- JDK最低版本要求:JDK 8
新增注解
注解 | 支持类型 | 含义 | null值是否校验 |
@Email | String | 元素必须是电子邮箱地址 | 否 |
@NotEmpty | 容器类型 | 集合的Size必须大于0 | 是 |
@NotBlank | String | 字符串必须包含至少一个非空白的字符 | 是 |
@Positive | Positive | 元素必须必须为正数(不包括0) | 否 |
@PositiveOrZero | 同上 | 同上(包括0) | 否 |
@Negative | 同上 | 元素必须必须为负数(不包括0) | 否 |
@NegativeOrZero | 同上 | 同上(包括0) | 否 |
@PastOrPresent | 时间类型 | 在@Past基础上包括相等 | 否 |
@FutureOrPresent | 时间类型 | 在@Futrue基础上包括相等 | 否 |
2.1.4. 发展总述
以上就是java中参数校验轮子的发展历程。
Validation 从1.1版本起就需要El管理器支持用于错误消息动态插值,因此需要自己额外导入EL的实现。EL也属于Java EE标准技术,可认为是一种表达式语言工具,它并不仅仅是只能用于Web,可以用于任意地方(类比Spring的SpEL)
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
以上是EL技术规范的API,Expression Language 3.0表达式语言规范于2013-4-29发布,Tomcat 8、Jetty 9、GlasshFish 4都已经支持实现了EL 3.0,如果你是web环境,就不用自己手动导入了。
简单来说以上JSR提供了一套Bean校验规范的API,维护在包javax.validation.constraints下。该规范使用属性或者方法参数或者类上的一套简洁易用的注解来做参数校验。开发者在开发过程中,仅需在需要校验的地方加上形如@NotNull, @NotEmpty , @Email的注解,就可以将参数校验的重任委托给一些第三方校验框架来处理。
2.1.5. 引入依赖
目前在最常用的springboot 项目中, Spring Boot 2.3.0 之前的 spring-boot-starter-web 依赖中已经自带了,可以直接使用。但是如果是 2.3.0以后的Spring Boot项目则需要手动引入依赖包
<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.2</version>
</dependency>
上面两个jar随便引入哪个都可以,就算是都引入了也没有关系,因为他们的api完全一致。
Hibernate Validator 官网说明:Hibernate Validator
2.1.6. 常用注解
在Spring MVC中,只需要使用@Valid注解标注在方法参数商,Spring MVC即可对参数对象进行校验,校验结果会放在BindingResult对象中。除了@Valid 还有 @Validated注解。@validated是对@Valid 进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同:
不同点 | @Valid | @Validated |
来源 | 是Hibernate validation 的 校验注解 | 是 Spring Validator 的校验注解,是 Hibernate validation 基础上的增加版 |
注解位置 | 构造函数、方法、方法参数、成员属性 | 类、方法、方法参数 |
嵌套验证 | 用在级联对象的成员属性上 | 不支持 |
分组 | 不支持 | 提供分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制 |
校验结果 | 校验时需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不return ,则并不会阻止程序的执行 | 校验时无需接收校验结果,当校验不通过时,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示。(配合@RestControllerAdvice非常好用) |
总体来说,在你不需要嵌套验证的情况下,@Validated 使用起来要比 @Valid 方便一些,它可以帮我们节省一定的代码,并且使得方法看上去更加的简洁,同时还有更友好的分组功能。
2.2. @Valid 详解
成员属性上增加注解
package com.zyq.beans;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.Range;
/**
* 员工对象
*
* @author sunnyzyq
* @since 2019/12/13
*/
public class Employee {
/** 姓名 */
@NotBlank(message = "请输入名称")
@Length(message = "名称不能超过个 {max} 字符", max = 10)
public String name;
/** 年龄 */
@NotNull(message = "请输入年龄")
@Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)
public Integer age;
}
然后再 Controller 对应方法上,对这个员工标上 @Valid 注解,表示我们对这个对象属性需要进行验证,同时使用@Valid 注解时就必须手动处理校验结果。做法也很简单,在参数直接添加一个BindingResult,具体如下:
@Controller
public class TestController {
@RequestMapping("/add")
@ResponseBody
public String add(@Valid Employee employee, BindingResult bindingResult){
// 所有字段是否验证通过,true-数据有误,false-数据无误
if (bindingResult.hasErrors()) [
// 验证有误情况,返回第一条错误信息到前端
return bindingResult.getAllErrors().get(0).getDefaultMessage():
}
// TODO 保存到数据库
return"新增员工成功"
}
}
可以看到,相比于手动校验,效果相同,代码却简洁了很多。
2.3. @Validated 详解
在使用 @Valid 进行验证的时候,需要用一个对象去接收校验结果,最后根据校验结果判断,此时如果去掉手动接收参数
@Controller
public class TestController {
@RequestMapping("/add")
@ResponseBody
public String add(@Valid Employee employee, BindingResult bindingResult){
// 所有字段是否验证通过,true-数据有误,false-数据无误
/*
if (bindingResult.hasErrors()) [
// 验证有误情况,返回第一条错误信息到前端
return bindingResult.getAllErrors().get(0).getDefaultMessage():
}
*/
// TODO 保存到数据库
return"新增员工成功"
}
}
可以看到我们的程序继续往后面去执行完成了。
也就说@Valid并不会阻挡程序的执行,只是将校验结果进行了一个存储,使用者需要进入校验结果集合中进行手动处理。
相比之下,@Validated更加人性,会自动阻塞程序运行,且不需要手动获取校验结果
@Controller
public class TestController {
@RequestMapping("/add")
@ResponseBody
public String add(@Validated Employee employee){
// TODO 保存到数据库
return"新增员工成功"
}
}
在实际开发的过程中,我们肯定不能讲异常直接展示给用户,而是给能看懂的提示。于是,我们不妨可以通过捕获异常的方式,将该异常进行捕获。
首先我们创建一个校验异常捕获类 ValidExceptionHandler ,然后打上 @RestControllerAdvice 注解,该注解表示他会去抓所有 @Controller 标记类的异常,并在异常处理后返回以 JSON 或字符串的格式响应前端。
package com.zyq.config;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ValidExceptionHandler {
@ExceptionHandler(BindException.class)
public String validExceptionHandler(BindException exception) {
return exception.getAllErrors().get(0).getDefaultMessage();
}
}
那么,我们现在重启程序,然后重新请求,就可以发现界面已经不报400错误了,而是直接提示了我们的错误信息。
2.4. 嵌套验证
比如我们现在有个实体叫做Item,Item带有很多属性,属性里面有:pid、vid、pidName和vidName
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@NotNull(message = "props不能为空")
@Size(min = 1, message = "至少要有一个属性")
private List<Prop> props;
}
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
正常情况,Spring Validation框架只会对Item的id和props做非空和数量验证,不会对props字段里的Prop实体进行字段验证。
如何进行嵌套校验?
为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证。由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
修改Item类如下所示:
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
@Valid // 嵌套验证必须用@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
除了上面常见的@NotNull、@Min、@NotBlank和@Size等校验注解我们还可以自定义校验注解~
2.5. 自定义校验
举例说明自定义注解的实现:需要一个自定义注解来校验入参name不能和已存在name重名
- 自定义注解
@Target({ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueConstraintValidator.class)
public @interface UniqueConstraint {
//下面三个属性是必须有的属性
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- 新建一个UniqueConstraintValidator类来验证注解
//自定义校验注解 的 校验逻辑
//不需要加注解@Component,因为实现了ConstraintValidator接口自动会注册为spring bean
public class UniqueConstraintValidator implements ConstraintValidator<UniqueConstraint,Object> {
@Autowired
private UserService userService;
@Override
public void initialize(UniqueConstraint uniqueConstraint) {
System.out.println("my validator init");
}
//Object为校验的字段类型
//返回true则校验成功
//o为校验字段的值,constraintValidatorContext为校验注解里的属性值
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
String username = (String) o;
TbUser user = userService.findByUsername(username);
return user==null?true:false;
}
}
- UniqueConstraintValidator类必须实现ConstraintValidator接口initialize方法以及验证方法isValid
- 具体的校验逻辑在isValid方法中做校验
- 使用的时候在需要的字段上标记该注解即可:
2.6. 类级别验证(多字段联合验证)
约束也可以放在类级别上(也就说注解标注在类上)。在这种情况下,验证的主体不是单个属性,而是整个对象。如果验证依赖于对象的几个属性之间的相关性,那么类级别约束就能搞定。
这个需求场景在平时开发中也非常常见,比如此处我举个简单场景案例:修改用户名密码,需要输入两遍新密码:newPass,newPassAgain,要求newPass.equals(newPassAgain)。如果用事务脚本来实现这个验证规则,那么你的代码里肯定穿插着类似这样的代码:
if (!this.newPass.equals(this.newPassAgain)){
throw new RuntimeException("...");
}
虽然这么做也能达到校验的效果,但很明显这不够优雅。
但是基于Hibernate-Validator内置的@ScriptAssert,可以很容易的处理这种case:
@ScriptAssert(lang = "javascript", alias = "_", script = "_.newPass.equals(_.newPassAgain)",message = "两个密码不相等")
public class SecContent implements Serializable {
@NotNull(message = "age 不能为空",groups = {TestGroup.class})
private Integer age;
@NotBlank
private String newPass;
@NotBlank
private String newPassAgain;
...
}
@ScriptAssert支持写脚本来完成验证逻辑,这里使用的是javascript(缺省情况下的唯一选择,也是默认选择)
@ScriptAssert是内置就提供的,因此使用起来非常的方便和通用。但缺点也是因为过于通用,因此语义上不够明显,需要阅读脚本才知。推荐少量(非重复使用)、逻辑较为简单时使用,更为轻巧
2.7. Dubbo RPC参数校验
Dubbo作为国产优秀的开源RPC框架,同样支持注解方式校验参数!同时也是基于JSR303去实现的,我们来看下具体是怎么实现的。
2.7.1. ValidationFilter & JValidator
ValidationFilter通过在实际方法调用之前,根据调用者url配置的validation属性值找到正确的{Validator}实例来调用验证。
关于ValidationFilter是如何被调用的是dubbo spi的内容这里就不提了,但是要想其生效需要在consumer或者provider端配置一下:
consumer:
@DubboReference(validation = "true")
private DemoService demoService;
或provider:
@DubboService(validation = "true")
public class DemoServiceImpl implements DemoService {
注:如果在消费端开启参数校验,不通过就不会向服务端发起rpc调用,但是要自己处理校验异常ConstraintViolationException
javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='用户名不能为空', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户名不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户手机号不能为空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户手机号不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户标识不能为空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户标识不能为空'}]
javax.validation.ValidationException: Failed to validate service: com.xxx.demo.UserFacade, method: updateUser, cause: [ConstraintViolationImpl{interpolatedMessage='用户名不能为空', propertyPath=name, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户名不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户手机号不能为空', propertyPath=phone, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户手机号不能为空'}, ConstraintViolationImpl{interpolatedMessage='用户标识不能为空', propertyPath=id, rootBeanClass=class com.xxx.demo.UpdateUserParam, messageTemplate='用户标识不能为空'}]
at org.apache.dubbo.validation.filter.ValidationFilter.invoke(ValidationFilter.java:96)
at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:83)
....
at org.apache.dubbo.remoting.exchange.support.header.HeaderExchangeHandler.received(HeaderExchangeHandler.java:175)
at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:51)
at org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
从异常堆栈内容我们可以看出这个异常信息返回是由ValidationFilter抛出的,从名字我们可以猜到这个是采用Dubbo的Filter扩展机制的一个内置实现,当我们对Dubbo服务接口启用参数校验时(即前文Dubbo服务配置中的validation=“true”),该Filter就会真正起作用,我们来看下其中的关键实现逻辑:
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (validation != null && !invocation.getMethodName().startsWith("$")
&& ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), VALIDATION_KEY))) {
try {
Validator validator = validation.getValidator(invoker.getUrl());
if (validator != null) {
// 注1
validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
}
} catch (RpcException e) {
throw e;
} catch (ValidationException e) {
// 注2
return AsyncRpcResult.newDefaultAsyncResult(new ValidationException(e.getMessage()), invocation);
} catch (Throwable t) {
return AsyncRpcResult.newDefaultAsyncResult(t, invocation);
}
}
return invoker.invoke(invocation);
}
从前文的异常堆栈信息我们可以知道异常信息是由上述代码「注2」处所产生,这边是因为捕获了ValidationException,通过走读代码或者调试可以得知,该异常是由「注1」处valiator.validate方法所产生。
而Validator接口在Dubbo框架中实现只有JValidator,这个通过idea工具显示Validator所有实现的UML类图可以看出(如下图所示),当然调试代码也可以很轻松定位到。
既然定位到JValidator了,我们就继续看下它里面validate方法的具体实现,关键代码如下所示:
@Override
public void validate(String methodName, Class<?>[] parameterTypes, Object[] arguments) throws Exception {
List<Class<?>> groups = new ArrayList<>();
Class<?> methodClass = methodClass(methodName);
if (methodClass != null) {
groups.add(methodClass);
}
Set<ConstraintViolation<?>> violations = new HashSet<>();
Method method = clazz.getMethod(methodName, parameterTypes);
Class<?>[] methodClasses;
if (method.isAnnotationPresent(MethodValidated.class)){
methodClasses = method.getAnnotation(MethodValidated.class).value();
groups.addAll(Arrays.asList(methodClasses));
}
groups.add(0, Default.class);
groups.add(1, clazz);
Class<?>[] classgroups = groups.toArray(new Class[groups.size()]);
Object parameterBean = getMethodParameterBean(clazz, method, arguments);
if (parameterBean != null) {
// 注1
violations.addAll(validator.validate(parameterBean, classgroups ));
}
for (Object arg : arguments) {
// 注2
validate(violations, arg, classgroups);
}
if (!violations.isEmpty()) {
// 注3
logger.error("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations);
throw new ConstraintViolationException("Failed to validate service: " + clazz.getName() + ", method: " + methodName + ", cause: " + violations, violations);
}
}
从上述代码中可以看出当「注1」和注「2」两处代码进行参数校验时所得到的「违反约束」的信息都被加入到violations集合中,而在「注3」处检查到「违反约束」不为空时,就会抛出包含「违反约束」信息的ConstraintViolationException,该异常继承自ValidationException,这样也就会被ValidationFilter中方法所捕获,进而向调用方返回相关异常信息。
2.7.2. @MethodValidated注解
在JValidator的validate方法中可以看到有一个@MethodValidated注解
点开查看它的注释
大意上能明白这注解是标记在方法上支持分组校验的!
2.7.3. 简单示例
服务方代码:
dubbo client interface:
public interface DemoService {
String sayHello(String name);
@MethodValidated({TestGroup.class})
String sayGoodBye(Content content);
default CompletableFuture<String> sayHelloAsync(String name) {
return CompletableFuture.completedFuture(sayHello(name));
}
}
方法入参Content:
public class Content implements Serializable {
@NotNull(message = "name不能为空",groups = {TestGroup.class})
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
消费方代码:
@Component("demoServiceComponent")
public class DemoServiceComponent implements DemoService {
@DubboReference(validation = "true")
private DemoService demoService;
@Override
public String sayHello(String name) {
return demoService.sayHello(name);
}
@Override
public String sayGoodBye(Content content) {
return demoService.sayGoodBye(content);
}
@Override
public CompletableFuture<String> sayHelloAsync(String name) {
return null;
}
}
注:没有设置groups的校验注解也会进行校验,作为默认分组(像kafka一样分配一个默认组)。最后捕获下抛出的ConstraintViolationException以结构化的json格式返回给调用方"校验错误信息"
2.8. springboot校验
2.8.1. 代码示例
参数校验
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class WangDianTongQO implements Serializable {
private static final long serialVersionUID = 6427241139887596421L;
/**
* 签名
*/
@NotBlank(message = "签名不能为空", groups = {
WangDianTongQO.PosterMsg.class,
WangDianTongQO.ShopBindMsg.class,
WangDianTongQO.SyncShopMsg.class})
private String signature;
/**
* 网店通系统的门店编号
*/
@NotNull(message = "门店编号不能为空",
groups = {WangDianTongQO.ShopBindMsg.class})
private Long shopNum;
/**
* 海报码信息
*/
@NotBlank(message = "海报码不能为空",
groups = {WangDianTongQO.PosterMsg.class})
private String posterCode;
/**
* 门店更新信息
*/
@Valid
@NotNull(message = "data不能为空", groups = {WangDianTongQO.SyncShopMsg.class})
@Size(min = 1, message = "data至少有1条信息", groups = {WangDianTongQO.SyncShopMsg.class})
@Size(max = 500, message = "data至多有500条信息", groups = {WangDianTongQO.SyncShopMsg.class})
private List<WdtShopBindMsgSyncRO> data;
/**
* 店主绑定信息查询分组
*/
public interface ShopBindMsg extends Default {}
/**
* 海报关联信息查询分组
*/
public interface PosterMsg extends Default {}
/**
* 门店绑定信息同步
*/
public interface SyncShopMsg extends Default {}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WdtShopBindMsgSyncRO implements Serializable {
private static final long serialVersionUID = -264965558542189902L;
@NotBlank(message = "门店编号不能为空")
private String shopId;
@NotBlank(message = "门店名称不能为空")
private String shopName;
private String shopKeeperName;
private String shopKeeperPhone;
private String shopCountry;
private String shopProvince;
private String shopCity;
private String shopDistrict;
private String shopStreet;
}
异常捕获:
@RestControllerAdvice
public class ValidExceptionConfig {
@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
public ResponseBean validExceptionHandler(Exception e) {
String msg = null;
if (e instanceof MethodArgumentNotValidException){
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
}
if (e instanceof ConstraintViolationException){
ConstraintViolationException ex = (ConstraintViolationException) e;
msg = ex.getConstraintViolations().iterator().next().getMessage();
}
if (e instanceof BindException){
BindException ex = (BindException) e;
msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
}
if (msg == null){
return new ResponseBean<>("500", JmlConstant.Common.ERROR_MSG, null);
}
return new ResponseBean<>("401", msg, null);
}
}
参数使用:
@RefreshScope
@RestController
@RequestMapping(value = "/docking")
@Api(value = "docking", tags = "第三方对接接口")
public class DockingController extends ABaseController {
@PostMapping(value = "/queryBindMsg", produces = "application/json;charset=utf-8")
@ApiOperation("店主绑定信息查询|利店通")
@SentinelResource(value = "queryBindMsg")
public ResponseBean queryBindMsg(@RequestBody @Validated({WangDianTongQO.ShopBindMsg.class}) WangDianTongQO req) {
try {
logger.info("店主绑定信息查询:{}",req.toString());
if (req.getSignature() == null || req.getShopNum() == null){
return new ResponseBean<>("400", "参数有误",null);
}
if (!RSAUtil.verify(Sha1Util.getSha1(String.valueOf(req.getShopNum())).getBytes(), JmlConstant.RSAKey.LIDIANTONG_PUBLIC_KEY, req.getSignature())) {
return new ResponseBean<>("401", "验签错误", null);
}
return new ResponseBean<>(ResponseCodeEnum.SUCCESS_CODE, iJmlShopService.queryBindMsg(req.getShopNum()));
} catch (Exception e) {
logger.info("店主绑定信息查询失败",e);
return new ResponseBean<>(ResponseCodeEnum.OPERATION_FAILURE, "操作失败");
}
}
@PostMapping(value = "/queryPosterMsg",produces = "application/json;charset=utf-8")
@ApiOperation("海报信息查询接口")
@SentinelResource(value = "queryPosterMsg")
public ResponseBean queryPosterMsg(@RequestBody @Validated({WangDianTongQO.PosterMsg.class}) WangDianTongQO req){
logger.info("海报信息查询:{}",req.toString());
if (!RSAUtil.verify(Sha1Util.getSha1(req.getPosterCode()).getBytes(), JmlConstant.RSAKey.LIDIANTONG_PUBLIC_KEY, req.getSignature())) {
return new ResponseBean<>("401", "验签错误", null);
}
return new ResponseBean<>(ResponseCodeEnum.SUCCESS_CODE, iJmlPcPosteractivityService.WdtQuery(req.getPosterCode()));
}
@PostMapping(value = "/updateBindMsg",produces = "application/json;charset=utf-8")
@ApiOperation("门店信息更新接口")
@SentinelResource(value = "updateBindMsg")
public ResponseBean updateBindMsg(@RequestBody @Validated({WangDianTongQO.SyncShopMsg.class}) WangDianTongQO req){
if (!RSAUtil.verify(Sha1Util.getSha1(req.getData().toString()).getBytes(), JmlConstant.RSAKey.LIDIANTONG_PUBLIC_KEY, req.getSignature())) {
return new ResponseBean<>("401", "验签错误", null);
}
return new ResponseBean<>(ResponseCodeEnum.SUCCESS_CODE, iJmlShopService.SyncShopMsg(req));
}
}
2.8.2. 注意事项
- @valid注解 可以用在成员属性上,决定了它可以嵌套校验的功能
- @valid只能⽤在controller。@Validated可以⽤在其他被spring管理的类上
- @Validated不指定分组时 只会匹配未分组的注解;分组后则会匹配组内注解+未分组的注解。它的机制为未分组也是一个组,也就是说Validated 始终有组的概念,即使你没有显示指定
- 校验bean时:
- @Valid 和 @Validated 都是直接修饰方法参数就可以生效,抛出org.springframework.web.bind.MethodArgumentNotValidException异常,都会阻断方法执行,@RestControllerAdvice只是优化方法返回值而已。
- @Valid 和 @Validated 放到Controller类和参数类都不起作用, Controller类的方法上也不起作用。
- 校验字段时:
- @Validated 加在类上,配合参数校验注解可以生效,其余情况 @Validated在 方法上、 参数上均不生效。
- @Valid注解无法校验非bean类型参数。
- 参数校验未通过Spring会抛出三种类型的异常:
- 当对@RequestBody需要的参数进行校验时会出现org.springframework.web.bind.MethodArgumentNotValidException
public String test1(@Validated @RequestBody ValidEntity validEntity){} - 当直接校验具体参数时会出现javax.validation.ConstraintViolationException,也属于ValidationException异常
public String test2(@Email Stringemail){} - 当直接校验对象时会出现org.springframework.validation.BindException
public String test3(@Validated ValidEntity validEntity){}
- 最后,一定要注意注解的修饰类型,类型不符时会直接报错的,并且提示很不友好。第6条的报错只有是进入校验逻辑之后才会出现,但是如果用错了注解,根本不会进入校验逻辑,而且也没有异常输出,十分不友好!
2.8.3. 注解详解
Validation基本注解:
注解 | 含义 | 修饰java类型 | 注意 | 示例 |
AssertFalse AssertTrue | 被标记的元素值必须为false/true | boolean、Boolean | @AssertTrue(message = “xxx必须为true”) | |
DecimalMax DecimalMin | 被标记的元素必须小/大于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | @DecimalMax(value = “30000”) | |
Digits | 被标记的元素整数位数和小数位数必须小于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 1)识别不了字段值为null的场景2)使用在不支持的Java类型,程序会抛出javax.validation.UnexpectedTypeException异常 | @Digits(integer = 6, fraction = 2) |
Email | 被标记的元素必须是邮箱地址 | String | ||
Future | 被标记的元素必须为当前时间之后 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是时间类即可 | ||
FutureOrPresent | 被标记的元素必须为当前时间或之后 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是时间类即可 | ||
Past | 被标记的元素必须为当前时间之前 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是时间类即可 | ||
PastOrPresent | 被标记的元素必须为当前时间或之前 | Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate等只要是时间类即可 | ||
Max | 被标记的元素必须小于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 不支持double、float | @Max(value = 10000) |
Min | 被标记的元素必须大于或等于指定的值 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 不支持double、float | @Min(value = 10000) |
Negative | 被标记的元素必须是负数 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
NegativeOrZero | 被标记的元素必须是负数或0 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
Positive | 被标记的元素必须是正数 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
PositiveOrZero | 被标记的元素必须是正数或0 | BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、float、Float、double、Double | ||
Null | 被标记的元素必须为null | Object | ||
NotNull | 被标记的元素必须不为null | Object | ||
NotEmpty | 被标记的元素不为null,且不为空(字符串的话,就是length要大于0,集合的话,就是size要大于0) | String、Collection、Map、Array | 整型不支持!!! | |
NotBlank | 被标记的元素不为null,且必须有一个非空格字符 | 只支持String | 和@NotEmpty的区别,作用于字符串的话,@NotEmpty能校验出null、”“这2种场景,而@NotBlank能校验出null、”“、” “这3种场景,作用于集合的话,@NotEmpty支持,但@NotBlank不支持 | |
Size | 被标记的元素长度/大小必须在指定的范围内(字符串的话,就是length要在指定的范围内,集合的话,就是size要在指定的范围内) | String、Collection、Map、Array | @Size(min = 2, max = 5) | |
Pattern | 被标记的元素必须匹配指定的正则表达式 | 只支持String | @Pattern(regexp = “1\d{5}$”) |
Hibernate Validator除了支持上面提到的22个原生注解外,还扩展了一些注解
常用注解的总结:
注解 | 含义 | 修饰java类型 | 注意 | 示例 |
Length | 被标记的元素必须在指定的长度范围内 | 只支持String | 此注解多余了,可以直接用size | @Length(min = 2, max = 5) |
Range | @Range注解相当于同时融合了@Min注解和@Max注解的功能 | 它支持的Java类型也和@Min注解和@Max注解一致:BigDecimal、BigInteger、byte、Byte、short、Short、int、Integer、long、Long、String | 相当于整合了Max和Min | @Range(min = 1000L, max = 10000L) |
URL | 被标记的元素必须是一个有效的url地址 | 它的内部其实是使用了@Pattern注解,因此它支持的Java类型和@Pattern注解一致:String |
- 1-9 ↩︎