Spring Boot 自带的 spring-boot-starter-validation 包支持以标准注解的方式进行输入参数校验。spring-boot-starter-validation 包主要引用了 hibernate-validator 包,其参数校验功能就是 hibernate-validator 包所提供的。
本文即关注 spring-boot-starter-validation 包所涵盖的标准注解的使用、校验异常的捕获与展示、分组校验功能的使用,以及自定义校验器的使用。
本文示例工程使用 Maven 管理。
下面列出写作本文时所使用的 JDK、Maven 与 Spring Boot 的版本:
JDK:Amazon Corretto 17.0.8
Maven:3.9.2
Spring Boot:3.2.1
本文以开发一个 User 的 RESTful API 为例来演示 Validation 包的使用。
所以 pom.xml 文件除了需要引入 spring-boot-starter-validation 依赖外:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
还需要引入 spring-boot-starter-web 依赖:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
为了省去 Model 类 Getters 与 Setters 的编写,本文还使用了 lombok 依赖:
<!-- pom.xml -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
依赖准备好后,即可以尝试对 Validation 包进行使用了。
下面列出 spring-boot-starter-validation 包中常用的几个注解。
| 注解 | 作用字段类型 | 说明 |
|---|---|---|
@Null | 任意类型 | 验证元素值为 null |
@NotNull | 任意类型 | 验证元素值不为 null,无法验证空字符串 |
@NotBlank | CharSequence 子类型 | 验证元素值不为空(不为 null 且不为空字符串) |
@NotEmpty | CharSequence 子类型、Collection、Map、数组 | 验证元素值不为 null 且不为空(字符串长度、集合大小不为 0) |
@Min | 任何 Number 类型 | 验证元素值大于等于 @Min 指定的值 |
@Max | 任何 Number 类型 | 验证元素值小于等于 @Max 指定的值 |
@Digits | 任何 Number 类型 | 验证元素值的整数位数和小数位数上限 |
@Size | 字符串、Collection、Map、数组等 | 验证元素值的在指定区间之内,如字符长度、集合大小 |
@Range | 数值类型 | 验证元素值在最小值和最大值之间 |
@Email | CharSequence 子类型 | 验证元素值是电子邮件格式 |
@Pattern | CharSequence 子类型 | 验证元素值与指定的正则表达式匹配 |
@Valid | 任何非原子类型 | 指定递归验证关联的对象 |
下面就看一下如何使用这些注解。
假设我们想编写一个创建 User 的 RESTful API,而创建 User 时,其中有一些字段是有校验规则的(如:必填、满足字符串长度要求、满足电子邮件格式、满足正则表达式等)。
下面即看一下使用了 Validation 注解的 User Model 代码:
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class User {
@NotNull(message = "name can not be null")
@Size(min = 2, max = 20, message = "name length should be in the range [2, 20]")
private String name;
@NotNull(message = "age can not be null")
@Range(min = 18, max = 100, message = "age should be in the range [18, 100]")
private Integer age;
@NotNull(message = "email can not be null")
@Email(message = "email invalid")
private String email;
@NotNull(message = "phone can not be null")
@Pattern(regexp = "^1[3-9][0-9]{9}$", message = "phone number invalid")
private String phone;
}
下面浅析一下 User Model 中每个字段的校验规则:
name
其为字符串类型,使用了 @NotNull、@Size 注解,表示这个字段为必填,且字符串长度应属于区间 [2, 20]。
age
其为整数类型,使用了 @NotNull、@Range 注解,表示这个字段为必填,且数值应属于区间 [2, 20]。
其为字符串类型,使用了 @NotNull、@Email 注解,表示这个字段为必填,且为 Email 格式。
phone
其为字符串类型,使用了 @NotNull、@Pattern 注解,表示这个字段为必填,且为合法的国内手机号格式。
下面看一下统一的错误返回 Model 类 ErrorMessage 的代码:
// src/main/java/com/example/demo/model/ErrorMessage.java
package com.example.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ErrorMessage {
private String code;
private String description;
}
最后看一下 UserController 的代码:
// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
import com.example.demo.model.ErrorMessage;
import com.example.demo.model.User;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("")
public ResponseEntity<?> addUser(@RequestBody @Valid User user, BindingResult result) {
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
if (!allErrors.isEmpty()) {
ObjectError error = allErrors.get(0);
String description = error.getDefaultMessage();
return ResponseEntity.badRequest().body(new ErrorMessage("validation_failed", description));
}
}
// userService.addUser(user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
可以看到,UserController 的 addUser 方法使用了 User Model 来接收请求体,User Model 前使用了 @Valid 注解,该注解会对 User Model 中的字段根据注解设定的规则自动进行校验。此外,addUser 方法还有另外一个参数 BindingResult,该参数会捕获所有的字段校验错误信息,本文仅是将其中的第一个错误按照 ErrorMessage 格式返回了出来,没有任何错误信息则会返回 201 状态码。
下面使用 CURL 命令测试一下这个接口:
curl -L \
-X POST \
-H "Content-Type: application/json" \
http://localhost:8080/users \
-d '{"name": "Larry", "age": 18, "email": "larry@qq.com"}'
// 400
{ "code": "validation_failed", "description": "phone can not be null" }
可以看到,如果有字段不满足校验规则时,会返回设定的错误信息。
如果 Model 类中有嵌套对象,该怎么做验证呢?只需要在对应的字段上加上 @Valid 注解就可以了。
比如,User Model 中有一个字段为 address,其为 Address 对象,Address 类的代码如下:
// src/main/java/com/example/demo/model/Address.java
package com.example.demo.model;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class Address {
@NotNull(message = "province can not be null")
@Size(min = 2, max = 100, message = "province length should be in the range [10, 100]")
private String province;
@NotNull(message = "city can not be null")
@Size(min = 2, max = 100, message = "city length should be in the range [10, 100]")
private String city;
@NotNull(message = "street can not be null")
@Size(min = 10, max = 1000, message = "street length should be in the range [10, 1000]")
private String street;
}
则 User Model 中,若想对 address 字段应用校验规则,则需要额外在该字段上加一个 @Valid 注解:
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class User {
...
@Valid
@NotNull(message = "address can not be null")
private Address address;
}
了解了 Validation 包中常用注解的使用方式,下面看一下校验错误的异常捕获与展示。
我们注意到,上面的例子中 UserController 的 addUser 方法使用一个额外的参数 BindingResult 来接收校验错误信息,然后根据需要展示给调用者。但这种处理方式有点太冗余了,每个请求方法都需要加这么一个参数并重新写一遍错误返回的逻辑。
其实不加这个参数的话,若有校验错误,Spring Boot 框架会抛出一个 MethodArgumentNotValidException。所以简单一点的处理方式是:使用 @RestControllerAdvice 注解来将一个类标记为全局的异常处理类,针对 MethodArgumentNotValidException,只需要在这个异常处理类中进行统一捕获、统一处理就可以了。
异常处理类 MyExceptionHandler 的代码如下:
// src/main/java/com/example/demo/exception/MyExceptionHandler.java
package com.example.demo.exception;
import com.example.demo.model.ErrorMessage;
import org.springframework.http.HttpStatus;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
@RestControllerAdvice
public class MyExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorMessage handleValidationExceptions(
MethodArgumentNotValidException ex) {
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
if (!allErrors.isEmpty()) {
ObjectError error = allErrors.get(0);
String description = error.getDefaultMessage();
return new ErrorMessage("validation_failed", description);
}
return new ErrorMessage("validation_failed", "validation failed");
}
}
有了该异常处理类后,UserController 的代码即可以变得很纯净:
// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
...
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("")
public ResponseEntity<?> addUser(@RequestBody @Valid User user) {
// userService.addUser(user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
使用该种方式后,对于调用方来说,有校验错误时,效果与之前是一样的:
# 使用 CURL 命令新建一个 User(未提供 phone 参数)
curl -L \
-X POST \
-H "Content-Type: application/json" \
http://localhost:8080/users \
-d '{"name": "Larry", "age": 18, "email": "larry@qq.com"}'
// 会返回 400 状态码,以及如下错误信息
{ "code": "validation_failed", "description": "phone can not be null" }
学会如何以统一的异常处理类来处理校验错误后,下面看一下如何使用分组校验功能。
分组校验功能可以针对同一个 Model,为不同的场景应用不同的校验规则。
下面我们尝试使用同一个 User Model 来同时接收新增和更新的请求数据,但为各个字段指定不同的分组来区别新增和更新时校验规则的不同。
User Model 的代码如下:
// src/main/java/com/example/demo/model/User.java
package com.example.demo.model;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.validation.groups.Default;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
@Data
public class User {
@NotNull(message = "id can not be null", groups = Update.class)
private Long id;
@NotNull(message = "name can not be null", groups = Add.class)
@Size(min = 2, max = 20, message = "name length should be in the range [2, 20]")
private String name;
@NotNull(message = "age can not be null", groups = Add.class)
@Range(min = 18, max = 100, message = "age should be in the range [18, 100]")
private Integer age;
@NotNull(message = "email can not be null", groups = Add.class)
@Email(message = "email invalid")
private String email;
@NotNull(message = "phone can not be null", groups = Add.class)
@Pattern(regexp = "^1[3-9][0-9]{9}$", message = "phone number invalid")
private String phone;
public interface Add extends Default {
}
public interface Update extends Default {
}
}
可以看到,我们在 User Model 中定义了两个分组:Add 与 Update。每个字段上都有一个 @NotNull 注解,但 id 字段的分组是 Update.class,其它字段的分组是 Add.class,其余注解则未指定分组(表示均适用)。意思是要求:在新增时,name、age、email、phone 为必填字段;在更新时,id 为必填字段;而且不论新增还是更新,只要提供了对应的字段,就需要满足对应字段的校验规则。
下面看一下 UserController 的代码:
// src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
...
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("")
public ResponseEntity<?> addUser(@RequestBody @Validated(User.Add.class) User user) {
// userService.addUser(user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PatchMapping("")
public ResponseEntity<?> updateUser(@RequestBody @Validated(User.Update.class) User user) {
// userService.updateUser(user);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
可以看到,新增 User 接口与更新 User 接口使用了同一个 User Model;但新增使用的分组是 User.Add.class,更新使用的分组是 User.Update.class。
注意:这里指定分组时用到了 @Validated 注解,而前面用到的是 @Valid 注解,这里简单解释一下两者的不同。@Validated 注解是 Spring 框架自带的,而 @Valid 注解是 jakarta.validation 包下的,@Validated 注解可以指定分组,而 @Valid 注解则没有这个功能。
下面尝试在不提供 id 字段的情况下更新一下 User:
curl -L \
-X PATCH \
-H "Content-Type: application/json" \
http://localhost:8080/users \
-d '{"name": "Larry", "age": 18, "email": "larry@qq.com"}'
会返回如下错误:
// 400
{ "code": "validation_failed", "description": "id can not be null" }
介绍完分组校验功能的使用,下面看一下自定义校验器的使用。
如果 Validation 包中自带的注解未能满足您的校验需求,则可以自定义一个注解并实现对应的校验逻辑。
下面自定义了一个注解 CustomValidation,其代码如下:
package com.example.demo.validation;
...
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomValidator.class)
public @interface CustomValidation {
String message() default "Invalid value";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
如上代码中,@Target 指定了该注解的作用域,本例中,表示该注解可应用在方法或字段上;@Retention 指定该注解的存活期限,本例中,表示该注解在运行时可以使用;@Constraint 指定该注解的处理类。
处理类 CustomValidator 用于编写自定义校验逻辑,其代码如下:
package com.example.demo.validation;
...
public class CustomValidator
implements ConstraintValidator<CustomValidation, String> {
@Override
public void initialize(CustomValidation constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return null != value && value.startsWith("ABC");
}
}
可以看到,CustomValidator 实现了 ConstraintValidator<CustomValidation, String>,表示被标记字段是一个 String 类型;initialize() 方法用于校验器的初始化,可以根据需要访问注解上的各种属性;isValid() 方法可以拿到被校验的字段值,用于编写真正的校验逻辑。
下面即在 User Model 中使用一下这个自定义注解:
package com.example.demo.model;
...
@Data
public class User {
@CustomValidation(message = "testField invalid")
private String testField;
}
这样,当这个字段值不满足自定义校验规则时,就会抛出对应的错误:
// 400
{ "code": "validation_failed", "description": "testField invalid" }
综上,本文以示例代码的方式详细介绍了 spring-boot-starter-validation 包的使用。文中涉及的主要代码已提交至本人 GitHub,欢迎关注或 Fork。
参考资料
[1] Validating Form Input | Spring - spring.io
[2] Validations in Spring Boot | Medium - medium.com
[3] Spring Boot 项目参数校验(Validator)| CSDN 博客 - blog.csdn.net
[4] 在 Spring Boot 中使用 Spring Validation 对参数进行校验 | 稀土掘金 - juejin.cn
[5] Spring Boot 使用 Validation 校验参数 | 博客园 - www.cnblogs.com
[6] Difference between @Valid and @Validated in Spring | Stackoverflow - stackoverflow.com
正在加载评论......