Java业务校验工具实现(续集)

一、背景

在前面的文章分享了一篇自已关于Java业务校验工具的实现Java业务校验工具实现,后面本着“不要重复造轮子”的原则,在网上搜索果然有志同道合的朋友已经实现过相同的功能框架fluent-validator

在大致看完整体功能与大概实现后,觉得这是一个不错的校验框架,但对于我现在的使用场景来说,会感觉有一些“重量级”,即有些功能对我们的使用场景来说有些多余,因为我们的使用场景比较简单直接,即在执行真正的业务逻辑前做业务规则校验,校验不通过则直接返回。本着“简单的就是最好的”原则,所以摘取了fluent-validator的一部分代码写了一个简单版本的fluent-validator,总共类只有6个,FluentValidator.java、Validator.java、ValidatorContext.java、ValidatorElement.java、ValidatorElementList.java、ValidateException.java。以下为全部代码:

二、show me your code

  • FluentValidator.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 链式调用验证器,参考fluent-validator的简单实现
 * https://github.com/neoremind/fluent-validator
 *
 * @author 会跳舞的机器人
 * @date 2019/12/27
 */
public class FluentValidator {
    private static final Logger logger = LoggerFactory.getLogger(FluentValidator.class);

    /**
     * 验证器链,惰性求值期间就是不断的改变这个链表,及时求值期间就是遍历链表依次执行验证
     */
    private ValidatorElementList validatorElementList = new ValidatorElementList();

    /**
     * 验证器上下文
     */
    private ValidatorContext context = new ValidatorContext();

    /**
     * 私有构造方法,只能通过checkAll创建对象
     */
    private FluentValidator() {
    }

    /**
     * 创建FluentValidator对象
     *
     * @return
     */
    public static FluentValidator checkAll() {
        return new FluentValidator();
    }

    /**
     * 使用验证器进行验证
     *
     * @param validator 验证器
     * @return
     */
    public <T> FluentValidator on(Validator<T> validator) {
        validatorElementList.add(new ValidatorElement(null, validator));
        return this;
    }

    /**
     * 使用验证器验证指定对象
     *
     * @param t         待验证对象
     * @param validator 验证器
     * @return
     */
    public <T> FluentValidator on(T t, Validator<T> validator) {
        validatorElementList.add(new ValidatorElement(t, validator));
        return this;
    }
    
    /**
     * 使用验证器验证指定对象
     *
     * @param t         待验证对象
     * @param validator 验证器
     * @param condition 条件,为true时才会将验证器加入验证器列表中
     * @return
     */
    public <T> FluentValidator on(T t, Validator<T> validator, boolean condition) {
        if (condition) {
            validatorElementList.add(new ValidatorElement(t, validator));
        }
        return this;
    }


    /**
     * 执行各个验证器中的验证逻辑
     *
     * @return
     */
    public FluentValidator doValidate() {
        if (validatorElementList.isEmpty()) {
            logger.info("Nothing to validate");
            return null;
        }
        long start = System.currentTimeMillis();
        logger.info("Start to validate,validatorElementList={}", validatorElementList.toString());
        String validatorName;
        try {
            for (ValidatorElement element : validatorElementList.getList()) {
                Object target = element.getTarget();
                Validator validator = element.getValidator();
                validatorName = validator.getClass().getSimpleName();
                logger.info("{} is running", validatorName);
                validator.validate(context, target);
            }
        } catch (ValidateException e) {
            throw e;
        } catch (Exception e) {
            throw e;
        } finally {
            logger.info("End to validate,time consuming {} ms", (System.currentTimeMillis() - start));
        }
        return this;
    }

    /**
     * 将键值对放入上下文
     *
     * @param key   键
     * @param value 值
     * @return FluentValidator
     */
    public FluentValidator putAttribute2Context(String key, Object value) {
        if (context == null) {
            context = new ValidatorContext();
        }
        context.setAttribute(key, value);
        return this;
    }

    /**
     * 获取验证器上下文
     *
     * @return
     */
    public ValidatorContext getContext() {
        return context;
    }
}
  • Validator.java

/**
 * 验证器接口, 泛型T表示待验证对象的类型
 *
 * @author 会跳舞的机器人
 * @date 2019/12/27
 */
public interface Validator<T> {

    /**
     * 执行验证,如果验证失败,则抛出ValidateException异常
     *
     * @param context 验证上下文
     * @param t       待验证对象
     */
    void validate(ValidatorContext context, T t);
}

  • ValidatorContext.java

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * 验证器在执行调用过程中的上下文
 * 1.验证器中的数据传递共享
 * 2.验证结果数据缓存以作后续使用
 *
 * @author 会跳舞的机器人
 * @date 2019/12/27
 */
public class ValidatorContext {
    /**
     * 验证器均可以共享使用的属性键值对
     */
    private Map<String, Object> attributes;


    /**
     * 设置属性值
     *
     * @param key   键
     * @param value 值
     */
    public void setAttribute(String key, Object value) {
        if (attributes == null) {
            attributes = new HashMap<>();
        }
        attributes.put(key, value);
    }


    /**
     * 获取String值
     *
     * @param key
     * @return
     */
    public String getString(String key) {

        return (String) getAttribute(key);
    }

    /**
     * 获取Integer值
     *
     * @param key
     * @return
     */
    public Integer getInteger(String key) {
        return (Integer) getAttribute(key);
    }

    /**
     * 获取Boolean值
     *
     * @param key
     * @return
     */
    public Boolean getBoolean(String key) {
        return (Boolean) getAttribute(key);
    }


    /**
     * 获取Long值
     *
     * @param key
     * @return
     */
    public Long getLong(String key) {
        return (Long) getAttribute(key);
    }

    /**
     * 获取BigDecimal值
     *
     * @param key
     * @return
     */
    public BigDecimal getBigDecimal(String key) {
        return (BigDecimal) getAttribute(key);
    }

    /**
     * 获取对象
     *
     * @param key
     * @param <T>
     * @return
     */
    public <T> T getClazz(String key) {
        return (T) getAttribute(key);
    }

    /**
     * 获取属性
     *
     * @param key 键
     * @return 值
     */
    public Object getAttribute(String key) {
        if (attributes != null && !attributes.isEmpty()) {
            return attributes.get(key);
        }
        return null;
    }
}
  • ValidatorElement.java

/**
 * 验证器包装类
 *
 * @author 会跳舞的机器人
 * @date 2019/12/27
 */
public class ValidatorElement {
    /**
     * 待验证对象
     */
    private Object target;

    /**
     * 验证器
     */
    private Validator validator;

    public ValidatorElement(Object target, Validator validator) {
        this.target = target;
        this.validator = validator;
    }

    public Object getTarget() {
        return target;
    }

    public Validator getValidator() {
        return validator;
    }
}

  • ValidatorElementList.java

import java.util.LinkedList;

/**
 * 在FluentValidator内部调用使用的验证器链
 *
 * @author 会跳舞的机器人
 * @date 2019/12/27
 */
public class ValidatorElementList {
    /**
     * 验证器链表
     */
    private LinkedList<ValidatorElement> validatorElementLinkedList = new LinkedList<>();

    /**
     * 将验证器加入链表
     *
     * @param element
     */
    public void add(ValidatorElement element) {
        validatorElementLinkedList.add(element);
    }

    /**
     * 获取验证器链表
     *
     * @return
     */
    public LinkedList<ValidatorElement> getList() {
        return validatorElementLinkedList;
    }

    /**
     * 验证器链表是否为空
     *
     * @return
     */
    public boolean isEmpty() {
        return validatorElementLinkedList.isEmpty();
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer();
        for (ValidatorElement element : validatorElementLinkedList) {
            sb.append("[");
            sb.append(element.getValidator().getClass().getSimpleName());
            sb.append("]->");
        }
        return sb.toString();
    }
}
  • ValidateException.java
/**
 * 校验异常
 *
 * @author 会跳舞的机器人
 * @date 2019/4/4
 */
public class ValidateException extends RuntimeException {
    // 异常码
    private Integer code;

    public ValidateException() {
    }

    public ValidateException(String message) {
        super(message);
    }

    public ValidateException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

}

三、使用样例

  • 校验器定义
@Component
public class CustomerSignValidator implements Validator<String> {

    @Override
    public void validate(ValidatorContext context, String s) {
        // 模拟获取客户信息并进行校验
        Customer customer = new Customer();
        if (1 == 2) {
            throw new ValidateException("校验客户状态失败");
        }
        // 将客户信息存入上下文以便后续使用
        context.setAttribute("customer", customer);
    }
}
  • 使用FluentValidator
// step1.业务规则校验
FluentValidator fluentValidator = FluentValidator.checkAll()
        .on(tradeTimeValidator)  // 判断交易时间
        .on(riskTradeStatusValidator)   // 判断交易风控状态
        .on(tradeValidateBo, productTradeStatusValidator)    // 交易商品状态判断
        .on(tradeValidateBo, tradeCountValidator)    // 判断交易商品数量是否正确
        .on(dto.getCustomerNo(), customerSignValidator)  // 判断客户签约开户状态
        .on(buyerTradeStatusValidator)  // 判断客户交易状态
        .on(tradeValidateBo, entrustOrderValidator)  // 判断委托单是否合法
        .on(tradeValidateBo, buyerBalanceValidator) // 判断买入资金是否足够
        .doValidate();
// 从校验器上下文中获取产生的数据
CustomerVo customerVo = fluentValidator.getContext().getClazz("customer");

以上代码即可实现各个业务规则的校验,校验过程中产生的数据可以存放在context上下文中,后续要用到这部分数据可以直接从context中获取。

实际运行例子如下:

[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:72] - Start to validate,validatorElementList=[TradeTimeValidator]->[RiskTradeStatusValidator]->[ProductTradeStatusValidator]->[TradeCountValidator]->[PriceValidator]->[CustomerSignValidator]->[BuyerTradeStatusValidator]->[BuyerBalanceValidator]->
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - TradeTimeValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - RiskTradeStatusValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - ProductTradeStatusValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - TradeCountValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - PriceValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - CustomerSignValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - BuyerTradeStatusValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:79] - BuyerBalanceValidator is running
[INFO] [http-nio-9006-exec-8] [com.xxxx.validator.FluentValidator:87] - End to validate,time consuming 24 ms

四、总结

以上的校验工具满足了我们当前的需要,我觉得在一般的场景是够用的。另外能想到的扩展场景包括:有需要动态的配置业务校验器的话,可以通过将校验器类名写配置文件中,使用的时候在spring容器中通过类名获取各个校验器实例后,再去执行校验逻辑。如果想要更好的管理各个业务对应的校验器以及校验器的启用与停用状态的话,可以考虑做一个管理界面,在界面中展示某个业务与业务对应的各个校验器,可以开启或者停止校验器。


如果文章对你有帮助的话,给文章点个赞吧。

如果有写得不正确的地方,欢迎指出。

文章首发公众号:会跳舞的机器人,欢迎扫码关注。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342