一、背景
在我们日常接口开发过程中,可能要面对一些稍微复杂一些的业务逻辑代码的编写,在执行真正的业务逻辑前,往往要进行一系列的前期校验工作,校验可以分为参数合法性校验和业务数据校验。
参数合法性校验比如最常见的校验参数值非空校验、格式校验、最大值最小值校验等,可以通过Hibernate Validator框架实现,本文不具体讲解。业务数据校验通常与实际业务相关,比如提交订单接口,我们可能需要校验商品是否合法、库存是否足够、客户余额是否足够、还有其他的一些风控校验。我们的代码可能看起来像是这样的:
public ApiResult submitOrder(OrderSubmitDto orderSubmitDto) {
// 业务校验1
// 业务校验2
// 业务校验3
// 业务校验n...
// 执行真正的业务逻辑
return ApiResult.success();
}
二、问题
- 实现不够优雅
上述代码在版本迭代的过程中,还可能陆陆续续增加/修改一些校验逻辑,如果业务逻辑校验的代码都耦合在核心业务逻辑中,这样实现其实是不够优雅,不符合设计原则的单一职责原则和开闭原则。
- 校验代码无法复用
如果某个业务校验代码需要在其他业务中也会用到,那我们则需要将相同的代码复制一份至业务代码中,比如校验用户状态,在很多业务校验中都需要校验,如果校验逻辑有些许更改的话,那么所有涉及到的地方都要同步修改,这样不利于系统维护。
- 校验逻辑无法按照顺序依赖执行,并且校验过程中产生的数据后续获取不便
如果我们将上述代码中的各个校验逻辑封装成独立的子方法,那有可能存在业务校验2要依赖于业务校验1的数据结果,并且在业务校验过程中产生的数据在后续执行真正的业务逻辑的时候是需要用得到的。
三、校验工具实现思路
我们要写的校验工具至少要解决上面所说的三个问题
业务校验代码与核心业务逻辑代码解耦
同一个校验器可以用于多个业务,提高代码的复用性和可维护性
校验代码可以按照指定顺序执行,并且校验过程中产生的数据可以后续传递
在用zuul来做网关服务的时候,我获得了一些灵感,
zuul中的filterType用来区分请求路由到目标之前、处理目标请求、目标请求返回后的类型,filterOrder用来指定过滤器的执行顺序,RequestContext为请求上下文,RequestContext继承自ConcurrentHashMap,且与ThreadLocal绑定保证线程安全,请求上下文中的数据在一次请求的所有过滤器中可以获取,很好的完成了数据传递。
首先我们需要定义一个校验器注解,注解中指定业务类型和执行顺序,在校验器上加上该注解表明这是一个校验器。定义一个校验器上下文,在业务校验执行过程中产生的数据可以通过上下文进行传递。定义一个校验器基类,校验器继承基类,并实现其中的具体校验方法。定义一个校验器的统一执行器,执行器可以根据业务类型找出所有带有校验器注解并且是指定业务类型的校验器列表,根据校验器注解中的执行顺序排序后,遍历所有校验器列表调用校验方法。如果校验过程中校验失败,则抛出校验异常中断业务执行。
以上为大概的实现思路,具体的实现代码如下:
四、show me your code
- Validator.java
import java.lang.annotation.*;
/**
* @author: 会跳舞的机器人
* @date: 2019/10/23 13:58
* @description: 业务校验注解
*/
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Validator {
/**
* 业务类型,同一个校验器可以指定多个业务类型
*
* @return
*/
String[] validateTypes();
/**
* 执行顺序,数值越小越先执行
*
* @return
*/
int validateOrder();
}
Validator校验注解,在校验器的类上加上该注解则表明为业务校验器,validateTypes表示业务类型,同一个校验器可以指定多个业务类型,多个业务类型可以复用同一个校验器,validateOrder表示执行顺序,数值越小越先被执行。
- ValidatorContext.java
import java.util.concurrent.ConcurrentHashMap;
/**
* @author: 会跳舞的机器人
* @date: 2019/9/11 14:56
* @description: 校验器上下文,与当前线程绑定
*/
public class ValidatorContext extends ConcurrentHashMap {
/**
* 请求对象
*/
public Object requestDto;
protected static final ThreadLocal extends ValidatorContext> threadLocal = ThreadLocal.withInitial(() -> new ValidatorContext());
/**
* 获取当前线程的上下文
*
* @return
*/
public static ValidatorContext getCurrentContext() {
ValidatorContext context = threadLocal.get();
return context;
}
/**
* 设值
*
* @param key
* @param value
*/
public void set(String key, Object value) {
if (value != null) put(key, value);
else remove(key);
}
/**
* 获取String值
*
* @param key
* @return
*/
public String getString(String key) {
return (String) get(key);
}
/**
* 获取Integer值
*
* @param key
* @return
*/
public Integer getInteger(String key) {
return (Integer) get(key);
}
/**
* 获取Boolean值
*
* @param key
* @return
*/
public Boolean getBoolean(String key) {
return (Boolean) get(key);
}
/**
* 获取对象
*
* @param key
* @param
* @return
*/
public T getClazz(String key) {
return (T) get(key);
}
/**
* 获取Long值
*
* @param key
* @return
*/
public Long getLong(String key) {
return (Long) get(key);
}
public T getRequestDto() {
return (T) requestDto;
}
public void setRequestDto(Object requestDto) {
this.requestDto = requestDto;
}
ValidatorContext为请求上下文,与当前请求线程绑定,继承自ConcurrentHashMap,requestDto属性为接口请求入参对象,提供get/set方法使得在上下文中能更加便捷的获取请求入参数据。
- ValidatorTemplate.java
/**
* @author: 会跳舞的机器人
* @date: 2019/10/23 11:51
* @description: 校验器模板,业务校验器需继承模板类
*/
@Slf4j
@Component
public abstract class ValidatorTemplate {
/**
* 校验方法
*/
public void validate() {
try {
validateInner();
} catch (ValidateException e) {
log.error("业务校验失败", e);
throw e;
} catch (Exception e) {
log.error("业务校验异常", e);
ValidateException validateException = new ValidateException(ResultEnum.VALIDATE_ERROR);
throw validateException;
}
}
/**
* 校验方法,由子类具体实现
*
* @throws ValidateException
*/
protected abstract void validateInner() throws ValidateException;
}
校验器抽象类,具体的校验器需要继承该类,并且实现具体的validateInner校验方法。
- ValidatorTemplateProxy.java
/**
* @author: 会跳舞的机器人
* @date: 2019/10/25 18:03
* @description: ValidatorTemplate代理类
*/
@Data
@AllArgsConstructor
public class ValidatorTemplateProxy extends ValidatorTemplate implements Comparable {
private ValidatorTemplate validatorTemplate;
private String validateType;
private int validateOrder;
@Override
public int compareTo(ValidatorTemplateProxy o) {
return Integer.compare(this.getValidateOrder(), o.getValidateOrder());
}
@Override
protected void validateInner() throws ValidateException {
validatorTemplate.validateInner();
}
}
ValidatorTemplate类的代理类,实现了Comparable排序接口,便于校验器按照validateOrder属性排序,并且将校验器中的注解转化为代理类中的两个属性字段,方便执行过程中的统一日志打印。
- ValidateProcessor.java
import java.lang.annotation.Annotation;
import java.util.*;
/**
* @author: 会跳舞的机器人
* @date: 2019/10/25 18:02
* @description: 执行器
*/
@Slf4j
@Component
public class ValidateProcessor {
/**
* 执行业务类型对应的校验器
*
* @param validateType
*/
public void validate(String validateType) {
if (StringUtils.isEmpty(validateType)) {
throw new IllegalArgumentException("validateType cannot be null");
}
long start = System.currentTimeMillis();
log.info("start validate,validateType={},ValidatorContext={}", validateType, ValidatorContext.getCurrentContext().toString());
List validatorList = getValidatorList(validateType);
if (CollectionUtils.isEmpty(validatorList)) {
log.info("validatorList is empty");
return;
}
ValidatorTemplateProxy validateProcessorProxy;
for (ValidatorTemplateProxy validatorTemplate : validatorList) {
validateProcessorProxy = validatorTemplate;
log.info("{} is running", validateProcessorProxy.getValidatorTemplate().getClass().getSimpleName());
validatorTemplate.validate();
}
log.info("end validate,validateType={},ValidatorContext={},time consuming {} ms", validateType,
ValidatorContext.getCurrentContext().toString(), (System.currentTimeMillis() - start));
}
/**
* 根据Validator注解的validateType获取所有带有该注解的校验器
*
* @param validateType
* @return
*/
private List getValidatorList(String validateType) {
List validatorTemplateList = new LinkedList<>();
Map map = SpringUtil.getApplicationContext().getBeansWithAnnotation(Validator.class);
String[] validateTypes;
int validateOrder;
Annotation annotation;
for (Map.Entry item : map.entrySet()) {
annotation = item.getValue().getClass().getAnnotation(Validator.class);
validateTypes = ((Validator) annotation).validateTypes();
validateOrder = ((Validator) annotation).validateOrder();
if (item.getValue() instanceof ValidatorTemplate) {
if (Arrays.asList(validateTypes).contains(validateType)) {
validatorTemplateList.add(new ValidatorTemplateProxy((ValidatorTemplate) item.getValue(), validateType, validateOrder));
}
} else {
log.info("{}not extend from ValidatorTemplate", item.getKey());
}
}
Collections.sort(validatorTemplateList);
return validatorTemplateList;
}
}
业务校验的执行器,getValidatorList方法根据validateType值获取所有带有该validateType值的校验器,并将其封装成ValidatorTemplateProxy代理类,然后再做排序。validate为统一的业务校验方法。
- ValidateException.java
/**
* @author: 会跳舞的机器人
* @date: 2019/4/4 6:34 PM
* @description: 校验异常
*/
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;
}
}
ValidateException为校验失败时,抛出的业务校验异常类。
- ValidateTypeConstant.java
/**
* @author: 会跳舞的机器人
* @date: 2019/10/30 15:16
* @description:
*/
public class ValidateTypeConstant {
/**
* 提交订单校验
*/
public static final String ORDER_SUBMIT = "order_submit";
}
ValidateTypeConstant为定义validateType业务校验类型的常量类。
五、使用样例
以订单提交为例,我们首先定义了两个个基本的校验器,下单商品信息校验器、客户状态校验器,均为伪代码实现。
- OrderSubmitProductValidator.java
/**
* @author: 会跳舞的机器人
* @date: 2019/10/30 15:34
* @description: 商品状态以及库存校验
*/
@Component
@Slf4j
@Validator(validateTypes = ValidateTypeConstant.ORDER_SUBMIT, validateOrder = 1)
public class OrderSubmitProductValidator extends ValidatorTemplate {
@Override
protected void validateInner() throws ValidateException {
ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
OrderSubmitDto orderSubmitDto = validatorContext.getRequestDto();
// 获取商品信息并校验商品状态
List productShelfVoList = new ArrayList<>();
if (0 == 1) {
throw new ValidateException("商品已下架");
}
// 将商品信息设置至上下文中
validatorContext.set("productShelfVoList", productShelfVoList);
}
}
- OrderSubmitCustomerValidator.java
/**
* @author: 会跳舞的机器人
* @date: 2019/10/30 19:24
* @description:
*/
@Component
@Slf4j
@Validator(validateTypes = ValidateTypeConstant.ORDER_SUBMIT, validateOrder = 2)
public class OrderSubmitCustomerValidator extends ValidatorTemplate {
@Override
protected void validateInner() throws ValidateException {
ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
String customerNo = validatorContext.getString("customerNo");
if (StringUtils.isEmpty(customerNo)) {
throw new IllegalArgumentException("客户编号为空");
}
// 获取客户信息并校验客户状态
CustomerVo customer = new CustomerVo();
if (0 == 1) {
throw new ValidateException("客户限制交易");
}
}
}
在提交订单的业务逻辑的代码中使用:
/**
* 提交订单
*
* @param orderSubmitDto
* @return
*/
public ApiResult submitOrder(OrderSubmitDto orderSubmitDto) {
// 业务校验
ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
validatorContext.setRequestDto(orderSubmitDto);
validateProcessor.validate(ValidateTypeConstant.ORDER_SUBMIT);
// 从上下文中获取下单商品信息
List productShelfVoList = validatorContext.getClazz("productShelfVoList");
// 后续业务逻辑处理
return ApiResult.success();
}
通过使用上述封装的校验工具后,业务代码与校验代码解耦,后续要增加/修改业务校验逻辑时候,我们只需要增加/修改相应的校验器即可,不必改动到主业务逻辑。为了我们能更简单和方便找到某个业务逻辑对应所有的校验器,我们在命名校验器的时候可以加上业务类型的前缀。
六、总结
1、在开发过程中,我们遇到一些“烦人”问题的时候,要想办法解决它,而不是忽略不管它,通过解决问题可以提高我们的技术能力。
2、要善于从其他优秀的技术框架学习其实现思路。
3、以上校验工具只是一个简单实现,解决的问题只是笔者在开发过程中遇到的问题,可能并不一定具有通用性。
如果文章对你有帮助的话,给文章点个赞吧。
如果有写得不正确的地方,欢迎指出。
文章首发公众号:会跳舞的机器人,欢迎扫码关注。