⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 程序员阿杆 「阿杆」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

参数校验这个东西,很多情况下都是比较简单的,用 @NotNull、@Size 等注解就可以解决绝大多数场景,但也有一些场景是这些基本注解解决不了的,只能用一些其他的方式处理,这样就导致参数校验变成了多层,其实是不利于代码维护的。

于是乎,我写了一套几乎可以满足任何场景的参数校验组件,非常好用,安利给大家。

GitHub地址文末获取!

💡 它解决了什么问题?

  • 枚举值字段校验:

    @SpelAssert(assertTrue = " T(cn.sticki.enums.UserStatusEnum).getByCode(#this.userStatus) != null ", message = "用户状态不合法")
    private Integer userStatus;
  • 多字段联合校验:

    @NotNull
    private Integer contentType;

    @SpelNotNull(condition = "#this.contentType == 1", message = "语音内容不能为空")
    private Object audioContent;

    @SpelNotNull(condition = "#this.contentType == 2", message = "视频内容不能为空")
    private Object videoContent;
  • 复杂逻辑校验,调用静态方法:

    // 中文算两个字符,英文算一个字符,要求总长度不超过 10
    // 调用外部静态方法进行校验
    @SpelAssert(assertTrue = "T(cn.sticki.util.StringUtil).getLength(#this.userName) <= 10", message = "用户名长度不能超过10")
    private String userName;
  • 调用 Spring Bean(需要使用 @EnableSpelValidatorBeanRegistrar 开启Spring Bean支持):

    // 这里只是简单举例,实际开发中不建议这样判断用户是否存在
    @SpelAssert(assertTrue = "@userService.getById(#this.userId) != null", message = "用户不存在")
    private Long userId;
  • 更多使用场景,欢迎探索和补充!

📝 特点

  • 强大的参数校验功能,几乎支持所有场景下的参数校验。
  • 扩展自 javax.validation 包,只新增不修改,无缝集成到项目中。
  • 基于 SpEL(Spring Expression Language) 表达式,支持复杂的校验逻辑。
  • 支持调用 Spring Bean,可在表达式中使用注入过的 Spring Bean。
  • 校验时基于整个对象,支持对象内字段间的校验逻辑。
  • 支持自定义校验注解,可根据业务需求自定义校验逻辑。
  • 无需额外的异常处理,校验失败时会上报到 javax.validation 的异常体系中。
  • 简单易用,使用方式几乎与 javax.validation 一致,学习成本低,上手快。

🎈 环境

目前仅测试了 JDK8 环境,理论上来说 JDK8+ 应该都是支持的。

📦 快速开始

  • 添加依赖

    Latest Version: 0.0.2-beta

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

    <dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>${hibernate-validator.version}</version>
    </dependency>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>${spring-boot-starter-web.version}</version>
    </dependency>
  • 在接口参数上使用 @Valid 或 @Validated 注解

    @RestController
    @RequestMapping("/example")
    public class ExampleController {

    /**
    * 简单校验示例
    */
    @PostMapping("/simple")
    public Resp<Void> simple(@RequestBody @Valid SimpleExampleParamVo simpleExampleParamVo) {
    return Resp.ok(null);
    }

    }
  • 在实体类上使用 @SpelValid 注解,同时在需要校验的字段上使用 @SpelNotNull 等约束注解

    @Data
    @SpelValid
    public class SimpleExampleParamVo {

    @NotNull
    private Boolean switchAudio;

    /**
    * 当 switchAudio 为 true 时,校验 audioContent,audioContent 不能为null
    */
    @SpelNotNull(condition = "#this.switchAudio == true", message = "语音内容不能为空")
    private Object audioContent;

    }
  • 添加全局异常处理器,处理校验异常

    @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);
    }

    }
  • 发起请求,即可看到校验结果

    • 示例一:@SpelNotNull 校验不通过

      请求体:

      {
      "switchAudio": true,
      "audioContent": null
      }

      响应体

      {
      "code": 400,
      "message": "audioContent 语音内容不能为空",
      "data": null
      }
  • 示例二:校验通过

    请求体

    {
    "switchAudio": false,
    "audioContent": null
    }

    响应体

    {
    "code": 200,
    "message": "成功",
    "data": null
    }
  • 示例三:@NotNull 校验不通过

    请求体

    {
    "switchAudio": null,
    "audioContent": null
    }

    响应体

    {
    "code": 400,
    "message": "switchAudio 不能为null",
    "data": null
    }

📖 使用指南

注意:本组件的目的不是代替 javax.validation 的校验注解,而是作为一个扩展,方便某些场景下的参数校验。能够使用 javax.validation 的场景就不要使用 spel-validator ,因为 spel-validator 会有一定的性能损耗。

开启约束校验

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

  1. 在接口参数上使用 @Valid 或 @Validated 注解
  2. 在实体类上使用 @SpelValid 注解

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

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

这是因为 @SpelValid 注解是基于 javax.validation.Constraint 实现的,只有在 @Valid 或 @Validated 注解的支持下才会生效。而 spel-validator 提供的约束注解是基于 @SpelValid 进行扫描校验的,只有在 @SpelValid 注解生效的情况下才会执行约束校验。

使用约束注解

目前支持的约束注解有:

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

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

  • message:校验失败时的提示信息。
  • group:分组条件,支持 SpEL 表达式,当分组条件满足时,才会对带注解的元素进行校验。
  • condition:约束开启条件,支持 SpEL 表达式,当 表达式为空 或 计算结果为true 时,才会对带注解的元素进行校验。

调用 Spring Bean

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

如果需要在 SpEL 表达式中调用 Spring Bean,需要在启动类上添加 @EnableSpelValidatorBeanRegistrar 注解, 开启 Spring Bean 支持。

@EnableSpelValidatorBeanRegistrar
@SpringBootApplication
public class Application {

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

}

自定义约束注解

  1. 在注解上使用 @SpelConstraint ,并指定验证器。
  2. 然后给注解添加上三个固定的字段 message 、group、condition。
  3. 实现验证器

自定义示例约束示例-定义约束注解

自定义约束示例-实现验证器

具体实现方式可以参考 cn.sticki.validator.spel.SpelConstraint 类。

如果你使用过 javax.validation 的自定义约束注解,那么你会发现 SpEL Validator 的自定义约束注解几乎与 javax.validation一致。

📦 示例项目

我也写了一个简单的示例项目,但目前还不太完善,只有基本的使用方法:

最后

关于性能

性能上我目前还没有进行测试,但代码里使用了很多的反射,会有一定的损耗,后面我准备多加一些缓存,尽量降低性能上的影响。

一个奇怪的现象

在项目中使用SpEL的时候,有一个很怪异的现象:

我给这两个字段都标记了 @Language("SpEL"),但是只有 condition 可以识别,我不知道这算不算idea的bug,我目前使用的idea版本是 IntelliJ IDEA 2024.1 (Ultimate Edition),有懂的朋友请帮忙解答一下。

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

文章目录
  1. 1. 💡 它解决了什么问题?
  2. 2. 📝 特点
  3. 3. 🎈 环境
  4. 4. 📦 快速开始
  5. 5. 📖 使用指南
    1. 5.1. 开启约束校验
    2. 5.2. 使用约束注解
    3. 5.3. 调用 Spring Bean
    4. 5.4. 自定义约束注解
  6. 6. 📦 示例项目
  7. 7. 最后
    1. 7.1. 关于性能
    2. 7.2. 一个奇怪的现象