通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (三)—— repeater plugin 开发
本文作者陈恒捷是TesterHome社区主编,第十届MTSC大会上海站-开源专场出品人。先后在PP助手、PPmoney、荔枝等公司从事测试效能提升相关工作,在测试技术及效率提升方面有丰富的经验积累。
在 通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (二)——repeater-console 使用 中,可以了解到,repeater 的核心还是在 plugin 中,因此有必要去学习下。
熟悉 jvm-sandbox
repeater-plugin 的底层涉及到 jvm-sandbox 里面的一些原理。需要先阅读下相关的文档:
- 整体的介绍:https://github.com/alibaba/jvm-sandbox
- 详细的 wiki 文档:https://github.com/alibaba/jvm-sandbox/wiki
repeater 本身是一种 jvm-sandbox 的 module ,因此重点关注使用者、模块研发者2个章节。由于这部分非本文重点,仅摘要记录和本文关系较大的部分。
- jvm-sandbox 是JVM沙箱容器,一种JVM的非侵入式运行期AOP解决方案。通过抽象 BEFORE、RETURN、THROW 等事件,达到在运行时增强及修改指定的类
- jvm-sandbox 支持 attach 和 javaagent 两种模式。我们前面 repeater 示例用的是 attach
- repeater 属于 jvm-sandbox 的 user_module ,因此放在
${HOME}/.sandbox-module/
下 - sandbox.sh 是沙箱的主要操作客户端,除了我们用过的 attach 命令外,还有包括刷新用户模块(-f)、强制刷新用户模块(-F)、重置(-R)、关闭容器(-S)。以后要停止 attach 可以直接用 -S
- 沙箱自身包含 http 模块,也因此我们的 console 可以通过 http 和沙箱内的 repeater 模块进行通讯(就是 repeater 用户文档里回放方法一提到的传
_data
字段的接口)
强烈建议自己动手完成 wiki 文档中的 模块编写初级,大概15分钟左右即可完成。代码不要复制粘贴,而是自己仿照文档敲出来,这样记忆比较深刻。
同时可以参考 怎么调试啊? 这个 issue ,了解下如何调试 jvm-sandbox 的模块。后续 repeater-plugin 的调试也用得上哦。
repeater-plugin 简介
特别说明,考虑到调研的目标是使用,用到一定程度再考虑更深入的了解。因此暂时先跳过对 repeater-module 及其相关依赖的解析。后续补回。
官方文档未有正式介绍,故根据个人理解,整理一下。
- repeater 本身提供的主要是 jvm 中各个方法入口入参、出参捕获机制,但一个应用内部方法众多,不可能也不需要全部方法的出入都捕获。因此需要进行筛选过滤,也需要根据不同的方法提供不同的实现(如是否支持回放、是否支持 mock )
- 为了便于增加这些支持,通过 plugin 是比较好的方式,实现简单且便于扩展。
- 每个 plugin ,需要完成3件事情:能按照规范标识自己、实现指定入参出参的记录、实现回放(可选)
目前官方已经提供的插件列表如下(截止 20190717):
插件类型 | 录制 | 回放 | Mock | 支持时间 | 贡献者 |
---|---|---|---|---|---|
http-plugin | √ | √ | × | 201906 | zhaoyb1990 |
dubbo-plugin | √ | × | √ | 201906 | zhaoyb1990 |
ibatis-plugin | √ | × | √ | 201906 | zhaoyb1990 |
mybatis-plugin | √ | × | √ | 201906 | ztbsuper |
java-plugin | √ | √ | √ | 201906 | zhaoyb1990 |
redis-plugin | × | × | × | 预期7月底 | NA/NA |
阅读 plugin 源码
同样源码阅读三步骤:明确阅读目的、了解整体架构、细读目标功能
step 0 明确阅读目的
学会 plugin 开发的步骤,并照样画葫芦完成一个 rabbitmq plugin 的设计与开发。
step 1 了解整体架构
先看下各个 plugin 的结构,是否有一些共通特征:
$ tree -L 12 repeater-plugins | grep -v iml | grep -v target
repeater-plugins
├── dubbo-plugin
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── jvm
│ └── sandbox
│ └── repeater
│ └── plugin
│ └── dubbo
│ ├── DubboConsumerPlugin.java
│ ├── DubboProcessor.java
│ ├── DubboProviderPlugin.java
│ └── DubboRepeater.java
├── http-plugin
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── jvm
│ └── sandbox
│ └── repater
│ └── plugin
│ └── http
│ ├── HttpPlugin.java
│ ├── HttpRepeater.java
│ ├── HttpStandaloneListener.java
│ ├── InvokeAdvice.java
│ └── wrapper
├── ibatis-plugin
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── jvm
│ └── sandbox
│ └── repeater
│ └── plugin
│ └── ibatis
│ ├── IBatisPlugin.java
│ └── IBatisProcessor.java
├── java-plugin
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── jvm
│ └── sandbox
│ └── repeater
│ └── plugin
│ └── java
│ ├── JavaEntrancePlugin.java
│ ├── JavaInvocationProcessor.java
│ ├── JavaPluginUtils.java
│ ├── JavaRepeater.java
│ └── JavaSubInvokePlugin.java
├── mybatis-plugin
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── jvm
│ └── sandbox
│ └── repeater
│ └── plugin
│ └── mybatis
│ ├── MybatisPlugin.java
│ └── MybatisProcessor.java
├── pom.xml
├── redis-plugin
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── jvm
│ └── sandbox
│ └── repeater
│ └── plugin
│ └── redis
│ ├── RedisPlugin.java
│ └── RedisProcessor.java
从上面可以看出,基本结构有2个大类。
- 一类是以 mybatis-plugin 为代表的简单插件。只需要实现一个 plugin 类和 processor 类即可。官方的手册示例用的也是这类。
- 一类是以 java-plugin、http-plugin 为代表的复杂插件。除了 plugin 类,还有其它辅助类。
为了便于理解,先从简单的开始。先看看 mybatis-plugin 。
├── mybatis-plugin
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── alibaba
│ └── jvm
│ └── sandbox
│ └── repeater
│ └── plugin
│ └── mybatis
│ ├── MybatisPlugin.java // 实现 InvokePlugin SPI 的类,主要标识了需监听的 java 类,以及插件的一些基础信息(名称、数据类型等)
│ └── MybatisProcessor.java // 实现 InvocationProcessor 接口处理调用的类,主要提供了 Identity 和 request 的组装实现。
好了,我们再看看复杂点的 http-plugin
$ tree -L 12 | grep -v iml | grep -v target
.
├── pom.xml
└── src└── main└── java└── com└── alibaba└── jvm└── sandbox└── repater└── plugin└── http├── HttpPlugin.java // 实现 InvokePlugin SPI 的类,可以理解为整体的入口,类似于 Spring 的 Application├── HttpRepeater.java // 实现 Repeater ,支持回放的核心类├── HttpStandaloneListener.java // 针对 standalone 模式的特别实现,主要是支持 header 透传 traceId ├── InvokeAdvice.java // http 请求感知 interface ,包含同步调用和异步调用└── wrapper├── WrapperAsyncListener.java // AsyncListener 的一个实现,主要用于应对异步请求?├── WrapperOutputStreamCopier.java // 一个输出流复制的类,没什么逻辑,感觉是个工具类├── WrapperRequest.java // HttpServletRequestWrapper 的一种实现类,把 request 变为一个自定义的 servlet ,便于定制实现├── WrapperResponseCopier.java // HttpServletResponseWrapper 的实现类,把 response 变为自定义的 servlet ,便于定制实现└── WrapperTransModel.java // 一个实体类,包含 request、response、url 等,并提供了入参为 WrapperRequest 对象的构造函数。作用未知。
小结一下:
- 每个 plugin 必定有一个 xxPlugin 的类,实现 InvokePlugin SPI 。
- 大部分 plugin 需要有一个 xxProcessor 类,实现 InvocationProcessor 。目前仅有 http-plugin 例外。
- 少量支持回放的插件(入口调用类插件),需要有一个 xxRepeater 的类,提供对应的实现。
step 2 细读目标功能
在上一步可以看到,plugin 和 processor 相对来说是更为普遍的实现方式。因此重点细读这个。
这里以 mybatis-plugin 为代表进行解析。
MybatisPlugin
@MetaInfServices(InvokePlugin.class) // 标明它是一个插件 SPI
public class MybatisPlugin extends AbstractInvokePluginAdapter {@Overrideprotected List<EnhanceModel> getEnhanceModels() { // 定义一个 EnhanceModel ,标记需要监听哪些类的哪些事件EnhanceModel em = EnhanceModel.builder().classPattern("org.apache.ibatis.binding.MapperMethod") // 需监听的类名为 org.apache.ibatis.binding.MapperMethod.methodPatterns(EnhanceModel.MethodPattern.transform("execute")) // 需监听的方法名为 execute.watchTypes(Type.BEFORE, Type.RETURN, Type.THROWS) // 监听的事件。此处监听 BEFORE(刚进入方法,调用实际逻辑前)、RETURN(调用逻辑结束,返回值已就绪,准备向上返回时)、THROWS(发现异常,异常已就绪,准备向上抛出时).build();return Lists.newArrayList(em);}@Overrideprotected InvocationProcessor getInvocationProcessor() { // 实现返回 InvocationProcessor 的方法。return new MybatisProcessor(getType()); // 这个插件本身自带 Processor ,因此返回插件自带的 Processor}@Overridepublic InvokeType getType() { // 设定 InvokeType 为 MYBATIS 。这个用于标识录制出来的是什么类型的调用。repeater 会根据录制消息的类型选择对应的插件进行回放或 mock return InvokeType.MYBATIS; }@Overridepublic String identity() { // 设定唯一识别名称。启动加载插件时会有一个日志打印加载的插件名称,名称即来自于此处。因此需要唯一。return "mybatis"; }@Overridepublic boolean isEntrance() { // 是否入口流量插件。return false;}}
MybatisProcessor
class MybatisProcessor extends DefaultInvocationProcessor {MybatisProcessor(InvokeType type) {super(type);}/*** 组装标识* @param event 从 sandbox 获取到的 BeforeEvent 对象,记录了这个事件的相关信息* @return 一个 Identity 对象,作为流量的标识*/@Overridepublic Identity assembleIdentity(BeforeEvent event) { // 获取触发调用事件的对象。简单的说就是当前被拦截到方法所属于的对象Object mapperMethod = event.target; // SqlCommand = MapperMethod.command// 获取这个对象对应的类中, command 这个 field Field field = FieldUtils.getDeclaredField(mapperMethod.getClass(), "command", true); // 如果获取到的值为 null ,把 location(第二个参数)、endpoint(第三个参数)设为 “Unknown” ,组装 Identity 并返回。if (field == null) { return new Identity(InvokeType.MYBATIS.name(), "Unknown", "Unknown", new HashMap<String, String>(1)); }try {// 获取触发调用事件对象中,“command” 这个 field 对应的对象,并存到变量 command Object command = field.get(mapperMethod); // 分别调用变量 command 的 getName 、getType 方法Object name = MethodUtils.invokeMethod(command, "getName");Object type = MethodUtils.invokeMethod(command, "getType");// 用 type.toString() 作为 location,name.toString() 作为 endpoint ,组装 Identity 并返回return new Identity(InvokeType.MYBATIS.name(), type.toString(), name.toString(), new HashMap<String, String>(1));} catch (Exception e) {// 出现任何异常,把 location(第二个参数)、endpoint(第三个参数)设为 Unknown ,组装 Identity 并返回。return new Identity(InvokeType.MYBATIS.name(), "Unknown", "Unknown", new HashMap<String, String>(1));}}@Overridepublic Object[] assembleRequest(BeforeEvent event) {// MapperMethod#execute(SqlSession sqlSession, Object[] args)// args可能存在不可序序列化异常(例如使用tk.mybatis)// 默认父类提供的实现是返回整个 event.argumentArray ,这里的实现把它改为只返回下标为1的元素,去掉其它元素。从注释上看是为了避免后续 args 会存在不可序列化异常所以想避开它,但从实现上看取的是第2个元素而非第一个参数。原因未知。return new Object[]{event.argumentArray[1]};}
}
简单小结下:
- plugin 主要监听
org.apache.ibatis.binding.MapperMethod
这个类的execute
方法,会监听 BEFORE、AFTER、THROW 三种事件。 - 在执行时,会尝试通过获取
MapperMethod
这个类的对象的 command 属性值,把里面的 type、name 属性加入到流量标识中。否则就用 unknown 填充。
那为何会做上面的操作呢?我们来看看 mybatis 中 MapperMethod
类 execute
方法的代码片段吧。
public class MapperMethod {// 这个就是我们加标识要获取的 command 对象了private final SqlCommand command;private final MethodSignature method;public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new SqlCommand(config, mapperInterface, method);this.method = new MethodSignature(config, mapperInterface, method);}// 这个就是我们要捕获 execute 方法public Object execute(SqlSession sqlSession, Object[] args) {Object result;// command 的 type 代表的是数据库操作类型,对应增删改查,以及 flush 共5种类型switch (command.getType()) { case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}case SELECT:if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {result = executeForMap(sqlSession, args);} else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);} else {Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);}break;case FLUSH:result = sqlSession.flushStatements();break;default:throw new BindingException("Unknown execution method for: " + command.getName());}if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");}return result;}...
从代码中可以看出,这个 execute
起到了承上启下的左右,基本上所有数据库操作都会经过这里,而且也有足够的信息做单次调用的唯一的标识。
正如官方文档所说,只要找对了需要捕获的类,剩下的就顺利了。
但还有1个未解之谜:
1、为何 assembleRequest 要调整返回,改为只返回第二个入参?
官方同学给的答复:
execute 第一个参数是 SqlSession,不需要也不能序列化,对于录制和回放也没有意义。assembleRequest本身也是作为 request 加工使用,有些参数是不一定需要使用的。
开始开发 rabbitmq 的插件
经过上面的解读,开发的方案就比较清晰了。
1、rabbitmq 主要会有2种被调用的情况。一种是生产者,作为子调用生成 mq 信息发到队列中。另一种是消费者,作为入口调用触发后续逻辑。在录制回放中,消费者的场景更为重要,需要优先满足。这种场景下,需要实现回放。
2、需要找到 rabbitmq 作为消费者的承上启下类和方法,并对应 RabbitMqPlugin 。
3、实现对应的 processor 类以及 repeater 类,实现回放。
更详细的,后续完成后再补充。
本文首发于TesterHome社区,点此链接可查看原文并与作者直接交流。
今日份的知识已摄入~
想了解更多前沿测试开发技术:欢迎关注「第十届MTSC大会上海站」>>>
1个主会场+12大专场,大咖云集精英齐聚
通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (三)—— repeater plugin 开发相关推荐
- 通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (二)——repeater-console 使用
本文作者陈恒捷是TesterHome社区主编,第十届MTSC大会上海站-开源专场出品人.先后在PP助手.PPmoney.荔枝等公司从事测试效能提升相关工作,在测试技术及效率提升方面有丰富的经验积累. ...
- 通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (四)——新版带界面 console 的使用
本文作者陈恒捷是TesterHome社区主编,第十届MTSC大会上海站-开源专场出品人.先后在PP助手.PPmoney.荔枝等公司从事测试效能提升相关工作,在测试技术及效率提升方面有丰富的经验积累. ...
- 月光宝盒(vivo流量录制回放平台)正式对外开源
作者:vivo 互联网服务器团队- Liu Yanjiang 月光宝盒是一个基于流量录制回放的自动化测试平台,通过录制回放取代编写脚本进行自动化回归,提升测试效率和覆盖率.因为其解决方案具有很强的通用 ...
- android 录制回放工具,android 自动化录制回放测试工具
uiautomator 做一般的 ui 自动化还是不错的,常见的操作基本都支持,下面是用 uiautomator 做的一个可以录制回放的工具 1, var.txt 用于存放一些参数,如登录用户名,密码 ...
- 使用Python开发测试小工具-录制回放工具的实现
Pyqt5 信号槽机制可参考:https://blog.51cto.com/9291927/2422187 信号槽是Qt的核心机制,也是PyQt编程中对象进行通信的机制.在Qt中,QObject对象和 ...
- 鼠标键盘 录制 可编程宏 /回放 工具 Macro Recorder 汉化 破解 优化
经常需要操作一些简单重复的工作 比如 就需要一些工具来做这些重复的工作来减轻工作量 鼠标键盘录制回放工具 用过很多 都不太理想 (Mouse Recorder 没有相应热键, 速度可调)(Pulov ...
- 流量录制与回放在vivo的落地实践
一.为什么要使用流量录制与回放? 1.1 vivo业务状况 近几年,vivo互联网领域处于高速发展状态,同时由于vivo手机出货量一直在国内名列前茅,经过多年积累,用户规模非常庞大.因此,vivo手机 ...
- 控件获取图像可从几方面取得?_基于图像特征与布局刻画的移动测试脚本跨平台录制回放...
一. 引言 移动应用在全球范围内有着越发举足轻重的地位,因此移动应用的快速迭代和频繁的需求变更的特点引发了对应用质量保障的要求不断提高.在大型设备集群上迁移测试脚本是移动应用质量保障的关键技术之一,因 ...
- iOS录制回放神器AutoTouch使用介绍
今天主要来安利一款iOS录制回放工具AutoTouch的使用. AutoTouch的一个重要的使用前提是手机必须是越狱状态,如果你不打算越狱你的iPhone,那可以暂时忽略这篇文章. AutoTouc ...
最新文章
- 深度学习领域四个不可不知的重大突破
- 求非线性方程组的最小二乘解的广义逆法C实现
- ORA-02291: 违反完整约束条件 - 未找到父项关键字 解决方法
- eclipse中提交git总是要求输入用户名、密码
- 【prometheus API】删除指定指标数据
- list赋值给另一个list_Python小知识: List的赋值方法,不能直接等于
- python异常处理结构_python-异常处理
- 几何与代数(1)知识框架(出题根据)
- python程序怎么修改_python文件如何修改
- linux Pci字符驱动基本加载流程
- Bootstrap 排版正文
- 【干货】腾讯内部-微信视频号介绍、商业玩法及涨粉方案.pdf(附下载链接)...
- 阿里巴巴 CTO 首次分享技术战略
- 通过自动化接口调用InstallShield 报错的解决办法
- 21-04-08 cms日志分析
- 浏览器全球的书签都在这里了,看看有没有你的!
- java web欢迎页 主页 设置为servlet的方法
- 如何在Mac上快速显示桌面以便快速访问
- Excel批量调整图片大小适应单元格且整齐排列
- 我做的一个Android 下的PDF书架阅读APP
热门文章
- ec,easyclick常用函数大全,集合1
- 百度SEO:如何进行网站关键字挖掘!
- 女生适合开发还是测试?该如何选择?
- 【Java】917. 仅仅反转字母---使用双指针移动
- linux mor命令使用技巧,linux中more命令如何使用(示例代码)
- 使用Flink对hudi MOR表进行离线压缩
- css3对页面打印设置的一些特殊属性,如@page,target-counter等
- [PWN]/瑞士军刀
- 《乔布斯传》经典摘录(七)
- 新西兰计算机预科学费多少钱,留学新西兰预科学费