「SpEL Validator」使用指南(一套无敌的参数校验组件)

news2024/11/15 17:29:11

前言

这是一套全新的参数校验组件,并非造轮子。

看完本文你可能会觉得用不上或不屑于使用,但这玩意确实有应用场景,你不妨稍微留意一下,日后你总会发现有用得上的时候。

此乃系列文章,当前为第②篇,其他文章地址:

①我写了一套无敌的参数校验组件:https://blog.csdn.net/little_stick_i/article/details/138540678

简介

SpEL Validator 是一个强大的 Java 参数校验包,基于 SpEL 实现,扩展自 javax.validation 包,用于简化参数校验,几乎支持所有场景下的参数校验。

GitHub地址:https://github.com/stick-i/spel-validator

本组件的目的不是代替 javax.validation 的校验注解,而是作为一个扩展,方便某些场景下的参数校验。

添加依赖

当前版本:0.2.0-beta。最新版本建议到GitHub查看。

大多数情况下,你只需要添加以下两个依赖:

<dependencys>
  <dependency>
    <groupId>cn.sticki</groupId>
    <artifactId>spel-validator</artifactId>
    <version>Latest Version</version>
  </dependency>

  <!-- 基于 javax.validation 标准的校验器实现包 -->
  <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.5.Final</version>
  </dependency>
</dependencys>

如果你的项目中没有任何对于Spring的依赖,那么你需要额外添加一个对SpEL的依赖:

<dependencys>
  <!-- SpEL 依赖项 -->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>${spring.version}</version>
  </dependency>
</dependencys>

开启约束校验

需要满足以下两个条件,才会对带注解的元素进行校验:

  1. 在接口参数上使用 @Valid@Validated 注解
  2. 在实体类上使用 @SpelValid 注解
@RestController
@RequestMapping("/example")
public class ExampleController {

  @PostMapping("/simple")
  public Resp<Void> simple(@RequestBody @Valid /*添加启动注解*/ SimpleExampleParamVo simpleExampleParamVo) {
    return Resp.ok(null);
  }

}

@Data
@SpelValid /*添加启动注解*/
public class SimpleExampleParamVo {
  // ...
}

如果只满足第一个条件,那么只会对带 @NotNull@NotEmpty@NotBlank 等注解的元素进行校验。

如果只满足第二个条件,那么不会对任何元素进行校验。

这是因为 @SpelValid 注解是基于 javax.validation.Constraint 实现的。
这就意味着,@SpelValid@NotNull@NotEmpty@NotBlank 等注解一样,
需要在 @Valid@Validated 注解的支持下才会生效。

SpEL Validator 提供的约束注解又是在 @SpelValid 的内部进行校验的,只有在 @SpelValid 注解生效的情况下才会执行约束校验。

所以,如果需要使用 SpEL Validator 进行校验,需要同时满足上述两个条件。

设置开启条件

@SpelValid 注解包含一个属性 condition,支持 SpEL 表达式,计算结果必须为 boolean 类型。

表达式的算结果为true 时,表示开启校验,默认情况下是开启的。

@Data
@SpelValid(condition = "1 > 0") /*设置开启校验的条件*/
public class SimpleExampleParamVo {
  // ...
}

这里的 condition 字段同样支持上下文引用,具体使用方式参考 引用上下文字段。

使用约束注解

目前支持的约束注解有:

注解说明对标 javax.validation
@SpelAssert逻辑断言校验@AssertTrue
@SpelNotNull非 null 校验@NotNull
@SpelNotEmpty集合、字符串、数组大小非空校验@NotEmpty
@SpelNotBlank字符串非空串校验@NotBlank
@SpelNull必须为 null 校验@Null
@SpelSize集合、字符串、数组长度校验@Size
@SpelMin即将支持@Min
@SpelMax即将支持@Max

所有约束注解都包含三个默认的属性:

  • condition:约束开启条件,支持 SpEL 表达式,表达式的计算结果必须为 boolean 类型,当 计算结果为true 时,才会对带注解的元素进行校验,默认情况下开启。
  • message:校验失败时的提示信息。
  • group:分组条件,支持 SpEL 表达式,当分组条件满足时,才会对带注解的元素进行校验。具体使用方式参考 分组校验。

在需要校验的字段上使用 @SpelNotNull 等约束注解。

@Data
@SpelValid
public class SimpleExampleParamVo {

  /**
   * 此处使用了 @SpelNotNull 注解
   * 当参数 condition 的计算结果 true 时,会启用对当前字段的约束,要求为当前字段不能为null
   * 约束校验失败时,提示信息为:语音内容不能为空
   */
  @SpelNotNull(condition = "true", message = "语音内容不能为空")
  private Object audioContent;

}

引用上下文字段

设计这套组件的初衷,就是为了满足一些需要判断另一个字段的值来决定当前字段是否校验的场景。

在组件内部,将当前校验的整个类对象作为了 SpEL 表达式解析过程中的根对象,所以在表达式中可以直接引用类中的任意字段。

通过 #this.fieldName 的方式来引用当前类对象的字段。

@Data
@SpelValid
public class SimpleExampleParamVo {

  private boolean switchAudio;

  /**
   * 此处引用了上面的 switchAudio 字段
   * 当 switchAudio 字段的值为 true 时,才会校验 audioContent 是否为null
   */
  @SpelNotNull(condition = "#this.switchAudio == true")
  private Object audioContent;

}

分组校验

启动注解 @SpelValid 上包含一个属性 spelGroups,类型为字符串数组,支持 SpEL 表达式。

每一个约束注解上也都包含一个属性 group,类型为字符串数组,支持 SpEL 表达式。

为什么 @SpelValid 注解上的 spelGroups 属性不叫 groups?

因为 @SpelValid 注解是基于 javax.validation.Constraint 实现的,而 Constraint 中已经有一个 groups 属性了,故命名为 spelGroups

在使用 @SpelValid 的时候,你可以同时使用 groupsspelGroups 属性,但是 groups 属性只能用于 javax.validation 的分组校验。
@SpelValid@NotNull@NotEmpty 等注解是兄弟关系,它的 groups 属性同样受上层 @Valid@Validated 注解的影响。

默认情况下,@SpelValid.spelGroups 为空,表示不进行分组校验,此时所有的约束注解都会生效。

@SpelValid.spelGroups 不为空时,表示开启分组校验,此时:

  • 约束注解中的 group 属性为空时,该约束注解生效。
  • 约束注解中的 group 属性不为空时,只有当 @SpelValid.spelGroups 中的分组信息与此处的分组信息有交集时,才会对带注解的元素进行校验。

这里表达式的计算结果可以是任何类型,但只有两个计算结果满足 o.equals(e) 时,才被认为是相等的。

使用示例:

@Data
@SpelValid(spelGroups = "#this.type")
public class GroupExampleParamVo {

  @NotNull
  @Pattern(regexp = "^text|audio$")
  private String type;

  /**
   * 当 type 字段的值为 text 时,才会对此字段进行校验
   */
  @SpelNotNull(group = "'text'")
  private Object textContent;

  /**
   * 当 type 字段的值为 audio 时,才会对此字段进行校验
   */
  @SpelNotNull(group = "'audio'")
  private Object audioContent;

  /**
   * 未指定分组,默认被校验
   */
  @SpelNotNull
  private Integer other;

}

嵌套校验

本组件支持嵌套校验,在需要校验的字段上添加 @Valid,以及在另一个类上添加 @SpelValid 注解。

@Data
@SpelValid
public class TestParamVo {

  private Boolean switchVoice;

  @SpelNotNull(condition = "#this.switchVoice == true")
  private Object voiceContent;

  @Valid
  private TestParamVo2 testParamVo2;

}

@Data
@SpelValid /*在此处添加注解*/
public class TestParamVo2 {

  @SpelNotNull
  private Object object;

}

或者将 @SpelValid 注解转移到对应的字段上。

@Data
@SpelValid
public class TestParamVo {

  private Boolean switchVoice;

  @SpelNotNull(condition = "#this.switchVoice == true")
  private Object voiceContent;

  @Valid
  @SpelValid /*在此处添加注解*/
  private TestParamVo2 testParamVo2;

}

@Data
public class TestParamVo2 {

  @SpelNotNull
  private Object object;

}

处理约束异常

当校验失败时,本组件会将异常信息上报到 javax.validation 的异常体系中。

正常情况下,你只需要处理 org.springframework.web.bind.MethodArgumentNotValidException
org.springframework.validation.BindException 这两个校验异常类就好了 ,而无需额外处理本组件的异常信息。

事实上,MethodArgumentNotValidException 继承自 BindException,只需要处理 BindException 就可以了。

@RestControllerAdvice
public class ControllerExceptionAdvice {

  @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
  public Resp<Void> handleBindException(BindException ex) {
    String msg = ex.getFieldErrors().stream()
        .map(error -> error.getField() + " " + error.getDefaultMessage())
        .reduce((s1, s2) -> s1 + "," + s2)
        .orElse("");
    return new Resp<>(400, msg);
  }

}

处理业务异常

由于本组件支持 调用静态方法调用Spring Bean方法,故在校验过程中可能会抛出除约束异常以外的其他业务异常。

举一个不太恰当的例子

以下是一个枚举类,它包含了一个静态方法,用于根据code获取枚举值,如果获取不到则抛出业务异常:

@Getter
public enum ExampleEnum {

  XXX(1);

  private final Integer code;

  ExampleEnum(Integer code) {
    this.code = code;
  }

  /**
   * 通过code获取枚举值,如果code不存在则抛出业务异常
   */
  public static ExampleEnum getByCode(Integer code) {
    for (ExampleEnum value : values()) {
      if (value.code.equals(code)) {
        return value;
      }
    }
    throw new BusinessException(400, "枚举值不合法");
  }

}

以下是一个参数类,它包含了一个枚举字段校验,在表达式中引用了上面的枚举类:

@Data
@SpelValid
public class ParamTestBean {

  /**
   * 枚举值校验
   * <p>
   * 通过静态方法调用,校验枚举值是否存在
   */
  @SpelAssert(assertTrue = "T(cn.sticki.validator.spel.enums.ExampleEnum).getByCode(#this.testEnum)")
  private Integer testEnum;

}

ParamTestBean 校验失败时,我们希望它抛出一个业务异常 BusinessException,但实际上会得到一个 ValidationException

在这里插入图片描述

由于本组件的特殊性,所有抛出的异常信息最终都会被我们下层的校验器捕获,然后包一层 javax.validation.ValidationException 再抛出。

要从框架层面去解决这个问题,只能够脱离 javax.validation 的规范和 hibernate 的执行器来进行校验,
目前看来这样做的成本比较大,且会带来一些其他的影响,故暂时不考虑这样做。

解决方案

当捕获到 ValidationException 时,首先判断下 e.getCause() 的类型是不是自己项目中的业务异常基类,如果是业务异常的类型,就丢给对应的方法去处理,像这样:

@RestControllerAdvice
public class ControllerExceptionAdvice {

  @ExceptionHandler({BusinessException.class})
  public Resp<Void> handleBusinessException(BusinessException ex) {
    return new Resp<>(ex.getCode(), ex.getMessage());
  }

  @ExceptionHandler({ValidationException.class})
  public Resp<Void> handleValidationException(ValidationException ex) {
    if (ex.getCause() instanceof BusinessException) {
      return handleBusinessException((BusinessException) ex.getCause());
    }
    return new Resp<>(500, "system error");
  }

}

当然这种方案也有缺点,需要将多种不同的异常类型都进行特殊处理,比较麻烦。

本人不才,目前只能想到这种方案,如果你有更好的解决方案,欢迎到 GitHub 提 issue。

开启对 Spring Bean 的支持

默认情况下,解析器无法识别 SpEL 表达式中的 Spring Bean。

如果需要在 SpEL 表达式中调用 Spring Bean,需要在任意一个被 Spring 托管的类上添加 @EnableSpelValidatorBeanRegistrar 注解,
开启 Spring Bean 支持。

@EnableSpelValidatorBeanRegistrar /*添加注解*/
@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

}

开启 Spring Bean 支持后,即可在 SpEL 表达式中调用 Spring Bean。

调用 Bean 的语法为:@beanName.methodName(参数)

@Data
@SpelValid
public class SimpleExampleParamVo {

  /**
   * 调用 userService 的 getById 方法,判断用户是否存在
   * 校验失败时,提示信息为:用户不存在
   * 这里只是简单举例,实际开发中不建议这样判断用户是否存在
   */
  @SpelAssert(assertTrue = "@userService.getById(#this.userId) != null", message = "用户不存在")
  private Long userId;

}

后记

OK兄弟们,如果你能看到这里,说明你对这个组件还算有点兴趣,不妨到GitHub给它点个star,或者给我点个关注,以便于持续关注项目更新动向,谢谢你的支持~~

GitHub地址:https://github.com/stick-i/spel-validator

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2097474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Puppeteer的高级用法:如何在Node.js中实现复杂的Web Scraping

概述 随着互联网的发展&#xff0c;网页数据抓取&#xff08;Web Scraping&#xff09;已成为数据分析和市场调研的重要手段之一。Puppeteer作为一款强大的无头浏览器自动化工具&#xff0c;能够在Node.js环境中模拟用户行为&#xff0c;从而高效地抓取网页数据。然而&#xf…

Java基于微信小程序的实习管理系统

简介 本次开发的实习生管理系统实现了字典管理、公告管理、公司管理、简历管理、老师管理、实习管理、实习日志管理、通知管理、学生管理、职位招聘管理、职位收藏管理、职位留言管理、简历投递管理、管理员管理等功能。系统用到了关系型数据库中王者MySql作为系统的数据库&am…

点云配准之ICP和NDT算法的高斯牛顿法求解

ICP算法 NDT算法 代码&#xff1a;https://github.com/taifyang/pointcloud-registration 参考&#xff1a;高翔《自动驾驶与机器人中的SLAM技术》

打造灵动空间,流动会场的声学优势—轻空间

在现代社会中&#xff0c;各类会议、展览、演出、培训等活动越来越多&#xff0c;对场地的需求也越来越多样化。传统的固定场地往往难以满足不同活动的需求&#xff0c;而“流动会场”凭借其灵活多变的特点&#xff0c;迅速成为各类活动的新宠。特别是其独特的声学优势&#xf…

【数据结构】二叉树的链式结构,二叉树的遍历,求节点个数以及高度

目录 1. 二叉树链式结构的概念 2. 二叉树的遍历 2.1 前序遍历 2.2 中序遍历 2.3 后序遍历 2.4 层序遍历 3. 二叉树的节点个数以及高度 3.1 二叉树节点个数 3.2 二叉树叶子节点个数 3.3 二叉树的高度 3.4 二叉树第k层节点个数 3.5 二叉树查找值为x的节点 4. 二叉树…

数造科技荣登“科创杯”领奖台,开启数据驱动新篇章!

8月27日&#xff0c;第十三届中国创新创业大赛(海南赛区)暨海南省第十届“科创杯”创新创业大赛决赛在海口圆满落幕。数造科技凭其在大数据管理领域的专业技术实力&#xff0c;荣获成长企业组三等奖。 突出重围&#xff0c;崭露头角 海南省“科创杯”创新创业大赛是在中国科技…

安科瑞ADL系列导轨式多功能电能表 带外置互感器 CE认证

产品概述&#xff1a; ‌安科瑞ADL系列导轨式多功能电能表‌是安科瑞企业微电网能效管理事业部推出的一款智能仪表&#xff0c;主要针对光伏并网系统、微逆系统、储能系统、交流耦合系统等新能源发电系统设计。这款电能表具有高精度、体积小、响应速度达100ms&#xff0c;以及…

噪音消除模块调研

一.原理 1.1降噪 noisereduce 库的 reduce_noise 函数使用的是一种基于频谱减法的噪声消除算法。它通过分析音频的频谱&#xff0c;识别出噪声成分&#xff0c;并尝试将这些噪声成分从音频信号中去除&#xff0c;从而提升信号的清晰度。 1.2 动态范围压缩&#xff08;预加重&am…

Ollama:本地大语言模型解决方案

在人工智能领域&#xff0c;大语言模型&#xff08;LLM&#xff09;因其在自然语言处理上的强大能力而备受瞩目。然而&#xff0c;这些模型往往需要大量的计算资源和网络连接&#xff0c;限制了它们在本地环境的应用。Ollama 的推出&#xff0c;为这一问题提供了解决方案。作为…

基于C语言实现文件压缩与解压缩算法

引言 随着互联网的发展&#xff0c;数据传输和存储的需求日益增长&#xff0c;文件压缩技术成为提高数据处理效率的关键技术之一。压缩技术不仅可以减少存储空间的需求&#xff0c;还能加快数据在网络中的传输速度。霍夫曼编码作为一种有效的无损数据压缩算法&#xff0c;广泛…

如何为你的 LLM 应用选择最合适的 Embedding 模型

如果你正在构建 2024 年的生成式人工智能&#xff08;GenAI&#xff09;应用&#xff0c;你现在可能已经听过几次 "嵌入&#xff08;embedding&#xff09; "这个词了&#xff0c;而且每周都能看到新的嵌入模型上架。 那么&#xff0c;为什么会有这么多人突然关心起嵌…

ElasticSearch 集群索引和分片的CURD

一、ES集群的索引 背景&#xff1a;Elasticsearch会对所有输入的文本进行处理&#xff0c;建立索引放入内存中&#xff0c;从而提高搜索效率。在这一点上ES优于MYSQL的B树的结构&#xff0c;MYSQL需要将索引放入磁盘&#xff0c;每次读取需要先从磁盘读取索引然后寻找对应的数据…

OpenAI Gym custom environment: Discrete observation space with real values

题意&#xff1a;OpenAI Gym 自定义环境&#xff1a;具有实数值的离散观测空间 问题背景&#xff1a; I would like to create custom openai gym environment that has discrete state space, but with float values. To be more precise, it should be a range of values wi…

翻译软件 Fastrans 开发日志 #2

就过了几天&#xff0c;我的 Fastrans 项目&#xff08; https://github.com/YaoqxCN/Fastrans &#xff09;又更新了两个版本&#xff0c;现在是 v1.1.1。&#xff08;求个 star 谢谢&#xff01;&#xff09; 上次我初步实现了 Fastrans 的翻译功能以及 UI&#xff0c;可以看…

【C++ Primer Plus习题】8.1

问题: 解答: #include <iostream> using namespace std;void print(const char* str) {cout << str << endl; }void print(const char* str,int size) {static int count 0;count;for (int i 0; i < count; i){cout << str << endl;} }int…

机器学习数学公式推导之线性回归

文章目录 线性回归一、最小二乘法1.1 范数的概念1.2 最小二乘法的推导1.3 几何意义 二、噪声为高斯分布的 MLE2.1 LSE&#xff08;最小二乘估计&#xff09;2.2 MLE&#xff08;极大似然估计&#xff09;2.3 LSE与MLE的联系与区别 三、权重先验也为高斯分布的 MAP四、正则化4.1…

APO的接口级拓扑 VS Dynatrace ServiceFlow

在可观测性系统中&#xff0c;几乎所有的产品都会提供拓扑功能。大部分用户在初看这个拓扑之时都会觉得非常有用&#xff0c;但是一旦真实落地使用&#xff0c;就感觉这个拓扑比较鸡肋。这篇文章重点探讨APO团队是如何考虑让用户能够更好的使用拓扑&#xff0c;真正发挥出拓扑的…

OpenCV绘图函数(14)图像上绘制文字的函数putText()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在图像上绘制指定的文本字符串。 cv::putText 函数在图像上绘制指定的文本字符串。无法使用指定字体渲染的符号会被问号&#xff08;?&#xff…

从理论层面设计简单的电池管理系统(BMS)

前言 最近阅读了《便携式设备的电池电源管理》和《大规模锂离子电池管理系统》这两本书&#xff0c;都是比较容易入门的BMS书籍&#xff0c;书中作者做了很多深层次的思考&#xff0c;所以我摘抄了一些部分&#xff1b;同时结合我个人的项目经验及一些理解&#xff0c;整理成这…

中核武汉首位“数字员工”报到,实在智能提供RPA技术解决方案

近期新员工入职季&#xff0c;中核武汉核电运行技术股份有限公司&#xff08;以下简称“中核武汉”&#xff09;迎来了一位“看不见的新同事”——公司首位数字员工“武小数”。“武小数”基于先进的机器人流程自动化技术&#xff08;RPA&#xff09;诞生&#xff0c;结合OCR图…