V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sticki
V2EX  ›  Java

我发布了一个自认为很厉害的 Java 参数校验组件,它几乎可以满足所有的参数校验场景,请各位 V 友发表一些看法

  •  1
     
  •   sticki ·
    stick-i · 238 天前 · 6825 次点击
    这是一个创建于 238 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题,组件名为 “SpEL Validator”,下面我会进行一些介绍,希望各位看完后可以发表一些看法。

    「 SpEL Validator 」是基于 SpEL 的参数校验包,也是 javax.validation 的扩展增强包,用于简化参数校验。

    解决了什么问题?

    • 枚举值字段校验:

      @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 一致,学习成本低,上手快。

    使用方式

    • 添加依赖

      Latest Version: Maven Central

      <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;
      
      }
      
    • 发起请求,即可看到校验结果

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

    大概就是这样

    以上是关于这个组件的大概介绍,希望各位大佬能够对此发表一些看法,好或者不好都可以发表,感谢各位~

    感兴趣的朋友也可以到 GitHub 或者掘金查看详细情况。

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

    我在掘金发布的详细说明文章: https://juejin.cn/post/7365698962531401766

    21 条回复    2024-06-04 09:46:36 +08:00
    jwj
        1
    jwj  
       238 天前
    下次一定用
    HojiOShi
        2
    HojiOShi  
       238 天前
    我虽然不是搞后端这方向的,不过 java 一定有很多同类的库,你的这个和已有的库相比有什么优势吗?看到测试也没有写,怎么让人放心用到生产环境中去呢?
    sticki
        3
    sticki  
    OP
       238 天前
    @HojiOShi
    1. 目前没有找到功能和我这个一样的库,它的优势就是我上面写到的 “解决了什么问题” 部分
    2. 目前确实没有写测试用例,只有少数的使用示例在一个单独的项目中,这块确实需要补充,感谢提醒
    fkdog
        4
    fkdog  
       238 天前
    就这句:
    @SpelNotNull(condition = "#this.switchAudio == true", message = "语音内容不能为空")

    我自己定义一个静态方法,ExceptionUtils.throwIf(this.switchAudio, "语音内容不能为空")不就好了?
    为什么还要额外引入你一个类库呢,而且借助反射 API 还会降低额外性能。

    而且参数校验逻辑是一个很个性化的东西,javax validation 自带的满足最通用的足矣。
    watzds
        5
    watzds  
       238 天前
    IDE 查看使用、重构之类不友好吧
    firecooloo1024
        6
    firecooloo1024  
       238 天前 via Android
    其实没必要,记这么多规则增加负担。试试这样写:
    ```java
    @Data
    public class UserVo {

    private String username;
    private Integer age;
    private List<String> hobby;

    @AssertTrue
    public boolean isValid() {
    return StringUtils.isNotEmpty(username)
    && age > 0
    && age < 100
    && !hobby.isEmpty();
    }
    }
    ```
    sticki
        7
    sticki  
    OP
       238 天前   ❤️ 1
    @fkdog 当然可以自己写代码实现,如果愿意的话,javax validation 也可以不用。4G 普及之前,大家也觉得没必要,我认为这是一样的道理。

    至于反射降低的性能,对于一个接口请求来说,只是九牛一毛罢了,框架带来的便利性,往往都会牺牲一定的性能,那几毫秒的延迟,在绝大多数场景下,都是不重要的。举个不恰当的例子,Spring 内也包含了大量的反射,但没人在乎。

    再说说个性化,这套组件就是为了解决个性化的参数校验而生的,它几乎可以满足任何个性化的参数校验。
    sticki
        8
    sticki  
    OP
       238 天前
    @firecooloo1024 我也这样写过,没什么毛病,就是代码略多一点。这套组件的规则并不复杂,其实和 javax validation 那些注解差不了多少,唯一需要学习的是 SpEL 的语法,但其实也很简单。
    sticki
        9
    sticki  
    OP
       238 天前   ❤️ 1
    @watzds 对,这是一个问题,我给字段使用了 @Language("SpEL"),但 idea 只能识别部分,这很奇怪。未来或许会通过插件的形式辅助开发者使用这套组件,从而解决这个问题。
    firecooloo1024
        10
    firecooloo1024  
       238 天前 via Android
    @sticki 一般常用注解加字段上就够了,只有你说的枚举、字段联合、复杂校验等才单独写个 is 方法校验,校验逻辑集中,逻辑清晰,没有心智负担,就多了个自定义方法而已,太纯粹的贫血模型也不怎么好。你那个当做学习还行,生产不敢用,哈哈哈嗝
    LeegoYih
        11
    LeegoYih  
       238 天前
    这么写有点恐怖
    xwayway
        12
    xwayway  
       237 天前
    condition 和 assertTrue 里面调用属性、方法 全是字符串,对于重构很不友好。
    justNoBody
        13
    justNoBody  
       237 天前
    我觉得给出来的例子不是很好。

    多字段联合校验中,`contentType=1`和`contentType=2`其实是两个不同的业务,如果用了您的`SpEL Validator`,这个业务校验逻辑就放到了`POJO`中。

    我个人认为最好是放到业务实现中,以免产生不必要的耦合。
    yihy8023
        14
    yihy8023  
       237 天前   ❤️ 1
    给你点赞~感觉作为 javax.validation 额外的补充包不错。
    个人觉得楼主直接用 condition 表达式灵活度太高了,里面的规则很难控制复杂度,把它作为保底手段,并且不要写复杂规则还行。condition 用成表达式会导致失去了 java 静态编译的检查,使错误从编译期便到了运行时,并且还要调用才能触发错误,危险程度太高了。假设你改了个字段名,对应的 condition 没改,上到生产后,客户一使用,才发现报错,你慌不慌。
    最后提个建议 在启动时扫描注解,把注解的表达式都编译一遍缓存下来,至少问题能在启动时发现。
    zmal
        15
    zmal  
       237 天前
    点赞!
    但 java 现在几乎是一个纯工业语言,灵活性相比 其他新兴语言差一些。在 java 里追求灵活性的各种魔法,反而会抛弃 java 相对严谨的语言特性。过于复杂的 SpEL 表达式不是一个很好的方案。
    sticki
        16
    sticki  
    OP
       237 天前
    @xwayway 这个问题我在 #9 回复过,实际上 idea 可以识别 SpEL 表达式,识别后字符串会有引用的效果,但目前我的组件对这个识别功能还不完全兼容
    sticki
        17
    sticki  
    OP
       237 天前
    @yihy8023 @zmal 对的,简单的规则还行,复杂的规则建议写成静态方法然后在表达式里调用,涉及业务数据的校验还是写在 service 层会更好。

    另外,静态编译的检查确实是一个问题,启动时扫描并编译感觉有点困难,参考 mybatis ,或许可以通过插件的形式来解决。
    chent114514
        18
    chent114514  
       235 天前
    那我要是来个 ipv6 规则校验呢
    fengpan567
        19
    fengpan567  
       235 天前
    手写 spel 。。。
    sticki
        20
    sticki  
    OP
       235 天前
    @chent114514 嘿,兄弟,注意我提的第三个示例,复杂逻辑校验,可以调用静态方法。你写一个 ipv6 的校验规则,然后在表达式里调用它就好了。
    Leoking222
        21
    Leoking222  
       209 天前
    给你点赞,兄弟
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1031 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 22:22 · PVG 06:22 · LAX 14:22 · JFK 17:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.