Spring Boot 参数校验
前言
作为服务端开发,验证前端传入的参数的合法性是一个必不可少的步骤,但是验证参数基本上是一个体力活,而且冗余代码繁多,也影响代码的可阅读性,所以有没有一个比较优雅的方式来解决这个问题?
JSR-303验证框架,JSR-303 是Java EE 6 中的一项子规范,叫做Bean Validation,官方参考实现是Hibernate Validator(与Hibernate ORM 没有关系),JSR 303 用于对Java Bean 中的字段的值进行验证,确保输入进来的数据在语义上是正确的,使验证逻辑从业务代码中脱离出来。JSR303是运行时数据验证框架,验证之后验证的错误信息会马上返回。
依赖
由于 spring-boot-starter-web 中提供的参数校验注解较少(如下图所示)
所以可以引入 spring-boot-starter-validation 来丰富项目中可以引用的参数校验注解
校验组
校验组可以对需要校验的字段进行分类,即为每个字段提供不同的校验规则。校验组只需定义简单的接口即可。本文案例中我们定义两个校验组 ReadAction 和 WriteAction,分别对应读和写两种情况下的字段的校验规则。
public interface ReadAction {
}
public interface WriteAction {
}
举例说明其使用方式,比如一个字段 id,只需要在写操作时前端传参不为 null,那么可以在 id 上加注解:
@NotNull(groups = {WriteAction.class})
private String id;
如果需要在两种情况下都不为 null,那么:
@NotNull(groups = {ReadWriteAction.class, WriteAction.class})
private String id;
如果需要在读时不为空,写时不为 null,那么可以加上以下两个注解,对应不同的组:
@NotBlank(groups = {ReadWriteAction.class})
@NotNull(groups = {WriteAction.class})
private String id;
需要注意的是只有在 Bean 参数前使用 @Validated(value = {Group.class...})
且必须在 value 中写明 groups 对应的类对象时 groups 属性才会生效。
若是以下这种没有 groups 属性的形式,那么需要使用 @Valid
注解才能使之生效
@NotBlank
@NotNull
private String id;
实体类
首先定义一个实体类 PersonReq
package com.jake.spring.boot.validation.model;
import com.jake.spring.boot.validation.group.ReadAction;
import com.jake.spring.boot.validation.group.WriteAction;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PositiveOrZero;
@Data
public class PersonReq {
@Valid
@NotNull(groups = {WriteAction.class, ReadAction.class})
private IdCardInfo idCardInfo;
@PositiveOrZero
@NotNull
private Integer age;
@Email(groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String email;
@Length(min = 11, max = 11, groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String phone;
}
其中,WriteAction
和 ReadAction
是之前自定义的分组标志接口。
根据校验注解名称,可以很清晰地了解这些注解的作用(比如 @PositiveOrZero
表示年龄需要是一个大于等于 0 的数字),此处不详细叙述。
而 @Valid
能够进行嵌套校验,即对于 Bean 中的另一个 Bean 属性做参数校验。
接着,定义其中的 Bean 属性 IdCardInfo
package com.jake.spring.boot.validation.model;
import com.jake.spring.boot.validation.group.ReadAction;
import com.jake.spring.boot.validation.group.WriteAction;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import java.time.LocalDate;
@Data
public class IdCardInfo {
@Length(min = 18, max = 18, groups = {WriteAction.class})
@NotNull(groups = {WriteAction.class})
private String id;
@NotBlank(groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String name;
@NotBlank(groups = {ReadAction.class})
@NotNull(groups = {ReadAction.class})
private String address;
@Past(groups = {ReadAction.class})
@DateTimeFormat(pattern = "YYYY-MM-dd")
@NotNull(groups = {ReadAction.class})
private LocalDate birthday;
@Future(groups = {ReadAction.class})
@DateTimeFormat(pattern = "YYYY-MM-dd")
@NotNull(groups = {ReadAction.class})
private LocalDate expiration;
}
控制层
package com.jake.spring.boot.validation.controller;
import com.jake.spring.boot.validation.group.ReadAction;
import com.jake.spring.boot.validation.group.WriteAction;
import com.jake.spring.boot.validation.model.PersonReq;
import com.jake.spring.boot.validation.model.PersonVO;
import org.hibernate.validator.constraints.Length;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("persons")
@Validated
public class PersonController {
@PostMapping("read")
public PersonVO read(@RequestBody @Validated(value = {ReadAction.class}) PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@PostMapping("write")
public PersonVO write(@RequestBody @Validated(value = {WriteAction.class}) PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@PostMapping("readOrWrite")
public PersonVO readOrWrite(@RequestBody @Validated(value = {ReadAction.class, WriteAction.class}) PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@PostMapping("copy")
public PersonVO copy(@RequestBody @Valid PersonReq personReq) {
PersonVO personVO = new PersonVO();
BeanUtils.copyProperties(personReq, personVO);
return personVO;
}
@GetMapping("{idNo}")
public String id(@Length(min = 18, max = 18) @PathVariable(name = "idNo") String id) {
return id;
}
}
注意,对于有 groups 属性定义的字段,必须使用 @Validated(value = {Group.class...})
其校验注解才能生效;对于没有 groups 属性定义的字段,则必须使用 @Valid
。
对于各个接口,其对应的调用参数如下:
-
read(针对分组属性为
ReadAction
)curl --location --request POST 'localhost:8080/persons/read' \ --header 'Content-Type: application/json' \ --data-raw '{ "idCardInfo": { "name": "wzk", "address": "sz", "birthday": "1992-06-01", "expiration": "2027-07-07" }, "age": 28, "email": "15118126432@163.com", "phone": "15118126432" }'
-
write(针对分组属性为
WriteAction
)curl --location --request POST 'localhost:8080/persons/write' \ --header 'Content-Type: application/json' \ --data-raw '{ "idCardInfo": { "id": "44528119920601005X" } }'
-
readOrWrite(针对分组属性为
ReadAction
或WriteAction
)curl --location --request POST 'localhost:8080/persons/readOrWrite' \ --header 'Content-Type: application/json' \ --data-raw '{ "idCardInfo": { "id": "44528119920601005X", "name": "wzk", "address": "sz", "birthday": "1992-06-01", "expiration": "2027-07-07" }, "email": "15118126432@163.com", "phone": "15118126432" }'
-
copy(针对无分组属性的情况,在本文代码案例中仅针对
PersonReq
的 age 属性)curl --location --request POST 'localhost:8080/persons/copy' \ --header 'Content-Type: application/json' \ --data-raw '{ "age": 28 }'
以上四组接口调用脚本均不会出现由参数格式错误引起的 400 Bad Request 错误,而且是请求参数最少化的形式。
例如,在调 read 接口时,无需填入 id,因为属性 id 的注解的分组属性中没有 ReadAction
;对于 write 接口同理,只需填入字段 idCardInfo 及id,而其他属性均没有被 WriteAction
定义;在调 readOrWrite 时,则除了没有定义 groups 属性的 age 之外,其他含有 ReadAction
和 WriteAction
的属性都要带上;而对于 copy 接口,则只需要关注没有 groups 属性的 age 字段。
另外,在 PersonController
中有一个 id
方法是直接在接口入参处使用 @Length
注解做参数校验的,但要使该注解生效,需要在 PersonController
上加上 @Validated
注解。
参考博客
本文地址:https://blog.csdn.net/qq_15329947/article/details/110877644
推荐阅读
-
详解Spring Boot中Controller用法
-
spring MVC中接口参数解析的过程详解
-
Spring Boot下的Job定时任务
-
使用Spring Boot快速构建基于SQLite数据源的应用
-
Spring Boot中使用 Spring Security 构建权限系统的示例代码
-
Spring-boot原理及spring-boot-starter实例和代码
-
spring boot 监控处理方案实例详解
-
详解Spring boot使用Redis集群替换mybatis二级缓存
-
详解spring boot集成ehcache 2.x 用于hibernate二级缓存
-
使用 Spring Boot 实现 WebSocket实时通信