欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Boot 参数校验

程序员文章站 2022-07-02 14:28:07
1、背景介绍 开发过程中,后台的参数校验是必不可少的,所以经常会看到类似下面这样的代码 这样写并没有什么错,还挺工整的,只是看起来不是很优雅而已。 接下来,用Validation来改写这段 2、Spring Boot文档中的Validation 在Spring Boot的官网中,关于Validati ......

前言

作为服务端开发,验证前端传入的参数的合法性是一个必不可少的步骤,但是验证参数基本上是一个体力活,而且冗余代码繁多,也影响代码的可阅读性,所以有没有一个比较优雅的方式来解决这个问题?
JSR-303验证框架,JSR-303 是Java EE 6 中的一项子规范,叫做Bean Validation,官方参考实现是Hibernate Validator(与Hibernate ORM 没有关系),JSR 303 用于对Java Bean 中的字段的值进行验证,确保输入进来的数据在语义上是正确的,使验证逻辑从业务代码中脱离出来。JSR303是运行时数据验证框架,验证之后验证的错误信息会马上返回。

依赖

由于 spring-boot-starter-web 中提供的参数校验注解较少(如下图所示)
Spring Boot 参数校验
所以可以引入 spring-boot-starter-validation 来丰富项目中可以引用的参数校验注解
Spring Boot 参数校验

校验组

校验组可以对需要校验的字段进行分类,即为每个字段提供不同的校验规则。校验组只需定义简单的接口即可。本文案例中我们定义两个校验组 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;

}

其中,WriteActionReadAction 是之前自定义的分组标志接口。
根据校验注解名称,可以很清晰地了解这些注解的作用(比如 @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(针对分组属性为 ReadActionWriteAction

    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 之外,其他含有 ReadActionWriteAction 的属性都要带上;而对于 copy 接口,则只需要关注没有 groups 属性的 age 字段。

另外,在 PersonController 中有一个 id 方法是直接在接口入参处使用 @Length 注解做参数校验的,但要使该注解生效,需要在 PersonController 上加上 @Validated 注解。

参考博客

SpringBoot中的参数校验

本文地址:https://blog.csdn.net/qq_15329947/article/details/110877644