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;

}

其中,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中的参数校验

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值