如何开发 Java Agent 插件实现自定义组件 Mock
背景
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](https://my.oschina.net/u/1162528) 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)。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
基于飞桨图学习框架实现的城市地点动态关系挖掘
李双利 飞桨开发者技术专家(PPDE),百度研究院商业智能实验室研究实习生,中国科学技术大学在读博士生。 主要进行时空数据挖掘和图深度学习的相关研究工作。曾获 2021 年百度研究院年度优秀实习生,有多篇基于飞桨完成的论文,发表于 KDD、AAAI 等计算机顶级会议。 周景博 飞桨开发者高级技术专家(高级 PPDE),现任百度研究院商业智能实验室资深研究员。 主要从事数据挖掘和机器学习相关的研究和应用工作,包括时空大数据、深度几何学习、知识图谱和 AI 辅助药物设计等,PaddleSpatial 技术负责人,基于飞桨完成论文多篇,发表于 KDD、AAAI、TKDE 等计算机顶级会议和期刊上。 背景 &概述 研究城市区域的多种动态地点关系具有重要意义。传统的关系预测研究工作大都假设城市中的区域地点关系是静态的,然而在城市区域中用户的行为活动往往是动态变化的,例如人们习惯在午饭时间在餐馆之间作出选择,在晚上则会在酒吧等休息娱乐场所之间进行选择,因此区域地点之间存在动态变化的关联性,不同时间(例如早上和晚上)的城市区域地点关系可能会不同。 研究细粒度的城市区域动态关系对于商业广告...
- 下一篇
函数性能探测:更简单高效的 Serverless 规格选型方案
2019 年 Berkeley 预测 Serverless 将取代 Serverful 计算成为云计算新范式。Serverless 为应用开发提供了一种全新系统架构。借助 2023 年由 OpenAI 所带来的 AIGC 风潮,以阿里云函数计算 FC、AWS Lambda 为代表的 Serverless 以其更高成本效益、更简化的后端代码 & 扩展性及更极致的弹性等众多特性,将开发者从繁重的手动资源管理与性能成本优化中解放,再次激发开发者蓬勃的想象力与创造力。国内越来越多开发者及企业开始尝试如何将 Serverless 应用于实际业务或者场景。 但在优雅使用 Serverless 之前,依旧有不少小问题需要提前解决。由于 Serverless 平台的扩缩容是基于请求处理/事件驱动的并发度进行扩缩容的,对于习惯基于 CPU 指标进行 Pod 水平扩缩的的开发者而言,就会遇到以下难题,比如并发度、最小实例数、最大实例数这几个参数之间的关系是什么样的?又比如单个实例最大并发度怎么设置,才能够符合自己的业务需求? 01 Serverless 参数配置的考量维度 Serverless 能...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19