写得好的没我写得全,写得全的没我写得好

引言

不知道大家平时的业务开发过程中 controller 层的参数校验都是怎么写的?是否也存在下面这样的直接判断?

public String add(UserVO userVO) {
if(userVO.getAge() == null){
return "年龄不能为空";
}
if(userVO.getAge() > 120){
return "年龄不能超过120";
}
if(userVO.getName().isEmpty()){
return "用户名不能为空";
}
// 省略一堆参数校验...
return "OK";
}

业务代码还没开始写呢,光参数校验就写了一堆判断。这样写虽然没什么错,但是给人的感觉就是:不优雅,不专业。

其实Spring框架已经给我们封装了一套校验组件:validation。其特点是简单易用,自由度高。接下来课代表使用springboot-2.3.1.RELEASE搭建一个简单的 Web 工程,给大家一步一步讲解在开发过程中如何优雅地做参数校验。

1. 环境搭建

springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入如下依赖:

<!--校验组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

springboot-2.3之前的版本只需要引入 web 依赖就可以了。

2.小试牛刀

参数校验非常简单,首先在待校验字段上增加校验规则注解

public class UserVO {
@NotNull(message = "age 不能为空")
private Integer age;
}

然后在controller方法中添加@Validated和用于接收错误信息的BindingResult就可以了,于是有了第一版:

public String add1(@Validated UserVO userVO, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
if(!fieldErrors.isEmpty()){
return fieldErrors.get(0).getDefaultMessage();
}
return "OK";
}

通过工具(postman)去请求接口,如果参数不符合规则,会将相应的 message信息返回:

age 不能为空

内置的校验注解有很多,罗列如下:

注解 校验功能
@AssertFalse 必须是false
@AssertTrue 必须是true
@DecimalMax 小于等于给定的值
@DecimalMin 大于等于给定的值
@Digits 可设定最大整数位数和最大小数位数
@Email 校验是否符合Email格式
@Future 必须是将来的时间
@FutureOrPresent 当前或将来时间
@Max 最大值
@Min 最小值
@Negative 负数(不包括0)
@NegativeOrZero 负数或0
@NotBlank 不为null并且包含至少一个非空白字符
@NotEmpty 不为null并且不为空
@NotNull 不为null
@Null 为null
@Past 必须是过去的时间
@PastOrPresent 必须是过去的时间,包含现在
@PositiveOrZero 正数或0
@Size 校验容器的元素个数

3. 规范返回值

待校验参数多了之后我们希望一次返回所有校验失败信息,方便接口调用方进行调整,这就需要统一返回格式,常见的就是封装一个结果类。

public class ResultInfo<T>{
private Integer status;
private String message;
private T response;
// 省略其他代码...
}

改造一下controller 方法,第二版:

public ResultInfo add2(@Validated UserVO userVO, BindingResult result) {
List<FieldError> fieldErrors = result.getFieldErrors();
List<String> collect = fieldErrors.stream()
.map(o -> o.getDefaultMessage())
.collect(Collectors.toList());
return new ResultInfo<>().success(400,"请求参数错误",collect);
}

请求该方法时,所有的错误参数就都返回了:

{
"status": 400,
"message": "请求参数错误",
"response": [
"年龄必须在[1,120]之间",
"bg 字段的整数位最多为3位,小数位最多为1位",
"name 不能为空",
"email 格式错误"
]
}

4. 全局异常处理

每个Controller方法中都如果都写一遍对BindingResult信息的处理,使用起来还是很繁琐。可以通过全局异常处理的方式统一处理校验异常。

当我们写了@validated注解,不写BindingResult的时候,Spring 就会抛出异常。由此,可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。

全局异常处理类只需要在类上标注@RestControllerAdvice,并在处理相应异常的方法上使用@ExceptionHandler注解,写明处理哪个异常即可。

@RestControllerAdvice
public class GlobalControllerAdvice {
private static final String BAD_REQUEST_MSG = "客户端请求参数错误";
// <1> 处理 form data方式调用接口校验失败抛出的异常
@ExceptionHandler(BindException.class)
public ResultInfo bindExceptionHandler(BindException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> collect = fieldErrors.stream()
.map(o -> o.getDefaultMessage())
.collect(Collectors.toList());
return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
}
// <2> 处理 json 请求体调用接口校验失败抛出的异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> collect = fieldErrors.stream()
.map(o -> o.getDefaultMessage())
.collect(Collectors.toList());
return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
}
// <3> 处理单个参数校验失败抛出的异常
@ExceptionHandler(ConstraintViolationException.class)
public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) {
Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
List<String> collect = constraintViolations.stream()
.map(o -> o.getMessage())
.collect(Collectors.toList());
return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
} }

事实上,在全局异常处理类中,我们可以写多个异常处理方法,课代表总结了三种参数校验时可能引发的异常:

  1. 使用form data方式调用接口,校验异常抛出 BindException
  2. 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
  3. 单个参数校验异常抛出ConstraintViolationException

注:单个参数校验需要在参数上增加校验注解,并在类上标注@Validated

全局异常处理类可以添加各种需要处理的异常,比如添加一个对Exception.class的异常处理,当所有ExceptionHandler都无法处理时,由其记录异常信息,并返回友好提示。

5.分组校验

如果同一个参数,需要在不同场景下应用不同的校验规则,就需要用到分组校验了。比如:新注册用户还没起名字,我们允许name字段为空,但是不允许将名字更新为空字符。

分组校验有三个步骤:

  1. 定义一个分组类(或接口)
  2. 在校验注解上添加groups属性指定分组
  3. Controller方法的@Validated注解添加分组类
public interface Update extends Default{
}
public class UserVO {
@NotBlank(message = "name 不能为空",groups = Update.class)
private String name;
// 省略其他代码...
}
@PostMapping("update")
public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
return new ResultInfo().success(userVO);
}

细心的同学可能已经注意到,自定义的Update分组接口继承了Default接口。校验注解(如: @NotBlank)和@validated默认都属于Default.class分组,这一点在javax.validation.groups.Default注释中有说明

/**
* Default Jakarta Bean Validation group.
* <p>
* Unless a list of groups is explicitly defined:
* <ul>
* <li>constraints belong to the {@code Default} group</li>
* <li>validation applies to the {@code Default} group</li>
* </ul>
* Most structural constraints should belong to the default group.
*
* @author Emmanuel Bernard
*/
public interface Default {
}

在编写Update分组接口时,如果继承了Default,下面两个写法就是等效的:

@Validated({Update.class})

@Validated({Update.class,Default.class})

请求一下/update接口可以看到,不仅校验了name字段,也校验了其他默认属于Default.class分组的字段

{
"status": 400,
"message": "客户端请求参数错误",
"response": [
"name 不能为空",
"age 不能为空",
"email 不能为空"
]
}

如果Update不继承Default@Validated({Update.class})就只会校验属于Update.class分组的参数字段,修改后再次请求该接口得到如下结果,可以看到, 其他字段没有参与校验:

{
"status": 400,
"message": "客户端请求参数错误",
"response": [
"name 不能为空"
]
}

6.递归校验

如果 UserVO 类中增加一个 OrderVO 类的属性,而 OrderVO 中的属性也需要校验,就用到递归校验了,只要在相应属性上增加@Valid注解即可实现(对于集合同样适用)

OrderVO类如下

public class OrderVO {
@NotNull
private Long id;
@NotBlank(message = "itemName 不能为空")
private String itemName;
// 省略其他代码...
}

在 UserVO 类中增加一个 OrderVO 类型的属性

public class UserVO {
@NotBlank(message = "name 不能为空",groups = Update.class)
private String name;
//需要递归校验的OrderVO
@Valid
private OrderVO orderVO;
// 省略其他代码...
}

调用请求验证如下:

7. 自定义校验

Spring 的 validation 为我们提供了这么多特性,几乎可以满足日常开发中绝大多数参数校验场景了。但是,一个好的框架一定是方便扩展的。有了扩展能力,就能应对更多复杂的业务场景,毕竟在开发过程中,唯一不变的就是变化本身

Spring Validation允许用户自定义校验,实现很简单,分两步:

  1. 自定义校验注解
  2. 编写校验者类

代码也很简单,结合注释你一看就能懂

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑
public @interface HaveNoBlank { // 校验出错时默认返回的消息
String message() default "字符串中不能含有空格"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; /**
* 同一个元素上指定多个该注解时使用
*/
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface List {
NotBlank[] value();
}
}
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// null 不做检验
if (value == null) {
return true;
}
if (value.contains(" ")) {
// 校验失败
return false;
}
// 校验成功
return true;
}
}

自定义校验注解使用起来和内置注解无异,在需要的字段上添加相应注解即可,同学们可以自行验证

回顾

以上就是如何使用 Spring Validation 优雅地校验参数的全部内容,下面重点总结一下文中提到的校验特性

  1. 内置多种常用校验注解
  2. 支持单个参数校验
  3. 结合全局异常处理自动组装校验异常
  4. 分组校验
  5. 支持递归校验
  6. 自定义校验

本文代码已上传至

GitHub


关注 Java课代表,获取最新 Java 干货

使用Spring Validation优雅地校验参数的更多相关文章

  1. SpringBoot Validation优雅的全局参数校验

    前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...

  2. Dubbo服务如何优雅的校验参数

    一.背景 服务端在向外提供接口服务时,不管是对前端提供HTTP接口,还是面向内部其他服务端提供的RPC接口,常常会面对这样一个问题,就是如何优雅的解决各种接口参数校验问题? 早期大家在做面向前端提供的 ...

  3. Spring Validation 表单校验

    最近开发一个项目,发现字段校验比较多,同事提出使用Validation校验能更方便一些,今天记录下来,供以后参考: 一.在pom.xml中添加依赖: <dependency> <gr ...

  4. Spring Validation最佳实践及其实现原理,参数校验没那么简单!

    之前也写过一篇关于Spring Validation使用的文章,不过自我感觉还是浮于表面,本次打算彻底搞懂Spring Validation.本文会详细介绍Spring Validation各种场景下 ...

  5. @Validated和@Valid区别:Spring validation验证框架对入参实体进行嵌套验证必须在相应属性(字段)加上@Valid而不是@Validated

    Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR- ...

  6. spring-boot 使用hibernate validation对参数进行优雅的校验

    springboot天生支持使用hibernate validation对参数的优雅校验,如果不使用它,只能对参数挨个进行如下方式的手工校验,不仅难看,使用起来还很不方便: if(StringUtil ...

  7. 2. Bean Validation声明式校验方法的参数、返回值

    你必须非常努力,才能干起来毫不费力.本文已被 https://www.yourbatman.cn 收录,里面一并有Spring技术栈.MyBatis.JVM.中间件等小而美的专栏供以免费学习.关注公众 ...

  8. Spring Validation-用注解代替代码参数校验

    Spring Validation 概念 在原先的编码中,我们如果要验证前端传递的参数,一般是在接受到传递过来的参数后,手动在代码中做 if-else 判断,这种编码方式会带来大量冗余代码,十分的不优 ...

  9. SpringBoot接口 - 如何优雅的对参数进行校验?

    在以SpringBoot开发Restful接口时, 对于接口的查询参数后台也是要进行校验的,同时还需要给出校验的返回信息放到上文我们统一封装的结构中.那么如何优雅的进行参数的统一校验呢? @pdai ...

随机推荐

  1. Java基础Day07(Map Calender Date 包装类 System类 异常)

    1.Map Map是集合容器,存放的元素有键与值两部分组成,通过键可以找到所对应的值,键和值必须是引用数据类型,键是唯一的,不能重复,没有顺序. HashMap是Map的实现类. Map的添加,删除, ...

  2. C++ 调用Python文件方法传递字典参数并接收返回值

    首先本地需要安装有Python环境,然后在c++工程中包含Python的头文件,引用Python的lib库. //python 初始化 Py_Initialize(); if (!Py_IsIniti ...

  3. jira仪表盘的建立与共享

    一般在项目测试阶段,可以通过jira仪表盘清晰的展示bug的各个状态的数量,各个开发人员的bug数量. 有效督促开发解决问题. 也为测试日报提供了良好的数据支持,减少人工统计的工作量. 1.建议筛选器 ...

  4. EntityFrameworkCore 开发实践问题及规范

    严重问题 客户端求值 如where条件包含的GetValueOrDefault()不能被翻译成sql语句 不规范代码段例子 public async Task<List<Person> ...

  5. eShopOnContainers 知多少[11]:服务间通信之gRPC

    引言 最近翻看最新3.0 eShopOncontainers源码,发现其在架构选型中补充了 gRPC 进行服务间通信.那就索性也写一篇,作为系列的补充. gRPC 老规矩,先来理一下gRPC的基本概念 ...

  6. 用python实现栈/队列/双端队列/链表

    栈是元素的有序集合,添加操作与移除操作都发生在其顶端,先进后出栈操作:创建空栈,增删(顶端),查(顶端元素,元素个数,是否为空)应用:将十进制数转换成任意进制数 class Stack: # 用列表创 ...

  7. scala 数据结构(五):队列 Queue

    1 队列 Queue-基本介绍 队列的说明 1)队列是一个有序列表,在底层可以用数组或是链表来实现. 2)其输入和输出要遵循先入先出的原则.即:先存入队列的数据,要先取出.后存入的要后取出 3)在Sc ...

  8. MYSQL 之 JDBC(十三):处理事务

    所谓事务是指:一组逻辑操作单元,使数据从一种状态变换到另一种状态. 事务的ACID属性 原子性,Atomicity:事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生. 一致性,Con ...

  9. Flask 基础组件(六):Session

    除请求对象之外,还有一个 session 对象.它允许你在不同请求间存储特定用户的信息.它是在 Cookies 的基础上实现的,并且对 Cookies 进行密钥签名要使用会话,你需要设置一个密钥. 设 ...

  10. Shaderlab-10chapter-立方体纹理、玻璃效果

    10.1.1天空盒子 window - Lighting - skyMaterial 创建mat,shader选自带的6 side shader 确保相机选skybox 如果某个相机需要覆盖,添加sk ...