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

收集 Java 程序员在处理接口的参数校验时觉得麻烦的例子,收集无法使用 @NotNull 等注解来校验参数的例子

  •  1
     
  •   sticki ·
    stick-i · 14 天前 · 1887 次点击

    对于 Java 程序员来说,接口的参数校验一般是通过 @NotNull 、 @NotBlank 、 @Size(min = 0, max = 255) 等这些注解来校验的。

    但是有一些情况下,这些注解无法满足校验的需求,比如下面这段代码:

    public class Test {
    
    	/**
    	 * 状态,值必须为枚举类 Status 中的 code
    	 */
    	Integer status;
    
    	/**
    	 * 是否开启语音提示
    	 */
    	Boolean enableVoice;
    
    	/**
    	 * 语音提示内容
    	 */
    	String voiceContent;
    
    }
    

    这里涉及到两种参数校验是现有的注解无法满足的:

    1. 状态的 Int 值必须存在于枚举类中
    2. 当开启语音提示时,内容必须不为空,本质上是上下文关联的参数校验

    这里我只是进行举例,我主要是想收集一下大家遇到的类型情况,然后我想做一个组件,能够尽可能的解决这些问题。

    其实我已经有一个这样的组件了,但是担心自己考虑到的情况还不太够,所以想收集一下大家的建议,之后完善一下然后开源出来给大家使用。

    第 1 条附言  ·  13 天前

    我目前实现的这套东西是基于 spel + 注解 的,本来想到V站问一下有需求的场景,没想到得到了这么多关于这个组件的评价😅,目前的情况是,我发布这个帖子到现在以来还没有收集到一个有用的用例场景😅。

    我实现的这套东西在使用上绝对是没毛病的,大大的提升了便利,举个例子:

    public class TestParamVo {
    
    	/**
    	 * 状态,值必须为枚举类 StatusEnum 中的 code
    	 */
            @SpelAssert(" StatusEnum.getCode(#this.status) != null ") // 当表达式成立时校验通过
    	Integer status;
    
    	/**
    	 * 是否开启语音提示
    	 */
    	Boolean enableVoice;
    
    	/**
    	 * 语音提示内容
    	 */
            @SpelNotNull(" enableVoice == true ") // 当表达式成立时,注解标记的值必须不为null
    	String voiceContent;
    
    }
    

    这是我目前实现的东西,由于是基于 Spel 的,所以可用性很高,如果你需要,甚至可以直接调用 spring bean(当然我不建议这样做),比如这样:

    public class TestParamVo {
    
    	/**
    	 * 用户id,必须是有效的用户
    	 */
            @SpelAssert(" userService.getById(#this.userId) != null ") // 当表达式成立时校验通过
    	Integer userId;
    
    }
    

    这只是部分简单的demo,我实现的东西比这个更完善。另外,这套组件确实基于 ConstraintValidator,但远不是简单的自定义一个 Validator 这么简单的,因为 ConstraintValidator 中只能拿到当前标记的字段值,无法获取到上下文的其他字段(enableVoice那个场景解决不了)。

    希望大家尽可能提供用例,而不是劝退我,谢谢。

    30 条回复    2024-05-06 10:20:19 +08:00
    gitdoit
        1
    gitdoit  
       14 天前
    你是否在寻找 javax.validation.constraints.AssertTrue
    liaojl
        2
    liaojl  
       14 天前 via iPhone
    复杂的参数校验直接 service 里写了,上下文关联的,有些还要查数据库校验的,很难做到一个组件通吃的。
    sticki
        3
    sticki  
    OP
       14 天前
    @gitdoit 看起来并不是
    sticki
        4
    sticki  
    OP
       14 天前
    @liaojl 我只做那些不用查库的,另外我只是想收集这些例子🤡
    cheng6563
        5
    cheng6563  
       14 天前
    简单解决下常用问题就行了,别搞太复杂。
    你是可以搞个解决所有问题的 DSL 来做参数验证,但到了那阶段还不如直接敲程序解决了
    eastjoehan
        6
    eastjoehan  
       14 天前
    之前碰到一个,前端传入状态值的,我想用判断状态值是否在已有枚举中,或者在集合里面,我找了下好像没有支持这个情形的注解?
    walle1530
        7
    walle1530  
       14 天前
    你是否在寻找 javax.validation.ConstraintValidator
    sticki
        8
    sticki  
    OP
       14 天前
    @eastjoehan 是的,我就是想收集这些无法使用现有注解来进行校验的情况

    其实我是只希望大家告诉我这些情况就好了,不用跟我说其他劝退的话😅(这句不是跟 @eastjoehan 说的)
    sticki
        9
    sticki  
    OP
       14 天前
    @walle1530 ConstraintValidator 可以解决一部分问题,但不够通用,比如我上面举例的第 2 点,enable 的那个,用 ConstraintValidator 是无法解决的
    walle1530
        10
    walle1530  
       14 天前
    @sticki #9 理论上来说应该是可以的,定义注解的目标参数和依赖参数,然后定义 Validator 来写检查逻辑,但除非你有大量此类的依赖参数需要检查,如果只是一两个的话,这样更麻烦了。
    pxcking
        11
    pxcking  
       14 天前
    public class NotIncludeNullValidator implements ConstraintValidator<NotIncludeNull, Collection<?>> {
    @Override
    public void initialize(NotIncludeNull constraintAnnotation) {

    }

    @Override
    public boolean isValid(Collection<?> value, ConstraintValidatorContext context) {
    // 不校验空集合
    if (CollectionUtils.isEmpty(value)) {
    return true;
    }
    for (Object next : value) {
    if (next == null) {
    return false;
    }
    }
    return true;
    }
    }
    Ashe007
        12
    Ashe007  
       14 天前 via iPhone
    没必要,我参数检验选择使用 spring 自带的断言 Assert ,写在参数类中,通过参数对象调用即可

    不明白为啥那么多写 jar 包的,特别是写一些垃圾代码的。让人看着就想喷
    Ashe007
        13
    Ashe007  
       14 天前 via iPhone
    @Ashe007 比如 Java bean 属性用基本类型,工具类是某个 apache 工具类套壳,或者内部本身存在 bug 或非常不合理的逻辑
    chihiro2014
        14
    chihiro2014  
       14 天前   ❤️ 1
    RedBeanIce
        15
    RedBeanIce  
       13 天前
    加油,

    需要查库等的复杂逻辑,我一般写在代码中。
    mmdsun
        16
    mmdsun  
       13 天前 via iPhone
    这个校验注解可以自定义,加类上都可以
    yosoroAida
        17
    yosoroAida  
       13 天前
    你这可以在参数里面写个方法,并加上 @NotNull 注解 ,参考下我这例子

    ```java
    @Setter
    class ExampleParam{
    private Integer code;

    @NotNull(message = "你的业务逻辑提示")
    public ExampleEnum getCodeEnum() {
    // 根据 code 去获取 ExampleEnum ,然后返回
    return ExampleEnum.getByValue(code);
    }
    }

    @AllArgsConstructor
    @Getter
    enum ExampleEnum{
    ;
    private final Integer code;

    public static ExampleEnum getByValue(Integer value){
    for (ExampleEnum exampleEnum : ExampleEnum.values()) {
    if(exampleEnum.getCode().equals(value)){
    return exampleEnum;
    }
    }
    return null;
    }
    }
    ```
    yosoroAida
        18
    yosoroAida  
       13 天前
    @yosoroAida
    这样的话,在 Validate 框架去做校验的时候,也会去校验上面个的 getCodeEnum() 是否返回为空,如果返回为空的话,就会把 @NotNull 的 message 的信息给返回回去,这样就免去你在业务逻辑里面做参数校验了。
    这样也体现了面向对象编程的优势对吧?
    JackCh3ng
        19
    JackCh3ng  
       13 天前
    如果 status 是枚举值,为什么不把 Integer 直接换成枚举类呢?有什么问题吗?
    halov
        20
    halov  
       13 天前
    不是可以增加自定义的验证注解吗 ,通过 aop 的方式 复杂的业务验证也可以实现
    jov1
        21
    jov1  
       13 天前
    也会有类似问题,通用的用提供的注解声明,这种情况判定来动态处理验证的,我目前放在业务实现里面校验处理,因为可能还需要对数据处理,比如 enableVoice 不等于 true ,如果前端传入了 voiceContent ,后端其实还类似需要清空这个值,那么顺手可以把校验 enableVoice=true 时 voiceContent 的非空校验做了,
    类似于
    if (BooleanUtils.isTrue(xx.getEnableVoice()) {

    }
    jov1
        22
    jov1  
       13 天前
    if (BooleanUtils.isTrue(xx.getEnableVoice()) {
    // 校验 voiceContent 是否为空
    } else {
    xx.setVoiceContent(null);
    } 应该也可以实现 ConstraintValidator 来自定义校验规则,拓展注解之类的,只是目前是类似这样处理的
    unclevv
        23
    unclevv  
       13 天前
    只有我一个人觉得 Boolean enableVoice 定义不规范吗,Boolean voiceSwitch 与 String voiceContent 对应,作为变量,enableVoice 应该是用作方法名
    layxy
        24
    layxy  
       13 天前
    1.自定义校验注解和校验器
    2.建议手动校验吧,自动校验也可以实现,但是把业务判断逻辑放到配置中感觉不是太好
    KongLiu
        25
    KongLiu  
       13 天前
    自定义验证+正则
    liuzhaowei55
        26
    liuzhaowei55  
       13 天前 via Android
    可以看看 laravel 的 validation 设计,完爆其他所有语言,框架的校验
    目前比较需要的就是你提到的这个校验入参值是否在给定可选值中
    Karte
        27
    Karte  
       13 天前
    对于枚举的验证做法:
    1. 硬代码, 通过 Assert 相关断言判断
    2. 通过 @Min, @Max 设定枚举边界.
    3. 通过实现 `ConstraintValidator` 对类进行校验.
    4. 通过代理 + 反射的方式添加自定义处理注解, 然后通过定义 `spel` 表达式确认是否校验通过.
    ```java
    @FieldValid(spel = "#p1.field != 0")
    ```

    用硬代码虽然会略显丑陋, 但是在新增枚举时可以不用改动代码.
    而 @Min, @Max 在枚举修改后需要同步修改, 如果没有相关注释说明会存在隐性 BUG.
    使用 `ConstraintValidator` 相对比较好, 因为能够自定义实现校验逻辑. 但是验证范围只能为对应类, 如果不做继承的话
    无法实现复用
    代理 + 反射会有略微性能损耗, 而 `spel` 表达式需要额外学习, 且有点耗费性能 (通过反射获取数据). 在多参数时需要在编译时增加 `-parameters` 否则参数名无法写入, `spel` 就无法获取到对应的对象, 校验就失败了.
    Ashe007
        28
    Ashe007  
       13 天前 via iPhone
    @halov 通常的 aop 是基于代理模式实现,为了一个参数检验,引入更大的项目复杂度,消耗更多的服务器资源,你觉得可取吗?
    sticki
        29
    sticki  
    OP
       12 天前
    我才添加完附言,就没人回复了,沉底了🤡🤡
    Aresxue
        30
    Aresxue  
       6 天前   ❤️ 1
    第一确实存在这样的业务诉求,这是很合理的,别被其它人绕晕了;
    第二实现方式最好不是自定义一套注解,理解成本很高,具体很好的实现方式一下子没想好,最好是可以和原来搭配使用,比如 SpelNotNull 就可以改成 @NotNull + @When("enableVoice == true"),上面只是举个例子;
    第三注意这里存在潜在的性能问题对于高并发接口来说这些校验最好是直接写在业务代码中的,但是绝大多数接口并不是什么高并发接口,这个东西的用处应当是用来快速实现校验功能,当这个接口的 QPS 要求发生变化时再重新设计实现,当然更多接口可能永远都都不到这一步。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1194 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 18:20 · PVG 02:20 · LAX 11:20 · JFK 14:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.