Skip to main content

SpringBoot RestAPI + Spring Validator 入参验证并处理异常

“不要相信前端发来的数据”,请求参数验证是后端必不可少的操作。Spring Validator 是 Spring 框架的其中一个模块,可以提供入参验证。

本文基于现有 SpringBoot MVC 项目(下方有快速搭建方法),再附加 Spring Validator 模块,实现 RestAPI 应用的入参验证功能。

准备工具

实验工具版本说明

工具 版本 说明
Gradle 7.2 依赖管理工具
springboot gradle plugin 2.4.8 SpringBoot Gradle 插件
springboot starter web 2.5.5 SpringBoot web 模块整合
springboot starter validator 2.5.4 SpringBoot validator 模块整合
lombok 1.18.20 辅助开发工具包,推荐使用
Java 11 任意 Java 11 发行版,推荐 AdoptOpenJDK

初始一个 SpringBoot RestAPI 项目

详细搭建过程已经在 SpringBoot RestAPI 文章详细说了,这里只展示快速搭建一个基本的 SpringBoot RestAPI 项目。

快速搭建 SpringBoot RestAPI 项目

1. Gradle 新建 Java Application 项目

gradle init

# 问项目类型: 选择 2(application)
# 问开发语言: 选择 3(Java)
# 问有无子项目: 选择 1(no)
# 问构建语言: 选择 1(Groovy)
# 问测试框架: 默认
# 问项目名称: 默认
# 问包名: org.example

2. app/build.gradle 添加 SpringBoot 插件

plugins {
  // ...

  // [添加] SpringBoot Gradle 插件
  id "org.springframework.boot" version "2.4.8"
}

3. app/build.gradle 添加依赖包

dependencies {
    // ...

    // [增加] SpringBoot Starter Web 模块整合包
    implementation 'org.springframework.boot:spring-boot-starter-web:2.5.5'
}

4. 程序入口类 app/src/main/java/org/example/App.java 初始化 Spring 应用

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

5. 测试启动,检查 SpringBoot 自带的 Tomcat 是否正常运行

./gradlew bootRun

接下来的实验都在上方新建的项目做操作。

添加 Spring Validator 依赖

app/build.gradle 添加 SpringBoot 验证工具整合包依赖。

dependencies {
  // ...

  // [添加] SpringBoot Starter Validator 模块整合包
  implementation 'org.springframework.boot:spring-boot-starter-validation:2.5.4'
  // [添加] lombok 辅助开发工具包 & 辅助编译工具包
  implementation 'org.projectlombok:lombok:1.18.20'
  annotationProcessor 'org.projectlombok:lombok:1.18.20'
}

lombok 是非常好用的辅助开发工具,最常用的功能 自动生成 Getter Setter

可以少写很多 getXXX()、setXXX(),避免浪费时间!作为替代,只要在类前面声明一下 @Getter、@Setter 即可自动生成 Getter Setter。

下方给出的示例会用到 lombok。

添加依赖后,可以引入入参验证的功能了。

法一: 用 @Validated 实现入参验证

实现步骤:

  • Controller 类前面(class level) 声明 @Validated

  • @Min@Max 等声明约束方法的形参,这些声明来自 javax.validation 包。

控制器 Usage1Controller.java
@RestController
@Validated // 添加 @Validated
public class Usage1Controller {

  // @Min(1) : 要求 userID 最小为 1
  @GetMapping("/user/{id}")
  public Map<String, String> handleUsage1(
    @PathVariable(name = "id") 
    @Min(value = 1, message = "userID 必须大于等于1") 
    long userID
  ) {
    return new HashMap<>(){{
      put("msg", "welcome back, " + userID);
    }};
  }

}

运行结果

# 测试 方法1验证器,通过
curl -XGET localhost:8080/user/1
# 结果 {"msg":"welcome back, 1"}


# 测试 方法1验证器,不通过
curl localhost:8080/user/0
# 返回 {"timestamp":"2021-10-15T07:53:40.170+00:00","status":500,"error":"Internal Server Error","path":"/user/0"}

如果入参验证不通过,会抛出 javax.validation.ConstraintViolationException,Spring 不会处理这个异常,所以会将请求视为 “内部错误”,返回 500。所以后端要处理这个异常,将正确的错误信息返回给客户端。

处理抛出的异常

处理步骤:

  • 类前面加上 @RestControllerAdvice,标记为错误处理类。

  • 添加自定义方法,处理指定的异常。

错误处理类 Usage1ExceptionHandler.java
@RestControllerAdvice
public class Usage1ExceptionHandler {

  // 处理 ConstraintViolationException
  @ExceptionHandler(ConstraintViolationException.class)
  // 遇到这个异常时,响应码为 400
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public Map<String, String> handleUsage1Exception(ConstraintViolationException e) {
    // 获取异常信息然后输出 { "msg": "错误信息" }
    return new HashMap<>(){{
      put("msg", e.getMessage());
    }};
  }

}

运行结果

# 测试 方法1验证器,不通过
curl -XGET localhost:8080/user/0
# 结果 {"msg":"handleUsage1.userID: userID 必须大于等于1"}

法二: 用 @Valid 实现 JSON 入参验证

另一种验证入参的方法,主要用在入参为 JSON 的场景。

实现步骤:

  • 创建 纯Java对象,用 @Min@Max 等声明,约束成员变量

  • Controller类,在方法签名中 @RequestBody + @Valid 标记形参

控制器 Usage2ExceptionHandler.java
@RestController
public class Usage2Controller {

  @PostMapping("/person")
  public Person person (@Valid @RequestBody Person person) {
    return person; // 原样返回 json
  }

  // POJO class (纯Java对象类)
  @lombok.Getter
  @lombok.Setter
  public static class Person {
    @NotEmpty // 要求名字不能空
    private String name;
    @NotEmpty // 要求昵称不能空
    private String nickname;
  }
}

运行结果

# 测试 方法2验证器,不通过
curl \
-X POST \
-H 'content-type: application/json' \
--url 'localhost:8080/person' \
-d '{ "name": "" }'
# 返回 {"timestamp":"2021-10-15T08:32:10.940+00:00","status":400,"error":"Bad Request","path":"/person"}

入参无法通过时,会抛出:

org.springframework.web.bind.MethodArgumentNotValidException

通过包名看到这是 Spring 的异常,Spring 会自己捕获这个异常,然后返回 400。但是有时候前端也希望知道入参哪里错了,这时候后端也要用 @RestControllerAdvice 来捕获这个异常,然后返回正确的错误信息。

处理抛出的异常

处理方法同上,也是用 @RestControllerAdvice 标记。

错误处理类 Usage2ExceptionHandler.java
@RestControllerAdvice
public class Usage2ExceptionHandler {

  // 处理 MethodArgumentNotValidException
  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  public Map<String, String> handleUsage2Exception(MethodArgumentNotValidException e) {
    return new HashMap<>(){{
      // Exception 有很多方法,这里只展示错误信息
      put("msg", e.getBindingResult().getFieldError().getDefaultMessage());
    }};
  }
}

运行结果

# 测试 方法2验证器,不通过
curl \
-X POST \
-H 'content-type: application/json' \
--url 'localhost:8080/person' \
-d '{ "name": "" }'
# 结果 {"msg":"名称不能为空"}


# 测试 方法2验证器,不通过
curl \
-X POST \
-H 'content-type: application/json' \
--url 'localhost:8080/person' \
-d '{ "name": "luo" }'
# 结果 {"msg":"昵称不能为空"}


# 测试 方法2验证器,通过
curl \
-X POST \
-H 'content-type: application/json' \
--url 'localhost:8080/person' \
-d '{ "name": "luo", "nickname": "sam" }'
# 结果 {"name":"luo","nickname":"sam"}

附录与引用

@Valid方法 与 @Validated方法 区别

  • @Valid 验证不通过时,抛出 ConstraintViolationException
  • @Validated 验证不通过时,抛出 MethodArgumentNotValidException

ConstraintViolation 和 MethodArgumentNotValid 区别 (StackOverflow)

ConstraintViolation 属于 Bean Validation(包名: javax.validation.*)。Bean Validation 是一个标准。Spring Validation 只是这个标准的实现之一,Bean Validation 表现在通用性比较好。

MethodArgumentNotValid 属于 Spring Validation,只会被 Spring 抛出来。

应该用哪种方法?

用哪种验证方法都可,取决具体需求。

需求 推荐方法 可能要处理的 Exception
验证 URL query、param 等参数 @Validated ConstraintViolationException
验证 JSON 入参 @Valid + @RequestBody + POJO MethodArgumentNotValidException

Bean validator 和 hibernate validator 区别 (StackOverflow)

一个是标准,一个是实现的区别。引用的标准(有时)可以放在别的环境用,所以建议多用标准。

SpringMVC 处理 Controller 抛出的错误 (Medium)

类前面(class level) 声明 @ControllerAdvice 配合 @ExceptionHandler 即可,如果是 RestAPI 应用则用 @RestControllerAdvice

区别是 @RestControllerAdvice 多标记一个 @ResponseBody。

@ResponseBody 的作用是将 方法的返回值 作为 respose body 输出。

本实验参考的使用例 (Spring Rest Validation Example by mkyong)

本实验项目归档(含步骤)

下载 | Download