文章目录

  • 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")@ResponseBodypublic 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")@ResponseBodypublic 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,即容器内元素须为正数

    1. 更灵活的集合类型级联验证;例如,现在可以验证映射的键和值,如:Map<@Valid CustomerType, @Valid Customer> customersByType
    2. 支持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")@ResponseBodypublic 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")@ResponseBodypublic 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")@ResponseBodypublic 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重名

  1. 自定义注解

    @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 {};
    }
  2. 新建一个UniqueConstraintValidator类来验证注解

    //自定义校验注解 的 校验逻辑
    //不需要加注解@Component,因为实现了ConstraintValidator接口自动会注册为spring bean
    public class UniqueConstraintValidator implements ConstraintValidator<UniqueConstraint,Object> {@Autowiredprivate UserService userService;@Overridepublic void initialize(UniqueConstraint uniqueConstraint) {System.out.println("my validator init");}//Object为校验的字段类型//返回true则校验成功//o为校验字段的值,constraintValidatorContext为校验注解里的属性值@Overridepublic 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;@NotBlankprivate String newPass;@NotBlankprivate 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) {// 注1validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());}} catch (RpcException e) {throw e;} catch (ValidationException e) {// 注2return 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) {// 注1violations.addAll(validator.validate(parameterBean, classgroups ));}for (Object arg : arguments) {// 注2validate(violations, arg, classgroups);}if (!violations.isEmpty()) {// 注3logger.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;@Overridepublic String sayHello(String name) {return demoService.sayHello(name);}@Overridepublic String sayGoodBye(Content content) {return demoService.sayGoodBye(content);}@Overridepublic 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. 注意事项

  1. @valid注解 可以用在成员属性上,决定了它可以嵌套校验的功能

  2. @valid只能⽤在controller。@Validated可以⽤在其他被spring管理的类上

  3. @Validated不指定分组时 只会匹配未分组的注解;分组后则会匹配组内注解+未分组的注解。它的机制为未分组也是一个组,也就是说Validated 始终有组的概念,即使你没有显示指定

  4. 校验bean时:

    • @Valid 和 @Validated 都是直接修饰方法参数就可以生效,抛出org.springframework.web.bind.MethodArgumentNotValidException异常,都会阻断方法执行,@RestControllerAdvice只是优化方法返回值而已。
    • @Valid 和 @Validated 放到Controller类和参数类都不起作用, Controller类的方法上也不起作用。
  5. 校验字段时:

    • @Validated 加在类上,配合参数校验注解可以生效,其余情况 @Validated在 方法上、 参数上均不生效。
    • @Valid注解无法校验非bean类型参数。
  6. 参数校验未通过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){}
  7. 最后,一定要注意注解的修饰类型,类型不符时会直接报错的,并且提示很不友好。第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. 1-9 ↩︎

springboot---检验请求参数相关推荐

  1. 解决Springboot GET请求参数过长的情况

    项目场景: 使用Spring Boot进行项目开发,解决Springboot GET请求参数过长的情况 问题描述 报错信息:Springboot GET请求参数过长抛出异常:Request heade ...

  2. SpringBoot自定义请求参数转换器

    需求 我们可能对接客户的系统的时候,虽然Spring为我们提供的很多方便的转换器,但是遇到还是可能遇到需要自定义请求参数转换器的情况. 日期转换器 SpringBoot默认是没有配置日期转换器的我们可 ...

  3. springboot post 请求参数过长请求接口报错 An HTTP line is larger than 4096 bytes

    springboot项目post请求参数过长请求接口报错如下: 解决办法将post请求参数值调大: server:tomcat:max-http-form-post-size: 5MB

  4. Springboot中请求参数校验

    1.添加依赖 <!-- 参数校验 --> <dependency><groupId>org.springframework.boot</groupId> ...

  5. 关于springBoot post请求参数过大导致后端接收不到参数问题

    在配置文件直接添加 也不知道哪个起作用了 反正都加上就完事了 指定上传文件的最大文件大小,默认为1MB spring.servlet.multipart.max-file-size=200MB 指定在 ...

  6. SpringBoot获取请求的参数

    说明 SpringBoot 为我们封装了许多,简便的获取请求参数的方法! 1.获取无注解获取请求参数 请求地址:http://192.168.0.115:8080/myproject/test/noA ...

  7. 拦截器获取请求参数post_「SpringBoot WEB 系列」RestTemplate 之自定义请求头

    [WEB 系列]RestTemplate 之自定义请求头 上一篇介绍了 RestTemplate 的基本使用姿势,在文末提出了一些扩展的高级使用姿势,本篇将主要集中在如何携带自定义的请求头,如设置 U ...

  8. springboot获取多个请求参数_springboot获取URL请求参数的多种方式

    1.直接把表单的参数写在Controller相应的方法的形参中,适用于get方式提交,不适用于post方式提交. /** * 1.直接把表单的参数写在Controller相应的方法的形参中 * @pa ...

  9. 解决Springboot get请求是参数过长的情况

    解决Springboot get请求是参数过长的情况 问题原因 Springboot get请求是参数过长抛出异常:Request header is too large 的问题 错误描述 java. ...

  10. SpringBoot全局异常处理及前端请求参数校验

    SpringBoot全局异常捕获处理及参数校验 文章目录 SpringBoot全局异常捕获处理及参数校验 为什么要用全局异常处理? 如何进行全局异常捕获和处理? 统一结果封装 统一返回结果 枚举类 使 ...

最新文章

  1. centos添加新硬盘
  2. pythonurllib模块-python爬虫之urllib模块和requests模块学习
  3. DSP学习 -- cJSON使用教程
  4. 快手有佳人|2020快手女性人群价值报告
  5. 【ES6(2015)】Module模块
  6. MySQL 左右两表比较问题
  7. C++Builder2010创建线程
  8. Java8 实战系列-06-lambda 方法引用
  9. windows下的文件服务器监控
  10. RC振荡电路 双三极管多谐振荡器
  11. 搜狗输入法纯净_五款良心无广告的纯净输入法推荐
  12. ajax不执行回调函数的原因
  13. 信息系统安全等级保护 备案表
  14. 批量微信号码过滤程序
  15. 中考考试的指令广播_中考考试指令系统使用的说明.doc
  16. skynet mysql 携程_有哪些小型后台服务端开源项目?
  17. php onlyoffice,快速增长的ONLYOFFICE平台带来重大更新以响应用户需求
  18. 【Freeswitch从入门到精通】二、初识Freeswitch
  19. Mezzanine user 扩展
  20. tomcat启动设备未就绪_安全启动异常就绪协程

热门文章

  1. 送你4个直抵灵魂深处的问题
  2. php 同一命名空间 不导入,PHP引入命名空间无法使用的问题报错!
  3. Bootstrap—标签label和徽章badge
  4. android源码学习-Toast实现原理讲解
  5. wordsReminder——自定义背单词小工具
  6. js:面向对象编程,带你认识封装、继承和多态
  7. 帮豆包刷“天天爱消除”,“天天连萌”
  8. Nginx windows 详细安装部署教程
  9. 过来人聊聊眼中的普通码农和技术大牛的区别
  10. 无线网络连接显示网络受限无法连接网络