SpringBoot使用Validation包进行输入参数校验
作者:磊磊落落
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 包进行使用了。
1 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]
。email
其为字符串类型,使用了
@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 包中常用注解的使用方式,下面看一下校验错误的异常捕获与展示。
2 校验错误的异常捕获与展示
我们注意到,上面的例子中 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" }
学会如何以统一的异常处理类来处理校验错误后,下面看一下如何使用分组校验功能。
3 分组校验功能的使用
分组校验功能可以针对同一个 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" }
介绍完分组校验功能的使用,下面看一下自定义校验器的使用。
4 自定义校验器的使用
如果 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
包的使用。
到此这篇关于SpringBoot使用Validation包进行输入参数校验的文章就介绍到这了,更多相关SpringBoot Validation参数校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!