Java业务校验工具实现方法

一、背景

在我们日常接口开发过程中,可能要面对一些稍微复杂一些的业务逻辑代码的编写,在执行真正的业务逻辑前,往往要进行一系列的前期校验工作,校验可以分为参数合法性校验和业务数据校验。

参数合法性校验比如最常见的校验参数值非空校验、格式校验、最大值最小值校验等,可以通过Hibernate Validator框架实现,本文不具体讲解。业务数据校验通常与实际业务相关,比如提交订单接口,我们可能需要校验商品是否合法、库存是否足够、客户余额是否足够、还有其他的一些风控校验。我们的代码可能看起来像是这样的:

public ApiResult<OrderSubmitVo> 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<String, Object> {
  /**
   * 请求对象
   */
  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 <T>
   * @return
   */
  public <T> T getClazz(String key) {
    return (T) get(key);
  }

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

  public <T> 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<ValidatorTemplateProxy> {
  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<ValidatorTemplateProxy> 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<ValidatorTemplateProxy> getValidatorList(String validateType) {
    List<ValidatorTemplateProxy> validatorTemplateList = new LinkedList<>();
    Map<String, Object> map = SpringUtil.getApplicationContext().getBeansWithAnnotation(Validator.class);
    String[] validateTypes;
    int validateOrder;
    Annotation annotation;
    for (Map.Entry<String, Object> 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<ProductShelfVo> 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<OrderSubmitVo> submitOrder(OrderSubmitDto orderSubmitDto) {
  // 业务校验
  ValidatorContext validatorContext = ValidatorContext.getCurrentContext();
  validatorContext.setRequestDto(orderSubmitDto);
  validateProcessor.validate(ValidateTypeConstant.ORDER_SUBMIT);
  // 从上下文中获取下单商品信息
  List<ProductShelfVo> productShelfVoList = validatorContext.getClazz("productShelfVoList");

  // 后续业务逻辑处理
  return ApiResult.success();
}

通过使用上述封装的校验工具后,业务代码与校验代码解耦,后续要增加/修改业务校验逻辑时候,我们只需要增加/修改相应的校验器即可,不必改动到主业务逻辑。为了我们能更简单和方便找到某个业务逻辑对应所有的校验器,我们在命名校验器的时候可以加上业务类型的前缀。

六、总结

1、在开发过程中,我们遇到一些“烦人”问题的时候,要想办法解决它,而不是忽略不管它,通过解决问题可以提高我们的技术能力。

2、要善于从其他优秀的技术框架学习其实现思路。

3、以上校验工具只是一个简单实现,解决的问题只是笔者在开发过程中遇到的问题,可能并不一定具有通用性。

到此这篇关于Java业务校验工具实现方法的文章就介绍到这了,更多相关Java 业务校验内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈Java 三种方式实现接口校验

    本文介绍了Java 三种方式实现接口校验,主要包括AOP,MVC拦截器,分享给大家,具体如下: 方法一:AOP 代码如下定义一个权限注解 package com.thinkgem.jeesite.common.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import j

  • java身份证合法性校验并提取身份证有效信息

    java身份证合法性校验并获取身份证号有效信息,供大家参考,具体内容如下 java身份证合法性校验 /**身份证前6位[ABCDEF]为行政区划数字代码(简称数字码)说明(参考<GB/T 2260-2007 中华人民共和国行政区划代码>): * 该数字码的编制原则和结构分析,它采用三层六位层次码结构,按层次分别表示我国各省(自治区,直辖市,特别行政区). * 市(地区,自治州,盟).县(自治县.县级市.旗.自治旗.市辖区.林区.特区). 数字码码位结构从左至右的含义是: 第一层为AB两位代码表

  • Java实现的校验银行卡功能示例

    本文实例讲述了Java实现的校验银行卡功能.分享给大家供大家参考,具体如下: 步骤: 首先区分借记卡和信用卡,然后就是校验卡号,最后根据银联Bin确定什么银行,Bin之后就是归属地. 本文所有数据来源于网络,不一定保证正确和完整,这里仅仅作为教学使用. Java代码: package org.luozhuang.bankcard; public class checkBankCard { /* 当你输入信用卡号码的时候,有没有担心输错了而造成损失呢?其实可以不必这么担心, 因为并不是一个随便的信

  • JAVA 18位身份证号码校验码的算法

    public static char doVerify(String id) { char pszSrc[]=id.toCharArray(); int iS = 0; int iW[]={7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}; char szVerCode[] = new char[]{'1','0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'}; int i; for(i=0;i

  • java中文及特殊字符的校验方法

    本文实例为大家分享了Android九宫格图片展示的具体代码,供大家参考,具体内容如下 参考链接:Character.UnicodeBlock中cjk的说明详解 1.关于Character.UnicodeBlock的介绍 CJK的意思是"Chinese,Japanese,Korea"的简写 ,实际上就是指中日韩三国的象形文字的Unicode编码 Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS : 4E00-9FBF:Unicode 编码为 U+

  • 深入了解java-jwt生成与校验

    什么是 JWT 这里是jwt 官方地址,想了解更多的可以在这里查看. jwt 全称是JSON Web Token,从全称就可以看出 jwt 多用于认证方面的.这个东西定义了一种简洁的,自包含的,安全的方法用于通信双方以 json 对象的形式传递信息.其中简洁,安全,传递信息和 web 系统非常契合. jwt 实际上就是一个字符串,由以下三个部分构成(通过.分隔): Header 头部 Payload 负载 Signature 签名 因此一个 jwt 字符串都是如下的形式: Header.Payl

  • Java基于正则实现的日期校验功能示例

    本文实例讲述了Java基于正则实现的日期校验功能.分享给大家供大家参考,具体如下: private void checkDate() throws IOException { // 4种分隔符 String sep = "[-\\./_]"; // 年份 String strPattern = "^(19[4-9]\\d|20\\d{2})" + sep; strPattern += "("; // 月(1,3,5,7,8,10,12) strP

  • Java业务校验工具实现方法

    一.背景 在我们日常接口开发过程中,可能要面对一些稍微复杂一些的业务逻辑代码的编写,在执行真正的业务逻辑前,往往要进行一系列的前期校验工作,校验可以分为参数合法性校验和业务数据校验. 参数合法性校验比如最常见的校验参数值非空校验.格式校验.最大值最小值校验等,可以通过Hibernate Validator框架实现,本文不具体讲解.业务数据校验通常与实际业务相关,比如提交订单接口,我们可能需要校验商品是否合法.库存是否足够.客户余额是否足够.还有其他的一些风控校验.我们的代码可能看起来像是这样的:

  • Java jar打包工具使用方法步骤解析

    java的jar是一个打包工具,用于将我们编译后的class文件打包起来,这里面主要是举一个例子用来说明这个工具的使用. 在C盘下的temp文件夹下面: 有一个com.pack.surfront的package 这个package下面有一些已经class文件如:Test1.class,Test2.class,Test3.class,其中Test1.class下有一个可执行文件. 我们打开cmd,然后cd temp到temp文件夹下面,因为com.pack.surfront是包路径,不需要再进去然

  • java身份证合法性校验工具类实例代码

    1.身份证规则 计算方法(来源百度) 将前面的身份证号码17位数分别乘以不同的系数.从第一位到第十七位的系数分别为:7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2. 将这17位数字和系数相乘的结果相加. 用加出来和除以11,看余数是多少? 余数只可能有0-1-2-3-4-5-6-7-8-9-10这11个数字.其分别对应的最后一位身份证的号码为1-0-X -9-8-7-6-5-4-3-2.(即余数0对应1,余数1对应0,余数2对应X-) 通过上面得知如果余数是3,就会在身份

  • Java身份证号码校验工具类详解

    本文实例为大家分享了Java身份证号码校验工具类的具体代码,供大家参考,具体内容如下 import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.regex.Pattern; public class IdcardVa

  • Java TokenProcessor令牌校验工具类

    关于TokenProcessor令牌校验工具类废话不多说了,直接给大家贴代码了,一切内容就在下面一段代码中,具体代码详情如下所示: public class TokenProcessor { private long privious;// 上次生成表单标识号得时间值 private static TokenProcessor instance = new TokenProcessor(); public static String FORM_TOKEN_KEY = "FORM_TOKEN_KE

  • Java压缩文件工具类ZipUtil使用方法代码示例

    本文实例通过Java的Zip输入输出流实现压缩和解压文件,前一部分代码实现获取文件路径,压缩文件名的更改等,具体如下: package com.utility.zip; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import

  • JAVA正则表达式校验qq号码的方法

    Java 正则表达式 正则表达式定义了字符串的模式. 正则表达式可以用来搜索.编辑或处理文本. 正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别. 正则表达式实例 一个字符串其实就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 "Hello World" 字符串. .(点号)也是一个正则表达式,它匹配任何一个字符如:"a" 或 "1". 下表列出了一些正则表达式的实例及描述: 正则表达式 描述 this is t

  • Java实体映射工具MapStruct使用方法详解

    目录 1.序 2.简单用例 3.使用详解 1)关于接口注解@Mapper几种属性用法详解 2) 其他方法级别注解 总结 1.序 通常在后端开发中经常不直接返回实体Entity类,经过处理转换返回前端,前端提交过来的对象也需要经过转换Entity实体才做存储:通常使用的BeanUtils.copyProperties方法也比较粗暴,不仅效率低下(使用反射)而且仅映射相同名的属性,多数情况下还需要手动编写对应的转换方法实现. 插件MapStruct以接口方法结合注解优雅实现对象转换,MapStruc

  • Java编写超时工具类实例讲解

    我们在开发过程中,在进行时间操作时,如果在规定的时间内完成处理的话,有可能会回到正确的结果.否则,就会被视为超时任务.此时,我们不再等待(不再执行)的时间操作,直接向调用者传达这个任务需要时间,被取消了. 1.说明 java已经为我们提供了解决办法.jdk1.5带来的并发库Future类可以满足这一需求.Future类中重要的方法有get()和cancel().get()获取数据对象,如果数据没有加载,则在获取数据之前堵塞,cancel()取消数据加载.另一个get(timeout)操作表明,如

  • SpringBoot 中使用 Validation 校验参数的方法详解

    目录 1. Validation 介绍 1.1 Validation 注解 1.2 @valid 和 @validated的区别 2. SpringBoot 中使用 Validator 校验参数 2.1 依赖引入 2.2 标注校验实体类 2.3 开启参数校验 2.3.1 简单参数校验 2.3.2 JavaBean 校验 2.4 捕捉参数校验异常 项目中写逻辑时,为保证程序的健壮性,需要对各种参数进行判断,这就导致业务代码不只健壮,还十分臃肿.其实 SpringBoot 中已经提供了 Valida

随机推荐