全链路(方法级)动态Mock - 实现篇

今天主要讲的故事发生在几周之前的一个晚上,S市的H哥给小树打了个电话。直来直去的交流方式,让这个对话持续了甚至没有1分钟,"小树,之前的mock平台不太够用,我们想要能够更细粒度的mock能力,比如针对某个类的某个方法进行mock呢?"秒懂的小树,简单回复了句"我调研下",结束了这次聊天。

Mock是在研发流程中非常常见的一种方法。它通过改变代码逻辑模拟返回想要的结果,解决包括前后端联调,测试场景模拟,隔绝第三方接口调用等问题。然后Mock的形式也有很多种,比如说,通过代码或者配置的方式修改路由到一个额外的服务,通过业务代码改写的方式,通过插件的方式挂载到应用上进行代码增强进行原方法的改写等等。

小树团队,现有的Mock平台采用的是第一种,通过路由到外部服务的方式进行的mock,主要针对是的第三方接口的mock,因此这种方式,只适用于第三方接口调用的场景,想要mock第三方接口外的场景就做不到了。

找寻方案

怎么样能做到方法级别的Mock能力呢。小树,第一个时间想到了之前公司也有这个能力,直接拨打了原先公司这部分的开发团队,一分钟解决聊天对话,"想要做到这个效果,又要业务无感知,那还得是agent"。挂断电话后,小树更加自信,因为原先小树也想到了用agent。然后再往下想想,因为之前调研过各种流量回放的实施方案,接触了JVMSANDBOX-repeater,大体知道是基于JVMSANDBOX开发落地的。所以就去查了JVMSANDBOX相关的信息,了解到它可以通过动态热插拔的方式进行AOP增强,重合度非常高。在检索过程中,搜到了一个基于JVMSANDBOX做的方法级mock平台叫imock的开源工具,简单上去看了下,基本满足诉求。

话不多说,小树,直接做了POC,确实能完成功能上的诉求。能够通过类名,方法名定位方法,用json数据反序列化成方法返回对象,包括异常等。小树心里和眼里满是小火苗。

完善需求

小树直接拨通了H哥的电话,"方法级mock能力POC已经完成,现在需要更加详细的细节描述,方便最后的完整方案设计落地。"电话还是一如既往的高效,"需要针对所有公有,私有方法,接口方法,无感知的接入,可以通过参数条件进行不同种类的mock,可以动态修改mock的配置",H哥说完,小树表示了同意便结束了对话。无意中了解到S哥也有这方面的需求,但是形式上有点区别,S哥不希望直接替换方法执行,想要真实执行出结果后,在结果上进行某些内容的篡改。综合H哥,S哥的需求,加上之前mock平台的能力,以及自己的思考,小树整理出了以下几个点

  1. 【核心能力】通过类/接口,(公有/私有)方法,参数类型进行方法定位。
  2. 【核心能力】通过方法的参数/自定义逻辑分支进行进mock逻辑的编排。
  3. 【核心能力】配置需要实时生效且可以不断的增删改
  4. 【可用性】简化规则配置

设计

这个系统其实整体实现上比较简单, 和imock异曲同工,但是前端这部分以及易用性需要重新来。简单来看下大致的流程

黄色和红色部分就是Mock平台的全部了,在JVMSANDBOX这块具体就不做细说了,这里简单描述下小树的Mock的平台的实现方案里面JVMSANDBOX怎么使用的。在应用上,JVMSANDBOX的部署图如下,

小树选用javaagent(premain)的方式,那说到怎么解决上面的4个需求点就会比较简单水到渠成了。那就展开说下。

1. 通过类/接口,(公有/私有)方法,参数类型进行方法定位。 这个需求点比较容易解决,JVMSANDBOX就是为了解决这个问题才引入的,让全链路AOP成为可能。伪代码如下:

java 复制代码
new EventWatchBuilder(eventWatcher)
         // 具体类
        .onClass(mc.getMockClass())
        // 具体方法
        .onBehavior(mc.getMockMethod())
        // 具体参数类型
        .withParameterTypes(mc.getMockMethodParamTypes())
        .onWatch(new AdviceListener() {
                     // 方法前置拦截
                     @Override
                     protected void before(Advice advice) throws Throwable {
                         super.before(advice);
                         // 是否是方法第一跳
                         if (!advice.isProcessTop()) {
                             return;
                         }
                        // 逻辑处理
                        // ...
                        
                     }
                     // 方法后置拦截
                     @Override
                     protected void afterReturning(Advice advice) throws Throwable {
                         super.afterReturning(advice);
                         // 逻辑处理
                         // ...
                     }
                 }


        );

2. 通过方法的参数/自定义逻辑分支进行进mock逻辑的编排 本需求主要是为了让更细的粒度规则,减少多人在同一个方法拦截位置使用不同的mock逻辑时候产生的干扰。

java 复制代码
@Override
protected void before(Advice advice) throws Throwable {
    super.before(advice);
    if (!advice.isProcessTop()) {
        return;
    }
    // mc.getBefore 方法前置拦截点的规则列表
    for (int i = 0; i < mc.getBefore().size(); i++) {
       if (resolveRule(advice,mc.getBefore().get(i))){
           return;
       }
    }
}

private static boolean resolveRule(Advice advice,MockConfigRuleDTO ruleDTO) {
    boolean conditionMatch = false;
    for (int j = 0; j < ruleDTO.getRules().size(); j++) {
       // 处理规则,并设置conditionMatch =true , 
    }
    if (conditionMatch) {
        // 返回模拟结果
    }
    return conditionMatch;
}

3. 配置需要实时生效且可以不断的增删改 这点需要的是,能够替换AOP的增强能力,这里也依赖了JVMSANDBOX的强大能力。通过JVMSANDBOX的api进行AOP的新增,删除来达到目的。通过debug的方式验证过,这种方案下的adviceListener个数是保持到有且仅有一个,生效的也仅有一个。

java 复制代码
// 缓存配置信息,方便比较是否需要更新aop
public final static Map<String, String> mockConfigMap = new HashMap<>();
// 缓存JVMSANDBOX产生的aop的id,方便后续更新,删除
public final static Map<String, Integer> mockConfigWatchMap = new HashMap<>();

private void initialize(List<MockConfigDTO> configs) {
    try {
        for (final MockConfigDTO mc : configs) {
            // 缓存规则配置信息
            String key = mc.getMockClass() + "." + mc.getMockMethod();
            String value = JSON.toJSONString(mc);

            if (mockConfigMap.containsKey(key)
                    && mockConfigMap.get(key).equals(JSON.toJSONString(mc))) {
                continue;
            }
            EventWatcher ew;
            mockConfigMap.put(key, value);
            // 添加新的规则配置AOP
            ew = genEw(mc);
            // 删除旧版本的规则配置AOP
            if (mockConfigWatchMap.containsKey(key)) {
                eventWatcher.delete(mockConfigWatchMap.get(key));
            }
            mockConfigWatchMap.put(key, ew.getWatchId());
        }

    } catch (Throwable throwable) {
    }
}

4.简化规则配置 往往自定义功能越厉害,配置就会越复杂,然而往往配置越复杂,功能就越没人用,死循环T^T。所以这个工具的开发最大的难点是在如何简化操作。于是小树就要想如何方便化配置,如何获取方法的拦截点,如何获取类信息,如何获取方法名,参数名,如何让配置简单,表达的逻辑又复杂。两个极端的终极碰撞。这里每个细节点的操作化繁为简,都是不小的挑战,如何获取被拦截应用的信息,远程反射获取?太夸张,陈本高不说,还容易造成应用本身性能压力。如果直接爬代码仓库接口,通过ast分析静态文件,读取文件内方法信息,成本不小,对平台管理端cpu的压力不小。就在各种想法涌入的时候,一个吃晚饭的时间,小树突然灵光一闪,"我不是有个,代码覆盖率平台么,那里好像有记录类方法信息。是不是直接...."。检查了下数据以后,小树,眼里又有了光。

小树,三下五除二就开启了数据接口的编排和前端的编写,很快方法信息就很快的做出来,直接可以通过搜索和下拉选择的方式进行获取和填写,如果找不到的还可以通过自己填写的方式设置。除此之外比较复杂的,就要数怎么用结构化数据,表达规则以及规则的返回结果了。

数据结构制定如下,一个方法定义拦截点的mock规则,即包含了规则,也包含了位置,还包含了返回的配置。

json 复制代码
[{
 "rule":[{
     "field": "a.a" , // 规则比较的对象
     "operator": "=" ,// 规则比较的判断符号
     "value": 8,// 规则比较的值
     "required": true,// 是否是必须要满足的条件
 }],
 "position":"after", // 规则适配的拦截未知
 "operator":"update",// 规则命中后,返回的方式
 "data" :{
     "throw":{},// 如果是需要返回异常,返回异常对象
     "return": "",// json字符串
     "update" : [{
         "field": "a.a" ,// 更新结果对象中的目标
         "operator": "=" ,// 更新结果对象的操作方式
         "value": 8 // 结果对象更新结果
     }]
 }
}]

其他,只需要agent定时获取配置平台的拦截点的配置信息,剩下的就交给时间一点点实现完善就好。

结语

小树,计划下周就落地结束并推给团队使用。善于利用开源资源,不但可以节省成本,还能完成能力的实现。本文没有涉及原理,只是比较浅显的应用,之后有机会用到全链路灰度或者日志相关能力的时候,会结合原理再细细理一理。

相关推荐
罗政5 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师6 小时前
spring获取当前request
java·后端·spring
Java小白笔记8 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
JOJO___10 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
白总Server11 小时前
MySQL在大数据场景应用
大数据·开发语言·数据库·后端·mysql·golang·php
Lingbug12 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore
计算机学姐12 小时前
基于SpringBoot+Vue的篮球馆会员信息管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis
好兄弟给我起把狙12 小时前
[Golang] Select
开发语言·后端·golang
程序员大金12 小时前
基于SpringBoot+Vue+MySQL的智能物流管理系统
java·javascript·vue.js·spring boot·后端·mysql·mybatis
ac-er888813 小时前
在Flask中处理后台任务
后端·python·flask