乐趣区

关于jvm:通用流量录制回放工具-jvmsandboxrepeater-尝鲜-三-repeater-plugin-开发

本文作者陈恒捷是 TesterHome 社区主编,第十届 MTSC 大会上海站 - 开源专场出品人。先后在 PP 助手、PPmoney、荔枝等公司从事测试效力晋升相干工作,在测试技术及效率晋升方面有丰盛的教训积攒。

在 通用流量录制回放工具 jvm-sandbox-repeater 尝鲜 (二)——repeater-console 应用 中,能够理解到,repeater 的外围还是在 plugin 中,因而有必要去学习下。

相熟 jvm-sandbox

repeater-plugin 的底层波及到 jvm-sandbox 外面的一些原理。须要先浏览下相干的文档:

  • 整体的介绍:https://github.com/alibaba/jv…
  • 具体的 wiki 文档:https://github.com/alibaba/jv…

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 {

    @Override
    protected 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);
    }

    @Override
    protected InvocationProcessor getInvocationProcessor() { // 实现返回 InvocationProcessor 的办法。return new MybatisProcessor(getType()); // 这个插件自身自带 Processor,因而返回插件自带的 Processor
    }

    @Override
    public InvokeType getType() {  // 设定 InvokeType 为 MYBATIS。这个用于标识录制进去的是什么类型的调用。repeater 会依据录制音讯的类型抉择对应的插件进行回放或 mock 
        return InvokeType.MYBATIS; 
    }

    @Override
    public String identity() {  // 设定惟一辨认名称。启动加载插件时会有一个日志打印加载的插件名称,名称即来自于此处。因而须要惟一。return "mybatis";         
    }

    @Override
    public boolean isEntrance() { // 是否入口流量插件。return false;
    }

}
  • MybatisProcessor
class MybatisProcessor extends DefaultInvocationProcessor {MybatisProcessor(InvokeType type) {super(type);
    }
    
    /**
     * 组装标识
     * @param event 从 sandbox 获取到的 BeforeEvent 对象,记录了这个事件的相干信息
     * @return 一个 Identity 对象,作为流量的标识
     */
    @Override
    public 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));
        }
    }

    @Override
    public 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 中 MapperMethodexecute 办法的代码片段吧。

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 大专场,大咖星散精英齐聚

退出移动版