AREX Java Agent 插件开发指南

背景

AREX 是一款开源的基于真实请求与数据的自动化回归测试平台,利用 Java Agent 技术与比对技术,通过流量录制回放能力实现快速有效的回归测试。

AREX Agent 项目(arex-agent-java) 现在已经支持了大部分开源组件的 Mock,但对某些公司内部完全自研或是基于开源组件做了修改的基础组件还暂不支持,回放时可能会产生预期外的差异,针对这种问题,可以使用插件的形式对 AREX Agent 进行扩展,其他需要扩展或增强的场景类似。

以下面的代码为例,假设公司内部有个自己研发的数据库访问层组件:DalClient

Object result = dalClient.query(key)
if (result != null) {
    return result; // 录制时数据库有值
} else {
    return rpc.search(); // 回放时没值去调用了远程接口
}

这种场景下虽然可以使用动态类方法 Mock,但动态类支持相对比较简单,且需要配置成本。

使用插件扩展的方式对 DalClient 组件进行 Mock,可以更灵活地自定义录制回放逻辑,适合复杂的场景。

下面我们就以 DalClient 组件为例,详细介绍下如何开发对应的插件来对它进行录制回放。

环境搭建

  1. AREX Agent 的代码下载到本地并执行 mvn install:
git clone https://github.com/arextest/arex-agent-java.git 
mvn clean install -DskipTests //这一步是把 arex-agent-java 相关依赖安装到你本地的 maven 仓库,我们的插件项目可能需要引用
  1. 创建一个普通的 Java 项目,这里选择 IntelliJ IDEA 的 new project:

创建成功后在 pom 文件中添加 AREX Agent 相关依赖:


             
    
        io.arex
        arex-instrumentation-api
        ${arex.version}
    
             
    
        io.arex
        arex-serializer
        ${arex.version}
    
    
    
        com.your.company.dal
        dalclient
        1.0.0
        provided
    

如果插件还需要使用 arex-agent-java 项目其他功能,也可以视情况自己添加依赖(记得先执行第一步操作)。

开发

步骤一:新建 DalClientModuleInstrumentation 入口类

项目搭建好后就可以开始开发了,arex-agent-java 是以 SPI 的方式加载和实例化插件的,所以这里基于 com.google.auto.service.AutoService 注解声明一个 DalClientModuleInstrumentation 类,该类是实现修饰 DalClient 组件的入口,会被 arex-agent-java 识别到( @AutoService

@AutoService(ModuleInstrumentation.class)
public class DalClientModuleInstrumentation extends ModuleInstrumentation {
    public DalClientModuleInstrumentation() {
       // 插件模块名,如果你的DalClient组件不同的版本之间代码差异比较大,且要分版本支持的话,可以指定不同的version匹配:
       // ModuleDescription.builder().name("dalclient").supportFrom(ComparableVersion.of("1.0")).supportTo(ComparableVersion.of("2.0")).build();
       super("plugin-dal");
    }
 
    @Override
    public List instrumentationTypes() {
          // 我们真正去修饰DalClient字节码的类
      return singletonList(new DalClientInstrumentation()); 
    }
}

这里说明下什么情况下需要区分版本号:

假如 DalClient 组件 1.0.0 版本的源码里有 invoke() 方法,但是 2.0.0 版本的代码里名字改成了 invokeAll() ,也就是说 DalClient 组件在 1.0.0、2.0.0 两个版本之间存在差异,这样的话 Agent 插件修饰的代码无法同时覆盖两个版本的 DalClient 框架,这种情况下就可能需要针对不同的版本做适配。

具体实现可以参考 arex-agent-java 项目的 arex-instrumentation/dubbo/ 模块的 arex-dubbo-apache-v2/DubboModuleInstrumentation.javaarex-dubbo-apache-v3/DubboModuleInstrumentation.java 里适配不同的 Dubbo 版本逻辑。

当然如果你修饰的框架源码在不同的版本里都一样的话,就可以不用区分。

版本号匹配的实现原理是根据你要 Mock 的组件的 jar 包中 META-INF/MANIFEST.MF 文件内容判断的:

步骤二:实现字节码修饰作用

下面新建 DalClientInstrumentation.java 文件,实现具体的字节码修饰逻辑。

修改 DalClient 源码的原理很简单,即找到它底层实现的方法,最好是通用的 API,然后使用 bytebuddy 等字节码工具修改这个 API,添加我们自己的代码,实现 Mock 功能。

以下是我们要修改的 DalClient 源码:

package com.your.company.dal;
 
public class DalClient {
    public Object query(String param) {
        return this.invoke(DalClient.Action.QUERY, param);
    }
 
    public Object insert(String param) {
        return this.invoke(DalClient.Action.INSERT, param);
    }
 
    private Object invoke(Action action, String param) {
      Object result;         
        switch (action) {
            case QUERY:
                result = "query:" + param;
            case INSERT:
                result = "insert:" + param;
            case UPDATE:
                result = "update:" + param;
            default:
                result = "unknown action:" + param;
        }     
        return result;
    }
 
    public static enum Action {
        QUERY,
        INSERT,
        UPDATE;
        private Action() {
        }
    }
}

平时业务项目里就是通过 dalClient.query(key) 的方式调用,通过上方源码可以看到底层都是调用 invoke 实现的。

所以就可以通过修改 invoke 方法,添加录制和回放代码,即 DalClientInstrumentation 类的功能如下:

public class DalClientInstrumentation extends TypeInstrumentation {
    @Override
    public ElementMatcher typeMatcher() {         
               // 我们要修改的 DalClient 类路径
               return named("com.your.company.dal.DalClient"); 
    }
 
    @Override
    public List methodAdvices() {
        ElementMatcher matcher = named("invoke") // 我们要修改的方法
                .and(takesArgument(0, named("com.your.company.dal.DalClient$Action"))) // 这个方法的第一个参数类型,可能有同名方法,便于区分
                .and(takesArgument(1, named("java.lang.String"))); 
                       // InvokeAdvice 类是我们在 invoke 方法里需要添加的代码         
                       return singletonList(new MethodInstrumentation(matcher, InvokeAdvice.class.getName())); 
    }
}

注意上面代码中 MethodInstrumentationElementMatchernamedtakesArgument 等方法都是 ByteBuddy 的 API,AREX Agent 默认使用 ByteBuddy(https://bytebuddy.net/) 修饰字节码实现录制和回放,详细用法可以参考官方文档:https://bytebuddy.net/#/tutorial

步骤三:实现录制回放

下面是要添加到 DalClient#invoke 方法中的代码,实现 InvokeAdvice

public static class InvokeAdvice {
    // OnMethodEnter 表示被修改的方法(invoke)逻辑调用前执行的操作
    @Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class, suppress = Throwable.class)
    public static boolean onEnter(@Advice.Argument(0) DalClient.Action action, // 获取被修饰方法的第一个参数引用
                                  @Advice.Argument(1) String param, // 获取被修饰方法的第二个参数引用
                                  @Advice.Local("mockResult") MockResult mockResult) { // 我们在该方法内自定义的变量 mockResult
        mockResult = DalClientAdvice.replay(action, param); // 回放
        return mockResult != null && mockResult.notIgnoreMockResult();
    }
 
    // OnMethodExit 表示被修改的方法 (invoke) 结束前执行的操作
    @Advice.OnMethodExit(suppress = Throwable.class)   
    public static void onExit(@Advice.Argument(0) DalClient.Action action,
                              @Advice.Argument(1) String param,
                              @Advice.Local("mockResult") MockResult mockResult,
                              @Advice.Return(readOnly = false) Object result) { 
        // 方法的返回结果 result
        if (mockResult != null && mockResult.notIgnoreMockResult()) {
            result = mockResult.getResult(); // 使用回放的结果
            return;
        }
        DalClientAdvice.record(action, param, result); // 录制逻辑
    }
}

这个类的功能就是在修改的 invoke 方法调用前后添加代码,实现录制和回放的功能。

其中:

skipOn = Advice.OnNonDefaultValue 参数表示如果 mockResult != null && mockResult.notIgnoreMockResult()true 时(非默认值,boolean 类型默认值是 false)则跳过方法原来的逻辑,即不执行原方法逻辑而直接返回我们 Mock 的值。如果是 false 则执行方法原有的逻辑。

修改后的字节码如下所示:

public class DalClientInstrumentation extends TypeInstrumentation {
    private Object invoke(DalClient.Action action, String param) {
               // 回放          
               MockResult mockResult = DalClientAdvice.replay(action, param);
        if (mockResult != null && mockResult.notIgnoreMockResult()) {
            return mockResult.getResult();
        }
 
        // 原来的逻辑
        Object result;
        switch (action) {
            case QUERY:
                result = "query:" + param;
            case INSERT:
                result = "insert:" + param;
            case UPDATE:
                result = "update:" + param;
            default:
                result = "unknown action:" + param;
        }
 
        DalClientAdvice.record(action, param, result); // 录制
 
        return result;
    }
}

类似于 AOP 的功能,分别在调用前后插入我们的代码,如果回放成功则返回 Mock 的结果,不走原来的逻辑,如果不回放,即需要录制,则在 return 前先录制结果。

另外 DalClientAdvice 类的代码如下(仅供参考):

public class DalClientAdvice {
    // 录制
    public static void record(DalClient.Action action, String param, Object result) {
        if (ContextManager.needRecord()) {
            Mocker mocker = buildMocker(action, param);
            mocker.getTargetResponse().setBody(Serializer.serialize(result));
            MockUtils.recordMocker(mocker);
        }
    }
 
    // 回放
    public static MockResult replay(DalClient.Action action, String param) {
        if (ContextManager.needReplay()) {
            Mocker mocker = buildMocker(action, param);
            Object result = MockUtils.replayBody(mocker);
            return MockResult.success(result); DalClientInstrumentation          }
        return null;
    }
 
    private static Mocker buildMocker(DalClient.Action action, String param) {
        Mocker mocker = MockUtils.createDatabase(action.name().toLowerCase());
        mocker.getTargetRequest().setBody(param);
        return mocker;
    }
}

以上只是简单的 Demo,你也可以自己实现逻辑,具体用法可以参考 arex-agent-java 项目的 arex-instrumentation 模块,里面都是修饰各种中间件的实现。

部署

开发完后可以先测试下自己的插件是否能正常工作,步骤如下:

  1. 执行 mvn clean compile package 生成插件 jar 包(默认在项目的/target目录下);
  2. arex-agent-java 项目执行 mvn clean compile package 命令,生成 arex-agent-jar 目录(位于项目根目录下);
  3. arex-agent-jar 目录下新建一个 extensions 文件夹用来存放扩展 jar 包;
  4. 把生成的插件 jar 包(如:plugin-dal-1.0.0.jar)放到之前创建的 extensions 文件夹里。

AREX Agent 在启动时会加载 extensions 文件夹下的所有 jar 作为扩展功能,目录结构如下:

接下来就可以调试我们的插件功能了,启动你的业务应用,注意要先在 VM option 里挂载 Agent:

详细参数如下:

-javaagent:你自己本地项目的\arex-agent-java\arex-agent-0.3.8.jar
-Darex.service.name=服务名能区分即可
-Darex.storage.service.host=arexstorage服务的ip:port
-Darex.enable.debug=true

这样启动项目后 Agent 就能加载插件包。

如果业务项目引用了公司内部的 DalClient 组件,启动项目后在控制台就可以看到 DalClientModuleInstrumentation 里声明的插件名:

如果控制台输出了 [arex] installed instrumentation module: plugin-dal 日志,就表示已经识别到并加载了插件。

接下来测试插件是否能正常工作。如下所示,业务项目中某个接口调用了 DalClient 组件的 query 方法,那么可以通过调用这个接口,观察插件是否能录制或回放 DalClient 组件的 invoke 方法(query 内部调用的是 invoke),如果能录制成功则会打印下面的日志:

同样地,如果测试回放功能,请求头里加上 arex-record-id:AREX-10-32-179-120-2126724921(图中录制时生成的 recordId),回放成功后也会在控制台输出 [arex]replay category: Database, operation: query 相关日志。

调试

如果说插件功能没有正常工作或符合预期行为,可以从下面两点排查:

  1. 查看 arex-agent-jar/bytecode-dump 文件夹里的字节码(参考部署章节中的截图)

bytecode-dump 里存放的是我们修改后的字节码文件,可以在这里找到修饰前和修饰后的类文件:

将以上两个文件拖至 IDE 直接打开(自动反编译),确认是否修饰成功,如下方对比图所示(左边是原来的代码逻辑,右边是插件修改后的代码):

  1. 先把 arex-agent-java 项目导入到业务项目所在的 IDE 中:File → New → Module from Existing Sources...

选择本地的 arex-agent-java 项目,然后再选择 Maven 即可。

  1. 将插件项目也导入到业务项目的 IDE 中,操作同上。

导入成功后,除了原来的业务项目外,还有 arex-agent-javaplugin-dal 项目源码:

这样就可以在这两个项目中打断点来调试 Agent 和插件的代码了,如下图:

总结

以上就是如何通过开发 Agent 插件支持组件 Mock 的全部教程,简单来说就是将返回结果录制下来,然后回放时再根据请求参数匹配到录制时的结果进行回放。
总结起来一共三步:

  1. 新建一个 DalClientModuleInstrumentation 入口类,arex-agent-java 启动时会加载;
  2. 新建 DalClientInstrumentation 类,告诉 AREX Agent 要修饰哪个类,哪个方法,以及执行的时机;
  3. 新建 DalClientAdvice 类,实现真正的录制回放,或是你自己实现的逻辑。

希望本文档可以帮助各位开发者进行二次开发,开发过程中遇到问题也随时欢迎与我们沟通反馈(QQ 交流群:656108079)。

你可能感兴趣的:(javaagent测试开源)