Spring Boot 参数校验

本文将介绍如何在 SpringBoot 中优雅地进行参数校验。

一、普通做法

最普通的做法就是通过代码进行校验,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String addUser(User user) {
if (user == null || user.getId() == null || user.getAccount() == null || user.getPassword() == null || user.getEmail() == null) {
return "对象或者对象字段不能为空";
}
if (StringUtils.isEmpty(user.getAccount()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
return "不能输入空字符串";
}
if (user.getAccount().length() < 6 || user.getAccount().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return "密码长度必须是6-16个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}

···

}

这种做法有许多缺点:

  • 十分繁琐
  • 同一个对象的校验代码需要在多个方法中重复书写
  • 参数校验逻辑与业务逻辑耦合

二、Validator 简单实用

1. 引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在 SpringBoot 2.3 之前,validation 包集成在 web 包中

2. 检验实体类

(1) 添加校验规则

在实体类中添加校验规则,如下:

1
2
3
4
5
6
7
8
9
public class LoginParam {

@Length(min = 5, max = 10, message = "用户名长度应在5-10之间")
private String username;

@NotNull(message = "密码不能为空")
private String password;

}

(2) 开启校验

在接口方法的参数处添加 @Valid@Validated 开启校验,如下:

1
2
3
4
@PostMapping("/login")
public String login(@RequestBody @Validated LoginParam loginParam) {
···
}

3. 校验基本类型

(1) 添加检验规则

在接口方法的参数上进行校验规则注解,如下:

1
2
3
4
@GetMapping("/test")
public String test(@NotNull String email) {
return "haha";
}

(2) 开启校验

在接口类上添加 @Validated 注解,如下:

1
2
3
4
5
6
7
8
9
10
@Validated
@RestController
public class AdminAccountController {

@GetMapping("/test")
public String test(@NotNull String email) {
return "haha";
}

}

4. 校验结果

如果参数无法通过校验,将会抛出异常,其中:

  • 如果校验实体类失败,并且该实体类 @RequestBody 注解,会抛出 org.springframework.web.bind.MethodArgumentNotValidException
  • 如果校验实体类失败,并且该实体类没有用 @RequestBody 注解,会抛出 org.springframework.validation.BindException
  • 如果校验基本类型失败,会抛出 javax.validation.ConstraintViolationException

三、关于 @Valid 和 @Validated

在进行参数校验时,既可以使用 @Valid 也可以使用 @Validated,两者的区别在于:

  • @Valid 在 javax 包下;@Validated 在 spring 包下

  • @Valid 支持嵌套验证;@Validated 不支持

  • @Valid 不支持分组检验;@Validated 支持分组检验

    分组检验:在不同的分组下具有不同的校验规则

四、Validator 配合 BindingResult

如果我们希望传递参数校验的错误信息,可以通过 BindingResult 实现,具体方法为:

  • 在接口方法处添加类型为 BindingResult 的参数

    此时,Validator 不会再直接抛出异常,而是会将异常放入 BindingResult 中,交由程序员处理

  • 通过 BindingResult 判断是否有错误,当有错误时,将错误信息返回

如下:

1
2
3
4
5
6
7
8
public String login(@RequestBody @Validated LoginParam loginParam, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
}
return adminAccountService.login(loginParam);
}

五、Validator 配置全局异常处理

1. 全局异常处理类

具体请看:

Spring Boot 全局异常处理

2. 校验异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestControllerAdvice
public class ExceptionAdvice {

/**
* 参数校验异常
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class, ConstraintViolationException.class})
public ResultVO validateFailedException(Exception e) {
String msg = null;
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
} else if (e instanceof BindException) {
BindException ex = (BindException) e;
msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException ex = (ConstraintViolationException) e;
msg = ((ConstraintViolation<?>) (ex.getConstraintViolations().toArray()[0])).getMessage();
}
return ResultVO.error(ResultCode.VALIDATE_FAILED, msg);
}

}

六、快速失败

默认情况下,Validator 始终会校验所有字段,可以通过配置开启 Fali Fast 模式,有一项校验失败,便终止校验、抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ValidatorConfig {

@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}

参考