秒懂SpringBoot之参数验证全解析(@Validated与@Valid)

2023-05-16

[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007

文章目录

  • 概述
  • 实例
  • SpringBoot 验证概述
    • 引入依赖
    • 使用相关注解标记
    • 使用@Valid标记
    • 统一处理异常
  • 高级用法
    • 复杂对象参数验证
    • 基本类型参数验证
    • Service方法参数验证
    • 手动验证
    • 分组验证
    • 自定义约束注解
    • JPA Entity验证
  • 概述
  • 源码
  • 题外话

概述

在构建 Web 应用程序时,确保进入应用程序的数据有效并满足您的业务需求非常重要。 实现此目的的一种方法是在服务器端验证输入数据。 在这篇博客中,我们将探讨如何在 Spring Boot 应用程序中进行输入数据验证,善用的话可以写出健壮而优美的代码。

让我们从一个实例开始吧

实例

邻家有女初长成,大名唤作牛翠华,家里催翠花找对象,无奈翠花深受互联网女拳师的影响,搞得翠花对另一半的要求非常高…

择偶标准

  • 非王思聪类型不嫁
  • 年龄大于30不嫁
  • 身高矮于185cm不嫁
  • 体重高于85kg不嫁
  • 没有大别墅不嫁
  • 没有大奔驰不嫁
  • 父母建在且没有城市养老金不嫁

假如我们要写一个产生符合其要求的男朋友的API,如何来写呢?

@Slf4j
@RestController
@RequestMapping("/validation")
public class ValidateController {

    @Autowired
    private ValidationService validationService;

    @PostMapping("/boy-friends")
    public ResponseEntity<BoyFriend> createBoyFriend(@RequestBody BoyFriend boy) {
        log.info("create:{}", boy);
        return ResponseEntity.ok(boy);
    }
 }

于是我们今天的主角就登场了。

SpringBoot 验证概述

Spring Boot 使用 Jakarta Bean Validation API 为输入数据验证提供内置支持,Java Bean Validation API 是用于验证 Java 对象的标准 API。 此 API 允许您使用注释定义 Java 类属性的约束,并根据这些约束验证输入数据。

目前一般使用2.0版本,由JSR 380提出。Java提出了这个标准,却没有给出实现,我们使用的都是Hibernate 的实现版本: Hibernate Validator .

引入依赖

要使用Hibernate Validator,所以要引入其依赖。SpringBoot2.3以后必须手动引入如下依赖(2.3以前在web的依赖包中包含了)。

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

使用相关注解标记

jakarta.validation-api模块中提供了很多添加约束的注解,包括

@NotNull @NotEmpty @NotBlank @Min @Max @Email @Size @Pattern  ...

使用时可以查阅源码,都很简单。

public class BoyFriend {
    @Max(30)
    private Integer age;
    ...
}

使用@Valid标记

使用@Valid 标记参数即可。

    @PostMapping("/boy-friends")
    public ResponseEntity<BoyFriend> createBoyFriend(@Valid @RequestBody BoyFriend boy) {
        log.info("create:{}", boy);
        return ResponseEntity.ok(boy);
    }

完成以上三步其实已经可以了,不过在验证失败时会抛出MethodArgumentNotValidException,对前端不友好,实际项目中我们都会对异常做统一处理,然后包装成统一的返回格式。

例如下面这样的格式:

{
    "code": 10000,
    "message": "错误消息",
    "data": xxx
}

统一处理异常

@RestControllerAdvice
public class ExceptionHandlers {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        String errMsg = e.getBindingResult().getFieldErrors().stream()
                .map(f -> f.getField() + ":" + f.getDefaultMessage())
                .collect(Collectors.joining("| "));
        return new BaseResponse<>(10000, MessageFormat.format("Invalid param: [{0}]", errMsg), null);
    }
    
}

高级用法

一般情况下,上面的方法已经可以应对,但是有些场景需要更多的努力

复杂对象参数验证

有时入参比较复杂,例如BoyFriend里面的house属性也是一个object,我们也需要对House这个对象属性进行验证。我们只需要在house属性申明处添加上@Valid注解即可。

public class BoyFriend {
    @Valid
    @NotNull
    private House house;
}

public class House {
    @AssertTrue
    private Boolean isVilla;
    
    @DecimalMin("100000000")
    private Integer price;
}

基本类型参数验证

有时我们的入参是基本类型,例如使用@RequestParam@PathVariable 标记的参数,这个怎么验证呢?例如下面的入参name。

@Validated
@RestController
@RequestMapping("/validation")
public class ValidateController {
      ...
    @GetMapping("/boy-friends")
    public ResponseEntity<BoyFriend> updateBoyFriend(@NotBlank @RequestParam("name") String name) {
        ...
    }
}

我们只需要给参数添加上相应的约束注解,例如@NotBlank,然后再给Controller类添加上@Validated即可。 @Validated是Spring提供的一个注解。

需要注意的是,这会抛出的是ConstraintViolationException异常,不是MethodArgumentNotValidException,所以需要将此异常也统一处理了,具体看文后源码。

Service方法参数验证

一般情况下我们都是在controller里就把参数验证做了,但是如果我们也想在Service里面的方法使用这套验证机制可以吗?答案是肯定的

  • 使用@Validated标记Service类
  • 使用@Valid注解标记方法入参,如果是基本类型的话只使用约束注解即可,与controller一样。
@Validated
@Service
public class ValidationService {
    public BoyFriend queryBoyFriendByName(@Size(min = 1,max = 3) String name) {
        return BoyFriend.builder().name(name).build();
    }
}

手动验证

有时我们需要手动触发验证而不依赖框架,这也是可以的。

  • 注入Validator的实例
  • 调用其validate方法并处理结果
@RestController
@RequestMapping("/validation")
public class ValidateController {
    @Autowired
    private Validator validator;

    @PatchMapping("/boy-friends")
    public ResponseEntity<BoyFriend> updateBoyFriend(@Valid  @RequestBody BoyFriend boy) {
            Set<ConstraintViolation<House>> validateResults = validator.validate(boy.getHouse());
            
            String errMsg = validateResults.stream().map(e -> e.getPropertyPath() + ":" + e.getMessage()).collect(Collectors.joining("| "));

            throw new BusinessException(errMsg,10002);
         ...
    }
 }

分组验证

有时一个类被多个方法使用,而每个方法对入参的要求却不一样,这种情况怎么办呢?例如我们的BoyFriend,里面有一个体重的属性,创建时要求不能高于85kg,由于条件苛刻,于是修改的时候要求不能高于100kg。

Spring提供了一种解决方法,那就是使用分组。每个注解里面都可以设置其属于哪些分组,在验证的时候只验证属于自己分组的那些约束。

例如我们这里设置两个分组:创建和更新,当调用创建方法的时候就只验证属于创建分组的约束,不高于85kg…

public class BoyFriend {
    @Max(groups = BoyFriendCreate.class, value = 85)
    @Max(groups = BoyFriendUpdate.class, value = 100)
    private Integer weight;
}

如何实现呢?

  • 定义分组

分组必须是接口,例如我们这里定义了两个分组

public interface BoyFriendCreate {
}

public interface BoyFriendUpdate {
}
  • 给约束添加相应的分组
public class BoyFriend {
    @Max(groups = BoyFriendCreate.class, value = 85)
    @Max(groups = BoyFriendUpdate.class, value = 100)
    private Integer weight;
}
  • 给Controller方法添加分组

先给Controller类添加@Validated,然后给方法添加带有分组信息的@Validated

    @Validated(BoyFriendUpdate.class)
    @PatchMapping("/boy-friends")
    public ResponseEntity<BoyFriend> updateBoyFriend(@Valid @RequestBody BoyFriend boy) {
        return ResponseEntity.ok(boy);
    }

注意,这种方法被认为是反模式的,因为其将代码耦合在了一起。本来创建和更新应该是两个不同的类,现在我们却将其耦合在了一起,通过一个分组的信息来区分

自定义约束注解

这是最后一招拉,每当框架提供的不能满足我们的需求时,我们就需要按照自己的需求自定义了,每个优秀的框架和类库都提供了这种能力

假设牛翠华对男朋友要求非常之高,要求必须是指定的某些人,例如王思聪,马化腾…,当然这个需求可以使用@Pattern然后写正则表达式来实现,不过我们这里为了演示就写一个自定义的约束注解来实现。

  • 定义一个约束注解

我们自定义一个注解@Target,前三个属性都是必须的,最后一个value是我们自己的,我们用它来保存目标的名称。

@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TargetManValidator.class)
public @interface TargetMan {
    String message() default "错误的人";

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

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

    String[] value() default {};
}

可以看到我们使用了@Constraint(validatedBy = TargetManValidator.class)来标记@TargerMan注解,所以我们还需要实现一个TargetManValidator

  • 实现一个TargetManValidator

具体的验证逻辑就是在这个类里面的。

public class TargetManValidator implements ConstraintValidator<TargetMan,String> {
    private List<String> values = new ArrayList<>();
    
    @Override
    public void initialize(TargetMan constraintAnnotation) {
       //从TargetMan 注解中获取用户设置的姓名列表
       values = Arrays.stream(constraintAnnotation.value()).collect(Collectors.toList());
    }

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

我们的验证逻辑就在isValid方法中,如果用户输入的姓名不在设置的姓名集合之中就返回FALSE。

  • 使用
public class BoyFriend {
    @NotBlank
    @TargetMan({"马化腾", "王思聪", "王二狗"})
    private String name;
}

JPA Entity验证

你自己加几个约束注解试试…

概述

今天就到这啦,希望小朋友们都学头秃了… 对了,最后牛翠华嫁给了王二狗,至少都姓王,O(∩_∩)O哈哈~ 。

我在此为牛翠华正名:说翠花打拳只是为行文方便,翠花是一个温婉贤良,上孝下敬的好女子…

源码

一如既往,你可以在首发找到本文源码:秒懂SpringBoot参数验证全解析(@Validated与@Valid),小星星点一点,需要的时候方便找的到。

参考文章:Validation with Spring Boot - the Complete Guide

题外话

最近小区物业群里警察叔叔经常发送辖区被电信诈骗的案例。我从头到尾只有两个感觉,一个是这些人怎么这么有钱?二是这些人怎么这么傻?基本上老人妇女死于刷单理财,壮男死于约炮…。我觉得要是骗我估计有点困难,不是我聪明主要人到中年一没钱,而没啥兴趣,骗子都绕着走啊,o( ̄︶ ̄)o…

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

秒懂SpringBoot之参数验证全解析(@Validated与@Valid) 的相关文章

随机推荐

  • windows查看默认编码类型

    xfeff xfeff 开始 cmd 运行chcp 你会得到一个数 例 xff1a 如936 xff0c 那就是GBK简体中文 ANSI代码页为1252 xff0c 日文代码页为932
  • 三、@PathVariable

    3 1 64 PathVariable 映射 URL 绑定的占位符 带占位符的 URL 是 Spring3 0 新增的功能 xff0c 该功能在SpringMVC 向 REST 目标挺进发展过程中具有里程碑的意义通过 64 PathVari
  • 走进CSDN

    走进CSDN 关注CSDN不久 xff0c 最近浏览的次数增多 xff0c CSDN的资讯刚开始基本上看的懂的不多 xff0c 专业名词扎堆的论坛 xff0c CSDN的氛围个人感觉挺好的 xff0c 一群俗称 程序员 的人聚集讨论问题 x
  • B端产品经理基本工作流程

    产品岗位必备素质 产品是一个门槛较低的岗位 xff0c 是一个看起来很容易 xff0c 做起来各个地方都是bug的岗位 产品需要更多的是软实力 xff0c 把握产品的方向 xff0c 目标用户是谁 xff0c 场景是什么 xff0c 达到怎
  • Mac实用的远程ssh连接工具( Royal TSX安装及使用)

    Mac实用的远程ssh连接工具 Royal TSX安装及使用 1 下载地址 https www royalapps com ts mac download 2 如何连接远程服务器 2 1 首先下载插件Terminal 2 2 然后创建新的D
  • 尝试VC控制外部程序

    这两天尝试VC控制外部程序呢 xff0c 慢慢完善 在参考了网络学习以后 xff0c 简单做了以下工作 xff1a 期间用了spy 43 43 器件 void CVCControlDlg OnStartreader 启动朗读女 TODO A
  • Windows Sever 2012 R2设置组策略对“不显示最后的登录名”选项已启用

    Windows Sever 2012 R2设置组策略对 不显示最后的登录名 选项已启用 作者 xff1a 我道梦 关注我的CSDN博客 xff0c 更多笔记知识还在更新 xff01 设置组策略启用 不显示最后的登录名 后 xff0c 系统将
  • Ubuntu22.04.1 & WIN11 双系统+双硬盘 grub启动项中无WIN11开机引导

    本机UEFI 43 GPT安装的双系统 xff0c 两块固态硬盘 xff0c 两个系统各自使用自己的硬盘分区 xff0c xff08 选择的全盘安装在新硬盘 xff0c 没有自定义分区 xff0c 所以安装的时候也没有提示与当前window
  • tightvnc,tightvnc软件介绍,详细介绍

    tightvnc一款用于windows操作系统的应用软件 xff0c 是一款远程控制软件 出门在外忘了带档案怎么办 xff1f FTP server 上头忘了开帐号怎么办 xff1f 这些麻烦的问题其实都可以靠 VNC 解决 tightvn
  • OpenCore-EFI-配置模版(持续更新)

    前言 随着OpenCore日臻完善 xff0c 将在以后会更多的用于黑苹果的安装 同时 xff0c 在各位大佬的大力支持与推广 xff0c 各种入门 xff0c 进阶教程的推出 xff0c OpenCore已经从神界降临到人间 逐渐为普通黑
  • OpenCore(OC)引导开机声音与图形界面设置

    关键字 xff1a OC xff1b OpenCore xff1b 引导 xff1b 开机声音 xff1b 图形界面 下面的设置基于OpenCore0 5 8 04 10编译版与1 22 0 0版OpenCore Configurator
  • The BMJ研究:现有的新冠病毒诊断AI模型,几乎毫无用处

    图片出处 xff1a unsplash 本文作者 xff1a 朱演瑞 新型冠状病毒对全球健康造成了严重的威胁 xff0c 为了减轻医疗保健系统的负担 xff0c 也给患者提供最佳的护理 xff0c 高效的诊断和疾病预后信息问题亟待解决 理论
  • 06-Docker-Centos 7.2 (Vmware最小化安装)之一篇搞定hyperledger/fabric的e2e_cli测试运行所遇到的ERROR总结

    bug产生原因分析如下 xff1a 1 系统过于单纯或复杂 xff08 即最小化安装与全部安装以及自行安装了很多软件 xff09 xff0c 很多命令和工具无法使用和执行或冲突 2 自己操作失误 xff0c 敲错代码 xff08 关键词和语
  • 秒懂Java之方法引用(method reference)详解

    版权申明 非商业目的注明出处可自由转载 出自 xff1a shusheng007 相关文章 xff1a 秒懂Java之深入理解Lambda表达式 文章目录 概述使用条件使用场景如何使用方法引用的类型调用类的静态方法调用传入的实例参数的方法调
  • 产品设计中关于思考力那些事

    这周的面试 xff0c 对我自己来说 xff0c 更像是一种迭代反思 从做什么怎么做 xff0c 到为什么做 xff0c 的一种强制思考 一方面是入行时间短 xff0c 另一方面是公司产品业务主导 xff0c 相对不需要产品去思考 xff0
  • 永不磨灭的设计模式(有这一篇真够了,拒绝标题党)

    版权申明 非商业目的注明出处可自由转载 出自 xff1a shusheng007 文章目录 概述定义分类创建型 xff08 creational xff09 结构型 xff08 structural xff09 行为型 xff08 beha
  • shusheng007编程手记

    版权申明 非商业目的注明出处可自由转载 出自 xff1a shusheng007 文章目录 概述工具篇IntelliJ IDEA在Idea中下载源码时 xff0c 报无法下载源码 Postman使用Postman发送Post请求服务端报得到
  • SpringBoot如何整合RabbitMQ

    版权申明 非商业目的注明出处可自由转载 出自 xff1a shusheng007 文章目录 概述rabbitmq简介SpringBoot整合安装rabbitmq初级用法高级用法配置交换器与队列发送消息消费消息测试 总结 概述 好久没有写博客
  • 秒懂SpringBoot之@Async如何自定义线程池

    版权申明 非商业目的注明出处可自由转载 出自 xff1a shusheng007 文章目录 概述异步初探线程池ThreadPoolExecutorThreadPoolTaskExecutor 验证线程池配置拒绝策略为AbortPolicy拒
  • 秒懂SpringBoot之参数验证全解析(@Validated与@Valid)

    版权申明 非商业目的注明出处可自由转载 出自 xff1a shusheng007 文章目录 概述实例SpringBoot 验证概述引入依赖使用相关注解标记使用 64 Valid标记统一处理异常 高级用法复杂对象参数验证基本类型参数验证Ser