Spring Boot优雅的做接口参数校验

610 阅读4分钟

gitee链接

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 "登录成功";
}

分别测试就可以了,同一个对象可以适配多个接口的参数校验需求。