共计 8041 个字符,预计需要花费 21 分钟才能阅读完成。
一、背景
在前面的文章分享了一篇自已关于 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 容器中通过类名获取各个校验器实例后,再去执行校验逻辑。如果想要更好的管理各个业务对应的校验器以及校验器的启用与停用状态的话,可以考虑做一个管理界面,在界面中展示某个业务与业务对应的各个校验器,可以开启或者停止校验器。
如果文章对你有帮助的话,给文章点个赞吧。
如果有写得不正确的地方,欢迎指出。
文章首发公众号:会跳舞的机器人,欢迎扫码关注。