背景
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
组件为例,具体介绍下如何开发对应的插件来对它进行录制回放。
环境搭建
- 将 AREX Agent 的代码下载到本地并执行
mvn install:
git clone https://github.com/arextest/arex-agent-java.git
mvn clean install -DskipTests // 这一步是把 arex-agent-java 相干依赖装置到你本地的 maven 仓库,咱们的插件我的项目可能须要援用
- 创立一个一般的 Java 我的项目,这里抉择 IntelliJ IDEA 的 new project:
创立胜利后在 pom 文件中增加 AREX Agent 相干依赖:
<dependencies>
<!-- arex-agent 应用的一些根底类和工具,比方录制回放相干组件,配置信息等 -->
<dependency>
<groupId>io.arex</groupId>
<artifactId>arex-instrumentation-api</artifactId>
<version>${arex.version}</version>
</dependency>
<!-- arex-agent 序列化相干组件 -->
<dependency>
<groupId>io.arex</groupId>
<artifactId>arex-serializer</artifactId>
<version>${arex.version}</version>
</dependency>
<!-- 此处仅为演示应用,理论开发中替换成你们公司须要 Mock 的组件即可 -->
<dependency>
<groupId>com.your.company.dal</groupId>
<artifactId>dalclient</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
如果插件还须要应用 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<TypeInstrumentation> 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.java
和 arex-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<TypeDescription> typeMatcher() {
// 咱们要批改的 DalClient 类门路
return named("com.your.company.dal.DalClient");
}
@Override
public List<MethodInstrumentation> methodAdvices() {ElementMatcher<MethodDescription> 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()));
}
}
留神下面代码中 MethodInstrumentation
、ElementMatcher
、named
、takesArgument
等办法都是 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 模块,外面都是润饰各种中间件的实现。
部署
开发完后能够先测试下本人的插件是否能失常工作,步骤如下:
- 执行
mvn clean compile package
生成插件 jar 包(默认在我的项目的 /target 目录下); - 在
arex-agent-java
我的项目执行mvn clean compile package
命令,生成arex-agent-jar
目录(位于我的项目根目录下); - 在
arex-agent-jar
目录下新建一个extensions
文件夹用来寄存扩大 jar 包; - 把生成的插件 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
相干日志。
调试
如果说插件性能没有失常工作或合乎预期行为,能够从上面两点排查:
- 查看
arex-agent-jar/bytecode-dump
文件夹里的字节码(参考部署章节中的截图)
bytecode-dump
里寄存的是咱们批改后的字节码文件,能够在这里找到润饰前和润饰后的类文件:
将以上两个文件拖至 IDE 间接关上(主动反编译),确认是否润饰胜利,如下方比照图所示(右边是原来的代码逻辑,左边是插件批改后的代码):
- 如果没有批改胜利,能够参考下我的项目 Demo 的源码:https://github.com/arextest/arex-agent-java-extension-demo 或者 https://gitee.com/arextest/arex-agent-java-extension-demo/
- 如果润饰代码胜利但代码不合乎预期行为,能够在 IDE 里用 debug 调试插件的源码,步骤如下:
- 先把
arex-agent-java
我的项目导入到业务我的项目所在的 IDE 中:File → New → Module from Existing Sources…
抉择本地的 arex-agent-java
我的项目,而后再抉择 Maven 即可。
- 将插件我的项目也导入到业务我的项目的 IDE 中,操作同上。
导入胜利后,除了原来的业务我的项目外,还有 arex-agent-java
和 plugin-dal
我的项目源码:
这样就能够在这两个我的项目中打断点来调试 Agent 和插件的代码了,如下图:
总结
以上就是如何通过开发 Agent 插件反对组件 Mock 的全副教程,简略来说就是将返回后果录制下来,而后回放时再依据申请参数匹配到录制时的后果进行回放。
总结起来一共三步:
- 新建一个
DalClientModuleInstrumentation
入口类,arex-agent-java
启动时会加载; - 新建
DalClientInstrumentation
类,通知 AREX Agent 要润饰哪个类,哪个办法,以及执行的机会; - 新建
DalClientAdvice
类,实现真正的录制回放,或是你本人实现的逻辑。
心愿本文档能够帮忙各位开发者进行二次开发,开发过程中遇到问题也随时欢迎与咱们沟通反馈(QQ 交换群:656108079)。