我给validation-api增加了按条件参数校验的功能并开源了

首发公众号: 赵侠客

我正在参加掘金2024年度人气创作者打榜中来帮我打榜吧~

引言

在后端接口开发中经常需要对接口传入的参数进行校验,如非空校验、长度校验、手机号码格式校验、邮箱格式校验等等,常用的if-else判断虽然可以胜任任何场景的参数校验,但是有着开发效率低、冗余代码多、代码复用性差等问题,于是就出现了像validation-api这种只要增加一个@NotNull@Email这样的注解就可以很优雅的进行参数校验的框架,validation-api在项目中的使用是非常广泛,也是非常方便的,但是它并不是万能的,在某些特定的情况下是无法很优雅的完成参数校验的,比如今天要讨论的:在不同参数条件下对不同的参数进行校验的场景它就无法完成了 。本文针对这种场景开发出validation-plus来扩展validation-api,让它能更方便、快捷的适用于更多、更广泛的参数校验场景。

validation-api 简单使用

以最简单的修改用户信息接口为例简单介绍一下validation-api的使用,只需要在接收参数的实体对象上增加@NotNull(message = "用户ID不能为空")注解,然后在接收接口的方法中增加@Valid就可以完成用户ID不能为空的参数校验了。

User实体参数增加@NotNull注解:

java 复制代码
@Data
public class User {
    @NotNull(message = "用户ID不能为空")
    private Long id;
    @NotNull(message = "用户名不能为空")
    private String userName;
}

用户修改方法增加@Valid注解:

java 复制代码
@PostMapping("/update")
public Map<String, Object> update(@RequestBody @Valid User user) {
    return Map.of("code", 0, "data", user);
}

但是实际开发过程的参数校验可能并不是这么简单,比如我们需要开发一个保存用户通知的接口,用户可以选择邮箱或者手机号码来接收通知消息。首先我们需要定义UserNotice对象:

java 复制代码
@Data
public class UserNotice {
    @NotNull(message = "通知类型不能为空 0-短信 1-邮件")
    private Integer noticeType;
    private String userMobile;
    private String userEmail;
}

UserNotice对象中noticeType通知类型肯定是不能为空的,我们以:

  • 0-表示使用短信接收通知消息
  • 1-表示使用邮箱接收通知消息

那么问题来了:我们不能直接在userMobile或者userEmail增加@NotNull

  • 只有当noticeType=0时userMobile不能为空
  • 当noticeType=1时userEmail才不能为空

在这种情况下validation-api就无法完成参数校验了。

方案一、增加额外方法

最开始我想到的解决方案是:写一个方法,在方法里添加条件判断逻辑,然后增加@AssertTrue注解达到按条件进行参数校验的目的

java 复制代码
@Data
public class UserNotice {
    @NotNull(message = "通知类型不能为空 0-短信 1-邮件")
    private Integer noticeType;
    private String userMobile;
    private String userEmail;

    @AssertTrue(message = "手机不能为空")
    public Boolean getMobileValidator() {
        return ObjectUtils.nullSafeEquals(noticeType, 1) || Validator.isMobile(userMobile);
    }

    @AssertTrue(message = "邮箱地址不能为空")
    public Boolean getEmailValidator() {
        return ObjectUtils.nullSafeEquals(noticeType, 0) || Validator.isEmail(userEmail);
    }
}

但是这种方案看起来并不是很优雅,主要有以下问题:

  • 还是增加了大量的if-else判断代码
  • 自己还要写很多类似Validator.isEmail()这样的数据格式判断
  • 没有直接的用validation-api中提供的丰富数据格式类型的判断

方案二、自己造轮子

我的想法很简单:在注解中增加一个条件参数,写上EL表达式,如果EL表达式为True则执行参数校验,为False就不执行

比如我们要自定义一个IPV4参数校验器的代码:

java 复制代码
@Documented
@Target({ METHOD, FIELD, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(Ipv4.List.class)
@Constraint(validatedBy = {Ipv4Validator.class})
public @interface Ipv4 {
    String message() default "not is Ipv4";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    @Target({ METHOD, FIELD, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        Ipv4[] value();
    }
}

public class Ipv4Validator implements ConstraintValidator<Ipv4, String> {
    @Override
    public void initialize(Ipv4 constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 对象为空验证通过
        if (value == null) {
            return true;
        }
        return Validator.isIpv4(value);
    }
}

按照这个思路我的想法是自定义一个@NotNullOn,然后通过EL表达式编写判断的条件:

java 复制代码
@Data
@EnableCondition
public class UserNotice {
    @NotNull(message = "通知类型不能为空 0-短信 1-邮件")
    private Integer noticeType;

    @NotNullOn(on="#noticeType=0", message = "手机不能为空")
    private String userMobile;

    @NotNullOn(on="#noticeType=1", message = "邮箱地址不能为空")
    private String userEmail;
}

但是@NotNullOn增加在字段上,是对字段进行拦截的,肯定是拿不到UserNotice对象,所以无法执行on中的El表达式,必须对类进行拦截,所以还要定义一个@EnableCondition注解,添加在类上。

添加开启条件注解@EnableCondition:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Constraint(validatedBy = ConditionValidator.class)
public @interface EnableCondition {
    @Deprecated
    String message() default "";
    @Deprecated
    Class<?>[] groups() default { };
    @Deprecated
    Class<? extends Payload>[] payload() default { };
}

添加条件不为空注解@NotNullOn

JAVA 复制代码
@Documented
@Target({ FIELD })
@Retention(RUNTIME)
public @interface NotNullOn {
    String on();
    String message() default "";
}

添加条件True注解@NotNullOn

JAVA 复制代码
@Documented
@Target({ FIELD })
@Retention(RUNTIME)
public @interface AssertTrueOn {
    String on();
    String message() default "";
}

添加条件False空注解@AssertFalseOn

JAVA 复制代码
@Documented
@Target({ FIELD })
@Retention(RUNTIME)
public @interface AssertFalseOn {
    String on();
    String message() default "";
}

添加注解解析器:ConditionValidator

java 复制代码
@Slf4j
public class ConditionValidator implements ConstraintValidator<EnableCondition, Object> {

    private final ExpressionParser parser = new SpelExpressionParser();
    private final EvaluationContext elContext = new StandardEvaluationContext();

    @Override
    public boolean isValid(Object validatedBean, ConstraintValidatorContext context) {
        fillBean(validatedBean);
        Field[] fields = ReflectUtil.getFields(validatedBean.getClass());
        Boolean res = true;
        for (Field field : fields) {
            //获取当前字段
            Object fieldValue = ReflectUtil.getFieldValue(validatedBean, field.getName());
            Annotation[] annotations = field.getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation instanceof NotNullOn) {
                    NotNullOn notNullOn = (NotNullOn) annotation;
                    res = res && isValid(NotNullOn.class.getName(), notNullOn.message(), context, notNullOn.on(), field.getName(), fieldValue);
                }
                if (annotation instanceof AssertTrueOn) {
                    AssertTrueOn assertTrueOn = (AssertTrueOn) annotation;
                    res = res && isValid(AssertTrueOn.class.getName(), assertTrueOn.message(), context, assertTrueOn.on(), field.getName(), fieldValue);
                }
                if (annotation instanceof AssertFalseOn) {
                    AssertFalseOn assertTrueOn = (AssertFalseOn) annotation;
                    res = res && isValid(AssertFalseOn.class.getName(), assertTrueOn.message(), context, assertTrueOn.on(), field.getName(), fieldValue);
                }
            }

        }
        return res;
    }

    private boolean isValid(String name, String message, ConstraintValidatorContext context, String on, String fieldName, Object fieldValue) {
        Boolean res = true;
        if (parseEl(on)) {
            ConstraintValidator validator = SupportContext.getValidator(name);
            if (!validator.isValid(fieldValue, context)) {
                res = false;
                context.buildConstraintViolationWithTemplate(message)
                        .addPropertyNode(fieldName)
                        .addConstraintViolation();
            }
        }
        return res;
    }


    private void fillBean(Object object) {
        Map<String, Object> map = BeanUtil.beanToMap(object);
        map.forEach((k, v) -> elContext.setVariable(k, v));
    }

    protected Boolean parseEl(String el) {
        Expression expression = parser.parseExpression(el);
        Object value = expression.getValue(elContext);
        return Boolean.valueOf(value.toString());
    }
}

代码的基本思路是这样的

  • @EnableCondition注解开启按条件参数校验,添加到类上,拦截整个对象
  • @NotNullOn@AssertTrueOn@AssertFalseOn等为自己扩展的条件注解
  • ConditionValidator通过判断 if (parseEl(on))解析条件
  • 在isValid()调用validation-api原有的方法进行参数判断

开源地址

本代码已开源gitee地址:

gitee.com/whzhaochao/...

使用方法

java 复制代码
<dependency>
    <groupId>com.zhaochao</groupId>
    <artifactId>validation-plus</artifactId>
    <version>1.0.0</version>
</dependency>
java 复制代码
@Data
@EnableCondition
public class UserNotice {
    @NotNull(message = "通知类型不能为空 0-短信 1-邮件")
    private Integer noticeType;
    @NotNullOn(on = "#noticeType==0", message = "手机号码不能为空")
    private String userMobile;
    @NotNullOn(on = "#noticeType==1", message = "邮箱不能为空")
    private String userEmail;
    @AssertTrueOn(on = "#noticeType==0", message = "必须是男生")
    private Boolean isBoy;
}
less 复制代码
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/notice")
    public Map<String, Object> save(@RequestBody @Valid UserNotice user) {
        return Map.of("code", 0, "data", user);
    }
}
bash 复制代码
POST http://localhost/user/notice
Content-Type: application/json

{
   "noticeType":0,
   "userMobile":"18093493432",
   "isBoy":false
 }

返回:

css 复制代码
{
 "code": 500,
 "message": "必须是男生"
}

是不是非常的Nice?

总结

本方案解决了validation-api无法在不同条件下使用不同参数判断的业务场景的痛点,扩展出了 @NotNullOn@AssertTrueOn@AssertFalseOn等按条件进行参数校验的场景。看看大家有没有类似的需求场景,目前支持的类型比较少,如果大家也有类似的场景,我可以继续完善validation-plus,扩展出更多条件参数校验的类型。

相关推荐
赵谨言18 分钟前
基于 Java 大数据的旅游推荐系统的设计与实现
java·经验分享·毕业设计
NHuan^_^1 小时前
RabbitMQ基础篇之Java客户端 Topic交换机
java·rabbitmq·java-rabbitmq
中國移动丶移不动2 小时前
Java List 源码解析——从基础到深度剖析
java·后端·list
javaTodo2 小时前
消息队列kafka详解:Kafka架构介绍
后端
Java知识技术分享2 小时前
spring boot 异步线程池的使用
java·spring boot·spring
m0_748232922 小时前
Springboot3.x整合swagger3
java
东林知识库2 小时前
Mysql高级
java·mysql
qianmoQ2 小时前
DataCap 2024.4.1 版本发布:MongoDB 驱动支持、工作流引擎升级
数据库·mongodb·开源
m0_748248233 小时前
Springboot项目:使用MockMvc测试get和post接口(含单个和多个请求参数场景)
java·spring boot·后端
顾北辰203 小时前
基本算法——回归
java·spring boot·机器学习