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 项目。
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
包。
@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,标记为错误处理类。
-
添加自定义方法,处理指定的异常。
@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 标记形参
@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 标记。
@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 输出。