Spring Boot版本:2.3.4.RELEASE
场景
请求参数:
// 用户对象
public class User {
// 用户名
private String username;
// 密码
private String password;
// 年龄
private Integer age;
// 地址
private String address;
...
}
项目里有两个接口:
-
注册接口:必填参数:username,password,age,address
@PostMapping("/register") public String registerWithoutValidate(@RequestBody User user) { return "注册成功"; } -
登录接口:必填参数:username,password
@PostMapping("/login") public String login(@RequestBody User user) { return "登录成功"; }
不好的校验方式
参数校验如果用if的话,是这样的:
@PostMapping("/register")
public String registerWithoutValidate(@RequestBody User user) {
if (StringUtils.isEmpty(user.getUsername())) {
return "缺少用户名";
}
if (StringUtils.isEmpty(user.getPassword())) {
return "缺少密码";
}
if (user.getAge() != null && user.getAge() == 0) {
return "缺少年龄";
}
if (StringUtils.isEmpty(user.getAddress())) {
return "缺少地址";
}
return "注册成功";
}
可以看出,代码显得臃肿,在参数较多的时候更是惨不忍睹。
解决方案
添加参数校验依赖:
<!--参数校验依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
然后修改下User类:
// 用户对象
public class User {
// 用户名
@NotBlank(message = "用户名不能为空")
private String username;
// 密码
@NotBlank(message = "密码不能为空")
private String password;
// 年龄
@NotNull(message = "年龄不能为空")
private Integer age;
// 地址
@NotBlank(message = "地址不能为空")
private String address;
...
}
还有修改下注册接口,给User对象添加@Validated注解:
@PostMapping("/register")
public String register(@Validated @RequestBody User user) {
return "注册成功";
}
此时请求注册接口,当缺少任意一个参数的时候,都会报错400:
{
"timestamp": "2021-11-15T07:37:32.847+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/register"
}
这是因为我们没有进行异常捕获,所以直接将异常抛出了,为了给用户友好的提示,我们可以在接口处添加BindingResult对象捕获异常并返回给用户:
@PostMapping("/register")
public String register(@Validated @RequestBody User user, BindingResult bindingResult) {
for (ObjectError error : bindingResult.getAllErrors()) {
// 缺少的参数提示
return error.getDefaultMessage();
}
return "注册成功";
}
此时我们测试下缺少密码参数的请求:
{
"username": "cc",
"age":1,
"address":"123"
}
结果是这样的:
密码不能为空
全局异常捕获
参数校验已经实现了,但是我们可以发现一个问题,需要做参数校验的接口,都要加上BindingResult对象和一个for循环,这种代码冗余是不能被接受的,所以我们结合全局异常捕获功能,统一对参数校验失败事件进行处理。
创建全局异常捕获类:
package com.cc.exception;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常捕获
* @author cc
* @date 2021/06/16 15:10
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
// 未注册的异常全部在这里处理
@ExceptionHandler(value = Exception.class)
public String catchException(Exception e) {
e.printStackTrace();
return e.getMessage();
}
/**
* 参数校验异常
* @author cc
* @date 2021-07-12 10:15
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public String catchMethodArgumentNotValidException(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
return "参数校验异常,请检查请求参数";
}
}
有了全局异常捕获,我们可以将接口上的BindingResult以及for循环语句去掉:
@PostMapping("/register")
public String register(@Validated @RequestBody User user) {
return "注册成功";
}
再次测试下缺少密码参数的请求,结果是这样的:
密码不能为空
参数校验分组
我们一直在对注册接口做参数校验,现在到登录接口的时候我们会发现一个问题:
注册和登录接口用的是同一个对象User,但是两个接口的参数校验并不一样,注册接口有4个必填参数,登录接口只有两个,为了能让一个对象适用多个接口的参数校验,我们需要使用参数校验分组功能。
让我们修改User对象:
// 用户对象
public class User {
public interface RegisterValid {} // 注册接口校验
public interface LoginValid{} // 登录接口校验
// 用户名
@NotBlank(message = "用户名不能为空", groups = {RegisterValid.class, LoginValid.class})
private String username;
// 密码
@NotBlank(message = "密码不能为空", groups = {RegisterValid.class, LoginValid.class})
private String password;
// 年龄
@NotNull(message = "年龄不能为空", groups = {RegisterValid.class})
private Integer age;
// 地址
@NotBlank(message = "地址不能为空", groups = {RegisterValid.class})
private String address;
...
}
因为分组需要有一个class对象,所以在User类里建立了interface作为组,然后给每一个参数分配组,表示该校验需要在该组下才生效。
有了分组,接下来当然要在接口上指定:
@PostMapping("/register")
public String register(@Validated(value = User.RegisterValid.class) @RequestBody User user) {
return "注册成功";
}
@PostMapping("/login")
public String login(@Validated(value = User.LoginValid.class) @RequestBody User user) {
return "登录成功";
}
分别测试就可以了,同一个对象可以适配多个接口的参数校验需求。