Redian新闻
>
别再用 if 校验参数了,太Low!这才是专业的 SpringBoot 参数校验方式!

别再用 if 校验参数了,太Low!这才是专业的 SpringBoot 参数校验方式!

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:码农小胖哥

1. 前言

因为网络传输的不可靠性,以及前端数据控制的可篡改性,后端的参数校验是必须的,应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2. 数据校验的痛点

为了保证数据语义的正确,我们需要进行大量的判断来处理验证逻辑。而且项目的分层也会造成一些重复的校验,产生大量与业务无关的代码。不利于代码的维护,增加了开发人员的工作量。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. JSR 303 校验规范及其实现

为了解决上面的痛点,将验证逻辑与相应的领域模型进行绑定是十分有必要的。为此产生了JSR 303 – Bean Validation 规范。Hibernate Validator 是JSR-303 的参考实现,它提供了JSR 303 规范中所有的约束(constraint)的实现,同时也增加了一些扩展。

Hibernate Validator 提供的常用约束注解

约束注解详细信息
@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@AssertTrue被注释的元素必须为 true
@AssertFalse被注释的元素必须为 false
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期
@Future被注释的元素必须是一个将来的日期
@Pattern(value)被注释的元素必须符合指定的正则表达式
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串的必须非空
@Range被注释的元素必须在合适的范围内

4. 验证注解的使用

Spring Boot 开发中使用Hibernate Validator 是非常容易的,引入下面的starter 就可以了:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

一种可以实现接口来定制Validator,一种是使用约束注解。胖哥觉得注解可以满足绝大部分的需求,所以建议使用注解来进行数据校验。而且注解更加灵活,控制的粒度也更加细。接下来我们来学习如何使用注解进行数据校验。

4.1 约束注解的基本使用

我们对需要校验的方法入参进行注解约束标记,例子如下:

@Data
public class Student {

    @NotBlank(message = "姓名必须填")
    private String name;
    @NotNull(message = "年龄必须填写")
    @Range(min = 1,max =50, message = "年龄取值范围1-50")
    private Integer age;
    @NotEmpty(message = "成绩必填")
    private List<Double> scores;
}

POST 请求

然后定义一个POST 请求的Spring MVC 接口:

@RestController
@RequestMapping("/student")
public class StudentController {

    @PostMapping("/add")
    public Rest<?> addStudent(@Valid @RequestBody Student student) {
        return RestBody.okData(student);
    }
}

通过对addStudent方法入参添加@Valid来启用参数校验。当使用下面数据进行请求将会抛出MethodArgumentNotValidException异常,提示age范围超出1-50

POST /student/add HTTP/1.1
Host: localhost:8888
Content-Type: application/json

{
    "name""felord.cn",
    "age"77,
    "scores": [
        55
    ]
}

GET 请求

如法炮制,我们定义一个GET 请求的接口:

@GetMapping("/get")
public Rest<?> getStudent(@Valid Student student) {
    return RestBody.okData(student);
}

使用下面的请求可以正确对学生分数scores进行了校验,但是抛出的并不是MethodArgumentNotValidException异常,而是BindException异常。这和使用@RequestBody注解有关系,这对我们后面的统一处理非常十分重要。

GET /student/get?name=felord.cn&age=12 HTTP/1.1
Host: localhost:8888

自定义注解

可能有些同学注意到上面的年龄我进行了这样的标记:

@NotNull(message = "年龄必须填写")
@Range(min = 1,max =50, message = "年龄取值范围1-50")
private Integer age;

这是因为@Range不会去校验为空的情况,它只处理非空的时候是否符合范围约束。所以要用多个注解来约束。如果我们某些场景需要重复的捆绑多个注解来使用时,可以使用自定义注解将它们封装起来组合使用,下面这个注解就是将@NotNull@Range进行了组合,你可以仿一个出来用用看。

import org.hibernate.validator.constraints.Range;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.lang.annotation.*;

/**
 * @author a
 * @since 17:31
 **/

@Constraint(
        validatedBy = {}
)
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
        ElementType.PARAMETER, ElementType.TYPE_USE})
@NotNull
@Range(min = 1, max = 50)
@Documented
@ReportAsSingleViolation
public @interface Age {
    // message 必须有
    String message() default "年龄必须填写,且范围为 1-50 ";

    // 可选
    Class<?>[] groups() default {};

    // 可选
    Class<? extends Payload>[] payload() default {};
}

还有一种情况,我们在后台定义了枚举值来进行状态的流转,也是需要校验的,比如我们定义了颜色枚举:

public enum Colors {

    RED, YELLOW, BLUE

}

我们希望入参不能超出Colors的范围["RED", "YELLOW", "BLUE"],这就需要实现ConstraintValidator<A extends Annotation, T>接口来定义一个颜色约束了,其中泛型A为自定义的约束注解,泛型T为入参的类型,这里使用字符串,然后我们的实现如下:

/**
 * @author felord.cn
 * @since 17:57
 **/

public class ColorConstraintValidator implements ConstraintValidator<ColorString{
    private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();

    @Override
    public void initialize(Color constraintAnnotation) {
        Colors[] value = constraintAnnotation.value();
        List<String> list = Arrays.stream(value)
                .map(Enum::name)
                .collect(Collectors.toList());
        COLOR_CONSTRAINTS.addAll(list);

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return COLOR_CONSTRAINTS.contains(value);
    }
}

然后声明对应的约束注解Color,需要在元注解@Constraint中指明使用上面定义好的处理类ColorConstraintValidator进行校验。

/**
 * @author felord.cn
 * @since 17:55
 **/

@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target(
{ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
        ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {
    // 错误提示信息
    String message() default "颜色不符合规格";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    // 约束的类型
    Colors[] value();
}

然后我们来试一下,先对参数进行约束:

@Data
public class Param {
    @Color({Colors.BLUE,Colors.YELLOW})
   private String color;
}

接口跟上面几个一样,调用下面的接口将抛出BindException异常:

GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888

当我们把参数color赋值为BLUE或者YELLOW后,能够成功得到响应。

4.2 常见问题

在实际使用起来我们会遇到一些问题,这里总结了一些常见的问题和处理方式。

检验基础类型不生效的问题

上面为了校验颜色我们声明了一个Param对象来包装唯一的字符串参数color,为什么直接使用下面的方式定义呢?

@GetMapping("/color")
public Rest<?> color(@Valid @Color({Colors.BLUE,Colors.YELLOW}) String color) {
    return RestBody.okData(color);
}

或者使用路径变量:

@GetMapping("/rest/{color}")
public Rest<?> rest(@Valid @Color({Colors.BLUE, Colors.YELLOW}) @PathVariable String color) {
    return RestBody.okData(color);
}

上面两种方式是不会生效的 。不信你可以试一试,起码在Spring Boot 2.3.1.RELEASE 是不会直接生效的。

使以上两种生效的方法是在类上添加@Validated注解。注意一定要添加到方法所在的类上才行 。这时候会抛出ConstraintViolationException异常。

集合类型参数中的元素不生效的问题

就像下面的写法,方法的参数为集合时,如何检验元素的约束呢?

/**
 * 集合类型参数元素.
 *
 * @param student the student
 * @return the rest
 */

@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {
    return RestBody.okData(student);
}

同样是在类上添加@Validated注解。注意一定要添加到方法所在的类上才行 。这时候会抛出ConstraintViolationException异常。

嵌套校验不生效

嵌套的结构如何校验呢?打个比方,如果我们在学生类Student中添加了其所属的学校信息School并希望对School的属性进行校验。

@Data
public class Student {

    @NotBlank(message = "姓名必须填")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "成绩必填")
    private List<Double> scores;
    @NotNull(message = "学校不能为空")
    private School school;
}


@Data
public class School {
    @NotBlank(message = "学校名称不能为空")
    private String name;
    @Min(value = 0,message ="校龄大于0" )
    private Integer age;
}

GET 请求时正常校验了School的属性,但是POST 请求却无法对School的属性进行校验。这时我们只需要在该属性上加上@Valid注解即可。

@Data
public class Student {

    @NotBlank(message = "姓名必须填")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "成绩必填")
    private List<Double> scores;
    @Valid
    @NotNull(message = "学校不能为空")
    private School school;
}

每加一层嵌套都需要加一层@Valid注解。通常在校验对象属性时,@NotNull@NotEmpty@Valid配合才能起到校验效果。

5. 总结

通过校验框架我们可以专心于业务开发,本文对Hibernate Validator 的使用和一些常见问题进行了梳理。我们可以通过Spring Boot 统一异常处理来解决参数校验的异常信息的提示问题。

6. 项目源码地址

https://github.com/NotFound403/spring-boot-validation



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
这16个有用的 SpringBoot 扩展接口,居然还有人不知道?SpringBoot + Flyway,自动化实现数据库版本控制快速定位SpringBoot接口超时问题的神器!SpringBoot 官方推荐,连接池,太快了!从传统老店到街头小吃,这才是多伦多泰餐的正确打开方式!来了!围炉煮茶居家版,这才是冬日的正确打开方式!这才是前期分析的正确打开方式!SpringBoot+Prometheus+Grafana 实现自定义监控对话 Spring 大神:Spring 生态系统的新时代来了!出境游重启,如何免费游欧洲,这才是它的正确打开方式!这样做优化,实现 0.059s 启动一个SpringBoot项目![时尚] AJ14兵马俑开箱 CLOT x Air Jordan 14 Low SP "Terracotta"SpringBoot + MDC 实现全链路调用日志跟踪SpringBoot四大核心组件,你知道几个?这才是打开Tiffany & Co x Nike联名款 的正确方式!Spring Boot 3.0 要来了,真心强!SpringBoot + Prometheus + Grafana 打造可视化监控一条龙!SpringBoot 3.0正式发布,有这几个新变化!李大眼:我们都是神农的后裔强盗的逻辑搞车震,宝马是专业的畅游法国(26)-薰衣草故乡SpringBoot超大文件上传,实现秒传这才是打开孩子寒假的正确方式!学习+娱乐两不误,家有小学1年级到高2的孩子速入我在Krefeld (2)SpringBoot 接口数据加解密技巧,so easy!Spring for Apache Kafka 3.0 和 Spring for RabbitMQ 3.0 发布你的Lumion效果图,太LOW了!SpringBoot 整合 Groovy 脚本,实现动态编程SpringBoot+ElasticSearch 实现模糊查询,批量CRUD,排序,分页,高亮SpringBoot 接口加密解密,新姿势!K8s + SpringBoot实现零宕机发布中秋十五月,寒露十三夜性能爆表:SpringBoot利用ThreadPoolTaskExecutor批量插入百万级数据实测!这才是波士顿跨年晚餐的正确打开方式!
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。