我发布了一个自认为很厉害的 Java 参数校验组件,它几乎可以满足所有的参数校验场景,请各位 V 友发表一些看法 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sticki
V2EX    Java

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

  •  1
     
  •   sticki
    stick-i 2024-05-06 17:50:57 +08:00 731 次点击
    这是一个创建于 522 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如题,组件名为 “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  
       2024-05-06 17:57:08 +08:00
    下次一定用
    HojiOShi
        2
    HojiOShi  
       2024-05-06 19:14:05 +08:00
    我虽然不是搞后端这方向的,不过 java 一定有很多同类的库,你的这个和已有的库相比有什么优势吗?看到测试也没有写,怎么让人放心用到生产环境中去呢?
    sticki
        3
    sticki  
    OP
       2024-05-06 19:25:12 +08:00
    @HojiOShi
    1. 目前没有找到功能和我这个一样的库,它的优势就是我上面写到的 “解决了什么问题” 部分
    2. 目前确实没有写测试用例,只有少数的使用示例在一个单独的项目中,这块确实需要补充,感谢提醒
    fkdog
        4
    fkdog  
       2024-05-06 19:54:52 +08:00
    就这句:
    @SpelNotNull(cOndition= "#this.switchAudio == true", message = "语音内容不能为空")

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

    而且参数校验逻辑是一个很个性化的东西,javax validation 自带的满足最通用的足矣。
    watzds
        5
    watzds  
       2024-05-06 20:12:37 +08:00
    IDE 查看使用、重构之类不友好吧
    firecooloo1024
        6
    firecooloo1024  
       2024-05-06 20:14:33 +08:00 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
       2024-05-06 20:45:35 +08:00   1
    @fkdog 当然可以自己写代码实现,如果愿意的话,javax validation 也可以不用。4G 普及之前,大家也觉得没必要,我认为这是一样的道理。

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

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

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

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

    另外,静态编译的检查确实是一个问题,启动时扫描并编译感觉有点困难,参考 mybatis ,或许可以通过插件的形式来解决。
    chent114514
        18
    chent114514  
       2024-05-09 16:10:16 +08:00
    那我要是来个 ipv6 规则校验呢
    fengpan567
        19
    fengpan567  
       2024-05-09 17:37:47 +08:00
    手写 spel 。。。
    sticki
        20
    sticki  
    OP
       2024-05-09 18:21:54 +08:00
    @chent114514 嘿,兄弟,注意我提的第三个示例,复杂逻辑校验,可以调用静态方法。你写一个 ipv6 的校验规则,然后在表达式里调用它就好了。
    Leoking222
        21
    Leoking222  
       2024-06-04 09:46:36 +08:00
    给你点赞,兄弟
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1411 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 25ms UTC 16:41 PVG 00:41 LAX 09:41 JFK 12:41
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86