【SpringBoot系列】如何优雅地校验客户端参数

358 阅读4分钟

前言

在日常开发中,我们少不了需要对前端的请求参数的验证。Spring提供了多种方法来实现请求参数的验证。

设置依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.22</version>
</dependency>

注解说明

1.Bean Validation 中内置的 constraint

注解含义
@Null验证对象必须为 null
@NotNull验证对象不为 null
@NotBlank验证字符串不能为空null或"",只能用于字符串验证
@NotEmpty验证对象不得为空,可用于Map和数组
@Pattern验证字符串是否满足正则表达式
@AssertTrue验证 boolean 类型值为 true
@AssertFalse验证 boolean 类型值为 false
@Min(value)验证数字的大小是否大于等于指定的值
@Max(value)验证数字的大小是否小于等于指定的值
@DecimalMin(value)验证数字的大小是否大于等于指定的值,小数存在精度
@DecimalMax(value)验证数字的大小是否小于等于指定的值,小数存在精度
@Size(max, min)验证对象(字符串、集合、数组)长度是否在指定范围之内
@Digits(integer, fraction)验证数字是否符合指定格式
@Pattern(value)验证字符串是否符合正则表达式的规则
@ParameterScriptAssert可以在方法参数级别上执行脚本验证,以验证传递给方法的参数是否符合指定的条件。该注解可以与 JSR 223 兼容的脚本引擎一起使用,例如 JavaScript、Groovy、Python 等。- script:脚本表达式,用于执行参数验证。该表达式应该返回一个 boolean 类型的值,表示验证是否通过。- lang:脚本语言的名称,默认为 "groovy"。
@Email验证字符串是否符合电子邮件地址的格式。
@Future验证一个日期或时间是否在当前时间之后。
@FutureOrPresent验证一个日期或时间是否在当前时间之后或等于当前时间。
@past验证一个日期或时间是否在当前时间之前。
@PastOrPresent验证一个日期或时间是否在当前时间之前或等于当前时间。
@Positive验证数字是否是正整数,0无效
@PositiveOrZero验证数字是否是正整数
@Negative验证数字是否是负整数,0无效
@NegativeOrZero验证数字是否是负整数

2.Hibernate Validator 附加的 constraint

注解作用
@Email被注释的元素必须是电子邮箱地址
@Length(min=, max=)被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串或集合不能为null或长度为0
@Range(min=, max=)被注释的数值元素必须在合适的范围内(包括指定值)
@NotBlank被注释的字符串不能为null和空格字符串
@URL(protocol=,host=, port=,regexp=, flags=)被注释的字符串必须是一个有效的url

统一异常处理

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
​
import java.util.Objects;
​
/**
 * 統一异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
​
    @ExceptionHandler(value = Exception.class)
    public String defaultErrorHandler(HttpServletRequest request, Exception exception) throws Exception {
        log.error(exception.getMessage(), exception);
        String result="";
        if (exception instanceof BindException){
            result = "参数错误 - " + Objects.requireNonNull(((BindException) exception).getFieldError()).getDefaultMessage();
        }else {
            result = "服务器异常";
        }
        return result;
    }
}

定义Controller

import com.example.validationdemo.model.VerificationLoginDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
@RequestMapping("/api/user")
@RestController
@Slf4j
public class DemoController {
​
    @PostMapping(value = "/loginByCode")
    public String loginByCode(@RequestBody(required = false) @Validated VerificationLoginDto dto) {
        System.out.println("dto:"+dto);
        return "ok";
    }
}

注意:这里必须要加 @Validated

测试效果

20230430002133.png

在我们的实际应用场景中,需要验证参数分组,比如我们新增和更新数据的操作一般都用同一个对象来接收前端传入参数。那么新增时的Id可能为空,而编辑时的Id不能为空,我们对参数的验证规则是不一样的。那么怎么实现呢?这里就需要用到我们的groups()了。下面给出一个示例。

定义2个组

public interface UserCreateGroup {
}
public interface UserUpdateGroup {
}

定义对象

import jakarta.validation.constraints.NotNull;
​
public class User {
    @NotNull(message = "id不能为空", groups = {UserUpdateGroup.class})
    private String id;
​
    @NotNull(message = "用户名不能为空", groups = {UserCreateGroup.class, UserUpdateGroup.class})
    private String username;
}

定义Controller

import com.example.validationdemo.model.User;
import com.example.validationdemo.model.UserCreateGroup;
import com.example.validationdemo.model.UserUpdateGroup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
@RequestMapping("/api/user")
@RestController
@Slf4j
public class DemoUserController {
​
    @PostMapping(value = "/create")
    public String createUser(@RequestBody @Validated(UserCreateGroup.class) User user) {
        System.out.println("user:"+user);
        return "ok";
    }
​
    @PostMapping(value = "/update")
    public String updateUser(@RequestBody @Validated(UserUpdateGroup.class) User user) {
        System.out.println("user:"+user);
        return "ok";
    }
}

根据需求,我们在 User 的 id 属性上标记上 groups = {UserUpdateGroup.class},在 username 属性上标记 groups = {UserCreateGroup.class, UserUpdateGroup.class}。则表明我们对id只在 @Validated(UserCreateGroup.class) 时验证id的值;username 则在 @Validated(UserUpdateGroup.class)或者 @Validated(Create.class) 时都需要验证。

源码获取

微信搜索"架构殿堂"公众号,回复 "SpringBoot接口校验参数" 或者 下载地址

写在最后:

如果大家对相关技术感兴趣,可以微信搜索"架构殿堂"公众号或者点击下方公众号卡片关注公众号,会持续更新分享AGIC,java基础面试题, netty, spring boot,spring cloud,系列文章,干货满满,赶紧关注吧