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

引言

不知道大家平时的业务开发过程中 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. 四. sql上线平台

    一.inception安装使用 inception是一个集审核.执行.备份及生成回滚语句于一身的MySQL自动化运维工具 [root@CentOS ~]# [root@CentOS ~]# wget ...

  2. SpringBoot-读取classpath下文件

    文章目录 开发过程中,必不可少的需要读取文件,对于打包方式的不同,还会存在一些坑,比如以jar包方式部署时,文件都存在于jar包中,某些读取方式在开发工程中都可行,但是打包后,由于文件被保存在jar中 ...

  3. 第三方登陆---GITEE

    第三方登陆QQ通行入口 https://www.cnblogs.com/Yangbuyi/p/13194007.html 呼~~~~ 应身边的同学要集成第三方登陆 gitee.github.qq登陆. ...

  4. ansible 2.7 API

    # coding:utf-8 # @Time : 2019-01-14 15:22 # @Author : 小贰 # @FileName: ansible_sync_hosts.py # @funct ...

  5. 小烈送菜——奇怪的dp

    小烈送菜 题目描述 小烈一下碰碰车就被乐满地的工作人员抓住了.作为扰乱秩序的惩罚,小烈必须去乐满地里的"漓江村"饭店端盘子. 服务员的工作很繁忙.他们要上菜,同时要使顾客们尽量高兴 ...

  6. When Lambo with Howdoo

    原文链接:https://howdoo.io/when-lambo/ 为了庆祝即将推出的革命性新社交媒体平台Howdoo以及我们令人惊喜的合作伙伴关系和社区,我们正在发起一项竞赛,以最终回答“When ...

  7. 5 个 IDEA 必备插件,让效率成为习惯

    IDEA 是最好的 IDE,谁赞成,谁反对? IDEA 是 Java 开发人员中最受欢迎和用户友好的 IDE 之一,它具有许多便利的功能,例如智能代码完成,自动执行繁琐的重复开发任务,因此它已成为 J ...

  8. 题解:2018级算法第一次上机 C1-pair

    题目描述 北航2018级软件学院算法分析与设计第一次上机第三题 样例 实现解释 题目类型: 这类题目其实就是典型的递归分析语句形式的问题,也是编译原理课程中语法分析的重要方法之一. 解决方案: 为了解 ...

  9. redis(十六):Redis 安装,部署(LINUX环境下)

    第一步:下载安装包 访问https://redis.io/download  到官网进行下载.这里下载最新的4.0版本. 第二步:安装 1.通过远程管理工具,将压缩包拷贝到Linux服务器中,执行解压 ...

  10. 数据可视化之DAX篇(八) DAX学习:使用VAR定义变量

    https://zhuanlan.zhihu.com/p/64414205 前面介绍如何使用DAX生成日期表的时候,使用了VAR,有人留言问这个VAR怎么理解,那么这篇文章就来介绍VAR以及它的用法. ...