引
今天主要讲的故事发生在几周之前的一个晚上,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平台的能力,以及自己的思考,小树整理出了以下几个点
- 【核心能力】通过类/接口,(公有/私有)方法,参数类型进行方法定位。
- 【核心能力】通过方法的参数/自定义逻辑分支进行进mock逻辑的编排。
- 【核心能力】配置需要实时生效且可以不断的增删改
- 【可用性】简化规则配置
设计
这个系统其实整体实现上比较简单, 和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定时获取配置平台的拦截点的配置信息,剩下的就交给时间一点点实现完善就好。
结语
小树,计划下周就落地结束并推给团队使用。善于利用开源资源,不但可以节省成本,还能完成能力的实现。本文没有涉及原理,只是比较浅显的应用,之后有机会用到全链路灰度或者日志相关能力的时候,会结合原理再细细理一理。