本文围绕
AREX Agent在 Java 后端工程中的录制与回放主线展开,重点说明一次录制会产出哪些对象、这些对象如何进入Storage Service + MongoDB + Redis、回放阶段又如何把真实下游调用替换为录制结果。分析重点放在数据模型、运行机制与源码入口,而不是停留在功能概览层。
分析对象覆盖
Spring Boot / Spring Cloud / Dubbo / Redis / Caffeine / Elasticsearch / MyBatis等常见 Java 技术栈,并补一个“订单聚合查询服务”的完整案例,把入口请求、依赖 mocker、recordId / replayId、存储协作以及premain -> AgentInitializer -> InstrumentationInstaller的启动主线串成一条完整链路。同时也会补上回放边界:AREX Agent的价值在于验证真实请求下的行为一致性,但回放质量仍然受组件增强范围、动态字段治理和目标环境约束的共同影响。
参考资料:
官方文档:AREX Community Edition 、 Framework Supported 、 Record and Replay Config 、 Forced Record 、 Noise Reduction
官方技术文章:Technical Implementation of AREX Agent 、 AREX Agent Source Code Analysis 、 Full Stack Tracing and Mock Data I/O
扩展开发:AREX Agent 插件开发指南
[TOC]
一、AREX Agent 的问题域与分析对象
梳理 AREX Agent 时,至少需要先回答下面五个问题:
- 一次录制到底录了什么,数据长什么样?
- 这些数据是怎么组织、怎么存、怎么查、怎么在 UI 里看的?
AREX Agent为什么能在不改业务代码的前提下拦到请求和依赖调用?- 录制与回放在源码里分别从哪里切进去?
- 它在
Spring Boot / Dubbo / Redis / Caffeine / Elasticsearch / MyBatis这类 Java 栈里,到底是怎么串成一条完整链路的?
可以先给出一个压缩结论:
AREX Agent本质上不是“流量抓包工具”,也不是“接口平台增强版”,而是一个基于Java Instrumentation + ByteBuddy的运行时增强代理。它把入口请求和依赖调用都包装成统一的录制对象,用recordId串成一条完整 case,在回放时再按同一条 case 的依赖快照去短路真实调用、返回录制结果,最后做差异比对。
所以真正要理解 AREX,重点不是记住“它支持哪些中间件”,而是要看清三层:
- 数据层:录制 case 由哪些对象组成
- 机制层:为什么它能录、能回放
- 源码层:入口、上下文、增强点、存储协作分别落在哪些模块
二、AREX 平台工作流总览
先把 AREX 的完整工作流压缩为一条主链:
- 应用通过
-javaagent挂载arex-agent.jar - JVM 启动时执行
premain - Agent 用
Instrumentation + ByteBuddy给入口类和依赖类织入 Advice - 真实请求进来后,入口 Advice 创建上下文,生成
recordId - 请求执行期间,Redis、Dubbo、MyBatis、HTTP、Elasticsearch、动态类、本地时间等拦截点把自己的请求与响应记录成 mocker
- Agent 把这些录制对象发给
AREX Storage Service - Storage 持久化到
MongoDB,并在回放阶段配合Redis做缓存 - 回放时,调度服务按
recordId取回入口请求并重新发给目标环境 - 同时,目标应用中的 Agent 发现这是回放请求,于是拦截内部依赖调用,直接返回已录制的响应
- 目标应用跑完后返回新的主响应,再和录制期的主响应做比对,生成报告
从这条链路看,AREX 同时解决了三件事:
- 如何拦截
- 如何串联
- 如何替换
其中最关键的是“串联”。
如果不能把入口请求、下游调用、最终响应放进同一条语义链里,就只能得到一堆散落的日志;那样不叫回放,只能叫采样。
AREX 平台组件一览
| 组件 | 职责 | 部署数 |
|---|---|---|
| AREX Java Agent | 挂载到应用 JVM,负责字节码增强、录制和回放 | n(每个被测应用实例一个) |
| Schedule Service | 发起回放请求,调度回放任务并收集响应 | 1 |
| Storage Service | 存储录制数据,回放时按 Mock 方式提供响应 | 1 |
| AREX-API | 为前端 UI 提供所有接口 | 1 |
| AREX-UI | 前端页面,查看录制/回放结果与差异 | 1 |
| MongoDB | 持久化录制数据和配置管理 | 1 |
| Redis | 高速回放缓存 | 1 |
三、AREX 录制数据的组织方式
这是最应该先讲清楚的一层。
初看 AREX 时,很容易把录制结果理解为:
- 一个 HTTP 请求
- 一个 HTTP 响应
但从官方技术文档和源码里的模型定义看,AREX 真实录制的是一组围绕 recordId 组织起来的资源对象。
两个标识:recordId 与 replayId
理解数据结构前,先记住两个标识:
recordId:一次录制 case 的主标识replayId:一次回放执行的主标识
这两个标识分别承担不同职责:
recordId解决“原始基线是谁”replayId解决“这次验证跑的是哪一轮”
一个 recordId 可以被重复回放很多次,因此会对应多个 replayId。
一条 case 是一组资源对象
从 arex-storage 的 README 和 Agent 侧模型命名看,AREX 会把录制资源组织成类似下面的结构:
- 一个主入口对象
MainEntry - 多个依赖调用对象
MockItem / ArexMocker - 回放产生的结果对象
- 差异比较结果对象
换句话说,一条 case 的核心不是“一个 request-response”,而是:
一个入口请求,外加它在执行过程中碰到的所有关键依赖调用快照。
主入口对象 MainEntry
arex-storage README 里给出的 MainEntry 接口大致有这些关键字段:
recordIdreplayIdcreateTimerequestcategoryTypeformatmethodrequestHeaderspath
换成业务视角,可以把这组字段理解为:
- 这是哪条录制
- 这是哪次回放
- 它什么时候生成
- 主入口请求体是什么
- 它属于哪类入口
- 入口格式、方法、请求头、路径分别是什么
也就是说,MainEntry 更像“这次 case 的封面页”。
依赖调用对象 ArexMocker
从源码中的 ArexMocker 看,核心字段大致包括:
idcategoryTypereplayIdrecordIdappIdrecordEnvironmentrecordVersioncreationTimetargetRequesttargetResponseoperationNametagsrequestresponseaccurateMatchKeyfuzzyMatchKeyeigenMap
这组字段基本已经覆盖了依赖 mock 的核心语义。
一个 ArexMocker 可以近似抽象为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"recordId": "AREX-xxx",
"replayId": null,
"categoryType": "REDIS",
"appId": "order-query-service",
"operationName": "RedisTemplate.opsForValue.get",
"targetRequest": {
"body": "base64/compressed request body",
"attributes": {
"key": "order:detail:1001"
}
},
"targetResponse": {
"body": "base64/compressed response body",
"type": "application/json"
},
"creationTime": 1711111111111,
"accurateMatchKey": 123456,
"fuzzyMatchKey": 654321,
"tags": {
"cluster": "prod-sh"
}
}
这里最值得关注的是六个字段:
categoryType:这是哪一类 mock,决定它属于入口、HTTP、Redis、Dubbo、数据库、动态类、时间等哪种资源targetRequest:这次依赖调用的请求快照targetResponse:这次依赖调用的响应快照operationName:这次调用到底是哪个操作recordId:它属于哪条主 casereplayId:这次是否已经进入某轮回放
categoryType 分类速查
| categoryType | 对应组件 | 典型 operationName 示例 |
|---|---|---|
SERVLET / MAIN_ENTRY |
HTTP 入口 | GET /api/orders/detail |
HTTP_CLIENT |
出站 HTTP 调用 | RiskClient.querySummary |
DUBBO |
Dubbo RPC | FulfillmentQueryFacade.query |
DATABASE |
数据库(MyBatis 等) | OrderMapper.selectById |
REDIS |
Redis 操作 | RedisTemplate.opsForValue.get |
DYNAMIC_CLASS |
本地缓存/动态类 | OrderLocalCache.get |
ELASTICSEARCH |
ES 查询 | SearchExtraClient.search |
TIME |
时间 mock | System.currentTimeMillis |
targetRequest 与 targetResponse
真正支撑回放的不是 recordId 本身,而是每个依赖调用都被序列化成了“请求-响应对”。
因为回放时 Agent 要做的并不是“重新执行一遍录制期逻辑”,而是:
- 识别当前内部调用是什么
- 根据当前上下文找到与之匹配的录制对象
- 从
targetResponse里取回原始响应 - 直接返回给业务代码
这也是 AREX 能在回放时避免真实下游调用的根本原因。
accurateMatchKey、fuzzyMatchKey 与 eigenMap
这几个字段很容易被忽略,但它们恰恰说明 AREX 不是简单地按“先来后到”匹配 mock。
这几个字段可以看作匹配层的辅助索引:
accurateMatchKey:偏精确匹配fuzzyMatchKey:偏模糊匹配eigenMap:偏特征值匹配
官方文档在动态类配置里明确提到:
- 先按请求参数做精确匹配
- 找不到再做模糊匹配
- 同签名情况下找不到精确命中,则可能退化到按录制时间选最新结果
这就解释了一个很关键的问题:
AREX 回放不是只靠
recordId把整条依赖链“按顺序播录像”,而是每个依赖点都要基于上下文和请求特征找到最合适的录制数据。
原始 request 与 response 字段
在 ArexMocker 里还有两个容易被忽视的字段:
requestresponse
源码注释写得很直接:
request是原始压缩请求文本response是原始压缩响应文本
而 UI 文档也提到:
- 某些录制的 request/response body 会经过 base64 压缩
- 页面里看到后,必要时需要人工解码或解压才能直观看内容
这意味着:
- 平台侧看到的“请求体、响应体”不一定是明文 JSON
- 存储层会为性能和通用性保留压缩后的原始文本形态
因此排查“AREX 已完成录制,但页面内容看起来像乱码”时,应优先确认是否存在压缩和编码处理,而不是先把问题归因到数据损坏。
四、录制数据的存储方式
默认存储路径:Storage Service + MongoDB
AREX 的默认存储路径不是:
- 业务应用本地磁盘上的一个 case 文件夹
而是:
- Agent 把录制资源发给
AREX Storage Service - Storage 再把录制数据持久化到
MongoDB
这一点在官方 README 里讲得很明确:
MongoDB用于持久化录制数据Redis用于回放阶段缓存
所以更准确地说:
MongoDB存”录制事实”Redis存”回放过程中的临时命中与缓存结果”
存储职责对比
| 维度 | MongoDB | Redis |
|---|---|---|
| 角色 | 权威持久化存储 | 回放高速缓存 |
| 存什么 | 录制 case 全量数据(MainEntry + 各 Mocker) | 本轮回放中的 mock 命中结果与比对中间态 |
| 生命周期 | 默认保留 4 天(TTL 可配) | 随回放任务生灭 |
| 数据量 | 大(文档型半结构化) | 小(热点命中键值) |
为什么适合落到 MongoDB
因为一条 case 本质上是半结构化对象集合:
- 主入口对象字段较固定
- 各类依赖 mocker 都有共性字段
- 但不同组件的
targetRequest、targetResponse内部内容差异很大
例如:
- Redis 更关心 key、命令、值
- MyBatis 更关心 SQL、参数、结果集
- Dubbo 更关心接口名、方法签名、参数列表、返回对象
- Elasticsearch 更关心 DSL 和 hits
这类数据天然更适合文档型存储,而不是硬塞进高度规范化的关系表结构。
TTL 与保留周期
官方 UI 文档特别提到:
- 默认只保留最近 4 天录制 case
- 过期数据会自动删除
- 本质上是通过 Storage Service 侧的 TTL 配置控制
这说明 AREX 默认把录制数据当成:
- 可消费的测试资源
- 而不是长期归档的审计日志
因此如果团队要长期保留某批 case,不能只靠默认配置。
Redis 在回放阶段的职责
Redis 在 AREX 体系中不只是普通缓存,而是回放阶段的性能加速层。
官方文档提到:
- Redis 负责缓存 mock 数据和比较结果
可以把它概括为:
- MongoDB 负责权威存储
- Redis 负责本轮回放的高频命中和中间结果缓存
五、录制数据的查看入口
录制数据最直接的查看入口不是 MongoDB,而是 AREX UI。
在 UI 中查看录制 case
官方流量回放文档给出的查看路径大致是:
- 进入
Replay页面 - 选择已经接入 Agent 的应用
- 进入某个接口
- 再点进某条具体 case
进入 case 详情后,页面会展示两部分信息:
- 左侧:录制过程中的请求内容,包括主入口请求、动态类请求、外部依赖调用请求
- 右侧:对应的响应内容
下图为 AREX Replay 页面录制用例列表截图:

case 详情页的阅读方式
| 页面区域 | 可见内容 | 对应数据模型 |
|---|---|---|
| 左侧调用树 | 主入口请求、Redis/Dubbo/MyBatis/HTTP/动态类等依赖请求 | MainEntry.targetRequest、各 ArexMocker.targetRequest |
| 右侧详情面板 | 选中节点后的响应内容、返回体、差异信息 | targetResponse、回放结果、diff detail |
| 顶部/列表区 | 应用、接口、时间范围、case 列表 | case 维度的检索条件和入口索引 |
graph LR
A[应用与接口列表] --> B[某条 recordId case]
B --> C[左侧调用树]
B --> D[右侧详情面板]
C --> E[主入口请求]
C --> F[Redis/Dubbo/MyBatis]
C --> G[动态类/时间 mock]
D --> H[targetResponse]
D --> I[回放结果]
D --> J[差异详情]
这与前文的数据模型是一一对应的:
- 左边更接近
targetRequest - 右边更接近
targetResponse
UI 中可见的信息层次
通常能看到:
- 主入口请求
- 各依赖调用的请求
- 各依赖调用的响应
- 最终主响应
对于复杂 case,页面呈现的不是单条接口结果,而是一组按依赖展开的调用快照。
从学习角度讲,这一点非常重要,因为它意味着:
从工程使用方式看,AREX 更接近“请求执行剖面快照查看器 + 回放器”,而不是单纯的接口测试平台。
页面内容是否一定是明文
不一定。
官方文档已经注明:
- 部分 request/response body 是压缩后再 base64 编码保存的
这意味着页面中展示的内容有时需要额外解码或解压,才能恢复为可直接阅读的结构。
因此如果要做深度排查,常见顺序通常是:
- 先在 UI 里确认主链路和依赖快照是否齐
- 再看具体 body 是否需要解码或解压
- 如果仍然不清楚,再下沉到存储层或源码层排查
强制录制场景下如何获取 recordId
AREX 支持强制录制一个指定请求。
常见方式是:
- 请求头加
arex-force-record: true
请求成功后,响应头里会返回:
arex-record-id
这个 recordId 就是后续定位这条 case、保存它、回放它的主键。
六、AREX Agent 的录制与回放机制
这部分才是机制核心。
录制能力来自 Java Agent
AREX 的第一性原理不是 HTTP 代理,不是 sidecar,也不是 SDK 埋点,而是:
- 用
-javaagent把 Agent 挂到 JVM 启动过程 - 用
Instrumentation拿到类增强能力 - 用
ByteBuddy给目标类织入 Advice
这意味着它能在”方法真正执行前后”插入自己的逻辑,而不要求业务代码主动调用某个 SDK。
下图展示了 AREX Agent 录制与回放的整体流程(来自官方技术文章):

图中展示了一次请求的调用链:入口(Entry)和依赖调用(Dependencies)通过
RecordId串成一条完整的录制 case。录制时拦截并保存请求/响应;回放时拦截内部调用,直接返回录制期数据。
回放能力来自“拦截后短路真实调用”
它为什么能回放?
因为被织入 Advice 的方法,在运行时不只会判断“要不要录制”,还会判断“当前是不是回放态”。
如果是回放态,Advice 不一定继续执行原始逻辑,而是可以:
- 根据当前上下文找到对应的 mocker
- 取出已录制的
targetResponse - 直接作为方法返回值交还给业务代码
所以回放的本质不是“重新访问下游”,而是:
在应用内部,把原本应该发出去的下游调用拦住,然后用录制期的结果替代。
recordId 如何串起整条链路
因为入口点和内部依赖调用都共享上下文。
AREX 的技术文章里提到:
- 录制过程会通过
recordId把入口和依赖调用串起来 - 对多线程和异步框架,会通过线程增强传递上下文
这实际是在解决一个比“拦截”更难的问题:
- 不是能不能拦到某个 Redis 调用
- 而是怎么知道这次 Redis 调用属于哪条 HTTP 请求
如果没有上下文传播,录到的数据只会是一堆孤立碎片。
多线程与异步传播
因为 Java 业务里很常见:
- 线程池异步查询
CompletableFuture- Reactor
- Dubbo 线程切换
- 回调风格 HTTP 客户端
官方技术文章里提到,AREX 通过线程包装的方式传递上下文。简化理解就是:
1
2
3
4
executor.execute(runnable);
// 被增强后变成近似这样
executor.execute(ContextAwareRunnable.wrap(runnable));
被包装后的 Runnable 在构造时捕获当前上下文,在执行时再把上下文恢复到子线程里。
这一步非常关键,因为没有它,recordId 会在线程切换后丢失。
AREX 支持的线程/异步框架
| 类别 | 支持项 |
|---|---|
| 基础线程 | Thread、ThreadPoolExecutor、ForkJoinTask |
| Future 体系 | FutureTask、FutureCallback、CompletableFuture |
| 响应式 | Reactor Framework |
| RPC 线程切换 | Dubbo Provider/Consumer 线程传播 |
| 异步 HTTP | Apache AsyncClient callback |
时间与本地缓存为何也要录制
如果只录外部调用,不录本地时间和本地缓存,回放成功率会大幅下降。
原因很直接:
- 时间不同,业务分支可能不同
- 本地缓存不同,执行路径可能不同
例如:
- 录制时
Caffeine命中 - 回放时本地缓存为空
- 代码就可能从“直接返回缓存”变成“回源查库”
这样即使数据库 mock 没问题,整条链路的执行顺序也变了。
所以 AREX 才会支持:
- 时间类 mock
- 动态类配置
- 本地缓存相关录制与替换
七、AREX Agent 的启动主线
这一段建议按真正的启动链来读。
下图为 AREX Agent 启动接入流程(来自官方技术文章):

从 CI Pipeline 选择 Agent → 打入启动脚本 → 拉取 agent jar → JVM 调用 premain → 拉配置并按需加载插件 → 字节码增强生效。
启动链关键跳转一览
| 跳转 | 入口类/方法 | 职责 |
|---|---|---|
| 第一跳 | ArexJavaAgent.premain() |
JVM 入口,声明于 MANIFEST.MF |
| 第二跳 | premain → agentmain → init |
执行 installBootstrapJar() 挂入底层 ClassLoader |
| 第三跳 | AgentInitializer.initialize() |
创建 AgentClassLoader、加载扩展 jar、实例化 Installer |
| 第四跳 | InstrumentationInstaller.install() |
用 ByteBuddy AgentBuilder 遍历模块做字节码增强 |
Premain-Class
AREX 是标准 Java Agent,因此打包时会在 MANIFEST.MF 里声明:
Premain-Class
对应的入口类是:
io.arex.agent.ArexJavaAgent
这意味着 JVM 启动应用时,只要看到 -javaagent 参数,就会优先调用这个类的 premain。
premain -> agentmain -> init
官方源码分析文章给出的启动过程是:
premainagentmaininit
也就是说,AREX 的启动入口和普通 Java Agent 一样,先进入 Agent 主类,再进入初始化流程。
Bootstrap ClassLoader 注入
源码分析里提到一个很关键的动作:
installBootstrapJar()
它会把包含 AgentInitializer 的 jar 追加到 Bootstrap ClassLoader 的搜索路径中。
这一层的意义是:
- 某些基础能力必须在更底层类加载器可见
- 否则应用类加载器在运行 Advice 时可能找不到 Agent 需要的类
AgentInitializer.initialize()
AgentInitializer 这层做的事情,从源码可以概括成:
- 初始化日志目录
- 查找
extensions/目录下的扩展 jar - 创建
AgentClassLoader - 记录
Instrumentation和 agent 文件 - 通过自定义类加载器加载
InstrumentationInstaller - 把 agent jar 和 extension jar 交给 Advice 类收集器
- 调用 installer 的
install()
这一层有两个非常重要的设计点:
- 自定义类加载器隔离 Agent 和业务应用依赖
- 扩展 jar 机制允许后续按插件补充增强能力
AgentClassLoader 的隔离意义
因为 Agent 往往依赖很多自己的库,比如:
- ByteBuddy
- 序列化库
- 比对库
- 各类运行时支持类
如果直接和业务应用共用类路径,很容易发生:
- 版本冲突
- 类覆盖
NoClassDefFoundErrorLinkageError
所以 AREX 的做法是:
- Agent 自己用一套类加载器
- 需要给应用使用的 Advice 相关字节码,再按受控方式注入应用类加载器可见范围
这就是为什么官方文章专门把”ClassLoader 隔离与互通”单独拿出来讲。
下图为 AREX Agent ClassLoader 隔离模型(来自官方技术文章):

AgentClassLoader加载 Agent 核心代码,通过ByteBuddy ClassInjector将录制/回放所需字节码注入应用 ClassLoader,从而既保证隔离又保证运行时可见。
八、字节码增强的装配方式
真正把录制和回放逻辑织进目标类的,不是 ArexJavaAgent,而是后面的安装器。
InstrumentationInstaller 是增强总装配器
从源码看,InstrumentationInstaller 做的事情可以概括为:
- 构建
ByteBuddy AgentBuilder - 通过 SPI 加载所有
ModuleInstrumentation - 遍历每个模块里的
TypeInstrumentation - 再遍历具体的
MethodInstrumentation - 把 Advice 织进目标方法
这说明 AREX 的增强机制不是“写死在一个大类里”,而是模块化组织的。
三层抽象:Module / Type / Method
AREX 对增强点做了三层抽象:
ModuleInstrumentationTypeInstrumentationMethodInstrumentation
可以这样理解:
ModuleInstrumentation:定义一个组件模块,例如 servlet、dubbo、redis、mybatisTypeInstrumentation:定义这个模块要匹配哪些类MethodInstrumentation:定义这些类里哪些方法要织入 Advice
三层抽象对照表
| 抽象层 | 职责 | 示例(Servlet 模块) |
|---|---|---|
ModuleInstrumentation |
声明一个完整组件模块 | FilterModuleInstrumentationV3 |
TypeInstrumentation |
指定该模块要匹配的类 | FilterInstrumentationV3(匹配 javax.servlet.Filter) |
MethodInstrumentation |
指定要织入的方法与 Advice | 匹配 doFilter,织入 FilterAdvice |
这种设计很适合做组件矩阵式扩展,因为每新增一个中间件支持,核心不是改一堆 if-else,而是新增一个模块实现。
ByteBuddy 在这里做了什么
从 InstrumentationInstaller 的实现看,核心是这类动作:
- 根据 matcher 找到目标类
- 根据 method matcher 找到目标方法
- 通过
Advice把前置和后置逻辑织进去
最终效果是:
- 方法进入时执行
OnMethodEnter - 方法退出时执行
OnMethodExit - 必要时还能替换、包装或短路原方法行为
这就是 AREX 能录制和回放的技术基础。
下图为 AREX Agent 对 SOA Client 同步调用做字节码增强的示例(来自官方技术文章):

左侧为原始调用逻辑,右侧为 Agent 增强后的逻辑。在
OnMethodEnter阶段判断录制/回放态,录制态保存请求响应,回放态直接从存储取出已录制响应返回。
组件支持为何可以持续扩展
因为 arex-agent 主工程本身就是由很多 instrumentation 模块拼出来的。
从依赖结构看,AREX 把不同组件拆成独立模块,例如:
- servlet
- executors
- mybatis
- redis
- dubbo
- http client
- mongo
- spring cache
- elasticsearch
这意味着“支持一个组件”本质上就是:
- 为这个组件写一套对应的 matcher 和 advice
九、录制阶段的执行主线
入口点创建上下文
以 Servlet 为例,官方源码分析里提到会增强:
javax.servlet.Filter#doFilter
这意味着 HTTP 请求一进应用,AREX 就有机会在最靠前的位置做这些事:
- 清理旧上下文
- 创建新上下文
- 生成 trace / record 关联信息
- 初始化当前请求的时间 mock 状态
因此录制的第一步不是“马上写 MongoDB”,而是先把这次请求纳入一个受控上下文。
依赖调用的前后置拦截
当业务代码继续往下执行时,如果碰到被支持的组件:
- Redis
- MyBatis
- Dubbo
- OpenFeign / HttpClient
- Elasticsearch
- 动态类
- 时间类
对应 Advice 就会在方法前后记录:
- 调用参数
- 调用结果
- 异常信息
- 所属上下文
这些信息最终会被组织成前面说的 ArexMocker。
主响应的补全与落盘
入口点不是只负责开头建上下文,结束时还要把主响应补齐。
这样一条 case 最终才完整:
- 主入口请求
- 所有依赖请求与响应
- 主入口响应
如果缺最后一步,AREX 只能回放依赖,无法做“最终响应差异比对”。
recordId 的串联作用
官方技术文章明确提到:
- 录制过程通过
recordId把入口和依赖调用串起来
所以从语义上说,recordId 不是“这个 HTTP 请求的 ID”这么简单,它更像:
- 一次完整录制快照事务号
录制阶段动作总览
| 阶段 | Agent 在做什么 | 产物 |
|---|---|---|
| 入口进入 | 初始化上下文、创建 recordId、准备时间 mock 状态 |
ArexContext、入口 trace 信息 |
| 依赖执行中 | 在 Redis、Dubbo、MyBatis、HTTP、ES、动态类等点位拦截请求/响应 | 各类 ArexMocker |
| 入口退出 | 记录主响应、收束整条调用链 | MainEntry + 完整 case |
十、回放阶段的执行主线
下图为 AREX UI 中开始回放的操作界面(来自官方文档):

指定目标回放地址(Target Host)、用例时间范围(Case Range),点击 Start Replay 即可触发调度服务按录制 case 逐条回放。
回放入口是重新发送请求,而不是重放本地文件
AREX 的回放不是把 JVM 状态整个倒回去,而是:
- 调度服务拿到某条
recordId - 取回主入口请求
- 把同样的请求重新发到目标验证环境
所以主流程依然是真实执行当前代码。
内部依赖的短路返回
区别就在这里。
当目标环境里的业务代码再次执行到 Redis、Dubbo、MyBatis、Elasticsearch 等依赖点时,Agent 会判断当前请求处于回放态。
如果是回放态,就不会真的执行业务下游调用,而是:
- 根据
recordId + 当前调用特征找 mocker - 命中后取出
targetResponse - 直接把响应对象返回给当前方法
因此从业务代码角度看,它以为自己调了下游;
但从系统真实行为看,这次调用已经被 AREX 短路成内存态或本地态 mock 返回。
主响应与依赖差异比对
最终至少会比两类东西:
- 录制时的主响应
- 回放时当前代码跑出来的主响应
必要时也会结合依赖调用层面的信息做差异分析。
下图为 AREX UI 中回放差异场景聚合视图(来自官方文档):

页面左侧按 Mock 类型和差异类型聚合展示差异场景;右侧可深入对比录制基线与回放结果的具体差异节点,蓝色高亮表示 value diff,橙色高亮表示 additional node。
所以 AREX 关心的核心问题是:
在“同一份入口请求 + 同一套依赖快照”下,新代码的行为和旧基线相比有没有变化。
replayId 的职责
replayId 的作用就是把某一轮回放过程中的:
- 主入口请求
- 内部依赖命中情况
- 新主响应
- 差异结果
串成一组运行结果。
所以:
recordId对应基线replayId对应验证轮次
录制与回放对照表
| 维度 | 录制阶段 | 回放阶段 |
|---|---|---|
| 入口请求 | 来自真实流量 | 来自调度服务按 recordId 重放 |
| 内部依赖调用 | 真实执行并被拦截记录 | 被 Agent 拦截并直接返回录制数据 |
| 下游系统 | 真正访问 | 原则上不真正访问 |
| 产出结果 | 基线 case | 一轮 replayId 对应的执行结果和 diff |
| 关注重点 | 录得全不全、上下文串没串起来 | mock 命中准不准、差异是否真实 |
十一、Java 场景中的完整案例
下面用一个更贴近 Java 业务的场景来把前面的抽象概念落地。
场景定义
假设有个 order-query-service,技术栈如下:
Spring BootSpring Cloud OpenFeignDubboRedisTemplateCaffeineElasticsearch ClientMyBatis
它暴露一个接口:
GET /api/orders/detail?userId=10&orderId=2001
执行链大致是:
- 先查
Caffeine - 未命中查
Redis - 再查
MyBatis - 查
Dubbo履约域 - 查
Feign风控服务 - 查
Elasticsearch搜索补充信息 - 聚合返回订单详情
录制期生成的对象
结构上可以近似理解成下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
recordId = AREX-20260621-0001
MainEntry
- method: GET
- path: /api/orders/detail
- query: userId=10&orderId=2001
- response: OrderDetailVO
Mocker #1
- categoryType: DYNAMIC_CLASS
- operationName: OrderLocalCache.get
- targetRequest: key=user:10:order:2001
- targetResponse: cached order summary
Mocker #2
- categoryType: REDIS
- operationName: RedisTemplate.opsForValue.get
- targetRequest: key=order:detail:2001
- targetResponse: json payload
Mocker #3
- categoryType: DATABASE
- operationName: OrderMapper.selectById
- targetRequest: sql + params
- targetResponse: row data
Mocker #4
- categoryType: DUBBO
- operationName: FulfillmentQueryFacade.query
- targetRequest: orderId=2001
- targetResponse: shipment list
Mocker #5
- categoryType: HTTP_CLIENT
- operationName: RiskClient.querySummary
- targetRequest: userId=10, orderId=2001
- targetResponse: risk summary
Mocker #6
- categoryType: ELASTICSEARCH
- operationName: SearchExtraClient.search
- targetRequest: DSL
- targetResponse: hits/aggregations
从“数据模型”角度看,这条 case 已经足够回答很多问题:
-
为啥能本地复现?
因为依赖快照都在。 -
为啥能避免真实调用下游?
因为每个依赖点都已经有targetResponse。 -
为啥能看出改动影响的是缓存、搜索还是聚合逻辑?
因为主响应之外,还能下钻看到每个依赖点。
回放期的执行过程
假设改动后重新回放:
- 调度服务用原始入口请求再次访问测试环境
- Servlet 入口 Advice 检测到这次是回放
- 业务代码查
Caffeine时,命中动态类 mock 数据 - 查 Redis/MyBatis/Dubbo/Feign/Elasticsearch 时,都由 Agent 返回已录制的
targetResponse - 新代码完成聚合
- 返回新的
OrderDetailVO - 与录制期基线响应做差异比对
例如出现下面这种 diff:
fulfillmentProgress从"2/3"变成null
就可以快速判断:
- 不是下游 Dubbo 变了
- 因为 Dubbo 的 mock 数据本身就是录制期固定快照
- 问题大概率发生在当前服务的新聚合逻辑里
这就是回放验证比普通联调更有价值的地方。
订单查询案例执行链
graph TB
A[GET /api/orders/detail] --> B[Caffeine]
B -->|未命中| C[Redis]
C -->|未命中| D[MyBatis]
D --> E[Dubbo 履约域]
E --> F[Feign 风控服务]
F --> G[Elasticsearch]
G --> H[聚合 OrderDetailVO]
十二、Java 技术栈中的落地风险
官方支持矩阵已经覆盖 Servlet、Dubbo、MyBatis、Redis、Caffeine、Spring Cache、RestTemplate、OpenFeign、WebClient、Elasticsearch Client 等常见组件,但工程落地时仍然需要把“技术栈名称”继续拆到“真实入口、真实客户端、真实线程模型”这一层。
Spring Cloud 场景
只写“用了 Spring Cloud”并不足以判断能否稳定接入。
更关键的是下面这些实现细节:
- 入口是不是
Servlet - 出站 HTTP 是不是
Feign / RestTemplate / WebClient - 熔断、线程隔离是不是改变了上下文传播
所以落地前一定要把生态词拆成组件词。
Dubbo 场景
Dubbo 在很多公司都有自定义过滤器、自定义协议、泛化调用或者线程切换逻辑。
需要重点确认:
- Provider 和 Consumer 哪一侧被拦
- 自定义协议有没有脱离官方默认增强点
- 线程切换后
recordId是否仍能透传
Caffeine 场景
本地缓存和 Redis 最大的区别是:
- 它不天然可共享
- 它更依赖当前 JVM 的运行时状态
所以 AREX 才需要引入动态类配置,用“方法级录制回放”去模拟缓存读取,而不是试图把整块本地内存复制过去。
Elasticsearch 场景
搜索链路里最常见的回归点不是连接失败,而是:
- DSL 改了一个过滤条件
- 排序权重顺序变了
- 高亮字段变了
- hits 后处理逻辑改坏了
AREX 在这里的价值主要体现在“用真实查询样本回放二次加工逻辑”。
Java 技术栈落地风险点速查
| 组件 | 核心风险 | 落地前确认点 |
|---|---|---|
| Spring Cloud | 生态词 ≠ 组件词 | 入口是 Servlet?出站走 Feign/RestTemplate?上下文是否被 Hystrix 线程隔离打断? |
| Dubbo | 线程切换 + 自定义协议 | Provider/Consumer 哪侧拦截?自定义 filter 是否丢 context?泛化调用是否在增强范围? |
| Caffeine | 本地态录制与回放不一致 | 是否配置了动态类?缓存 key 是否稳定?命中/未命中的执行路径差异是否可控? |
| Elasticsearch | DSL 与后处理逻辑 | DSL 变更后排序/过滤/高亮是否影响回放通过率?hits 后处理逻辑能否被 mocker 覆盖? |
| 异步框架 | recordId 跨线程丢失 |
用的是 ThreadPoolExecutor 还是自定义线程?Reactor 版本是否在支持列表? |
十三、回放失效场景与治理重点
理解工作流之后,还需要单独看一组经常影响回放通过率的问题:哪些副作用真正被接管、哪些字段属于噪声、哪些环境差异会让同一条 recordId 失去可比性。
支持矩阵与副作用边界
官方资料强调 AREX 可以验证写接口并避免脏数据,但这一定义成立的前提,是写路径上的关键数据库客户端、缓存客户端、RPC/HTTP 客户端以及动态类访问都已经处在 Agent 的增强范围内。
如果关键副作用经过的是:
- 自定义 SDK
- 原生
Socket/Netty直连逻辑 - 未覆盖的消息生产器
- JNI 或其他绕开常规 Java 客户端的调用
那么回放阶段仍然可能发生真实外部访问。落地时不能只看到“支持写接口验证”这句能力描述,还要逐段核对写路径上的实际组件。
动态字段与比对噪声
AREX 官方文档已经把 replay noise 当作单独能力来处理,这说明“能回放”与“能稳定通过比对”并不是同一件事。最常见的噪声来源包括:
- 时间戳、随机数、UUID、IP
- token、签名串、traceId、sessionId
- Base64 编码节点
- 数组顺序不稳定的 JSON 结构
这类问题通常要结合 Compare Config 做治理,包括:
- 忽略全局或接口级字段
- 忽略特定 category 的比对
- 为 Base64 节点配置解码
- 对无序数组启用无序比较
上下文传播与线程切换
recordId 能否跨线程稳定透传,直接决定依赖快照能否被完整收集。常见风险点包括:
- 自定义线程池包装不在官方支持列表内
- Hystrix、Reactor、回调式客户端引入额外线程切换
- Dubbo 过滤器或业务线程模型重写了上下文
一旦上下文在中途丢失,表象通常不是“请求失败”,而是某一段依赖录制缺失,或者回放时 mock 命中率异常下降。
录制基线与目标环境漂移
AREX 回放的前提不是“复制整个生产环境”,而是在新环境里用同一份入口请求和依赖快照重跑当前代码。因此下面几类漂移仍然会影响结果:
- 配置中心、特性开关、鉴权策略与录制期不一致
- 数据模型、序列化格式或枚举值发生兼容性变化
- 本地缓存 key 规则、SQL 生成逻辑、ES DSL 构造逻辑发生结构性调整
这类问题未必属于代码缺陷,但会直接反映在 diff 报告里,因此需要结合当前分支变更做解释,而不是把所有 diff 都当成业务回归。
回放机部署要求
官方文档明确要求:录制机与回放机都应部署 Agent;如果回放环境未注入 Agent,回放请求会继续访问真实外部系统,产生与录制期不同的副作用。此外,回放机通常应把录制频率设为 0,避免在验证环境重新采集新流量。
官方 FAQ 也明确不建议在生产环境执行 replay。更稳妥的做法始终是:
- 生产环境负责录制
- 测试环境或本地环境负责回放
- 回放前先确认 Agent 状态、支持矩阵和比较配置都已就位
回放失效场景速查
| 类别 | 典型现象 | 诊断与治理重点 |
|---|---|---|
| 组件未覆盖 | 回放时仍访问真实下游 | 核对写路径与读路径上的客户端是否都在支持矩阵内 |
| 噪声字段过多 | diff 大量失败但业务无变更 | 配置 ignore node、Base64 解码、无序数组比较 |
| 上下文传播断裂 | 依赖快照不全、mock 命中率低 | 检查线程池、Reactor、Dubbo 线程切换与自定义包装 |
| 环境漂移 | diff 集中出现在配置/枚举/序列化层 | 对照录制期与回放期的配置中心、开关与数据契约 |
| 回放机配置错误 | 产生真实副作用或重复录制 | 确认回放机已注入 Agent,且录制频率已关闭 |
十四、源码阅读顺序
如果只是泛读仓库,很容易迷路。更高效的顺序通常是:
源码阅读路线总览
| 阶段 | 模块 | 核心关注点 | 回答的问题 |
|---|---|---|---|
| ① 入口与装配 | arex-agent / arex-agent-bootstrap / arex-agent-core |
ArexJavaAgent → AgentInitializer → InstrumentationInstaller |
谁启动?谁装配? |
| ② 运行时上下文 | arex-instrumentation-api |
ArexContext、配置类、运行时模型 |
recordId 怎么传?回放态怎么判? |
| ③ 组件增强模块 | 各 arex-instrumentation-* |
匹配类、拦截方法、enter/exit 逻辑 | 每个组件怎么被录?怎么被回放? |
| ④ 存储协作层 | arex-storage |
MockItem、MainEntry、持久化策略 |
录完发给谁?怎么分层存? |
入口与装配
arex-agentarex-agent-bootstraparex-agent-core
重点理解:
ArexJavaAgentAgentInitializerInstrumentationInstaller
这三层分别回答:
- 谁起进程入口
- 谁做类加载与扩展初始化
- 谁真正把增强模块装配进去
运行时上下文
arex-instrumentation-api
重点看:
ArexContext- 配置类
- 运行时模型
这一层负责解释:
recordId怎么跟着请求走- 回放态怎么判断
- 当前线程上下文怎么保存
具体组件增强模块
按自己最关心的技术栈看对应 instrumentation:
- servlet
- executors
- redis
- mybatis
- dubbo
- http client
- elasticsearch
阅读重点不是把每行代码抠完,而是看三个问题:
- 匹配了哪个类?
- 拦了哪个方法?
- 在 enter/exit 阶段做了什么?
存储协作层
arex-storage
重点看:
MockItemMainEntry- 持久化与缓存职责
这一层负责回答:
- Agent 录完之后把什么对象发给谁
- 这些对象在存储侧如何分层
- 为什么
MongoDB + Redis是默认组合
十五、AREX Agent 的工程化定位
前文可以压缩为几条工程判断:
核心能力不是“录流量”,而是“录执行上下文”
只录入口流量不难,真正难的是把依赖调用、时间、本地缓存、异步线程上下文一起带上。
AREX 的价值,主要就体现在这一层。
核心抽象是围绕 recordId 的 mock 资源集合
一旦理解这一点,很多设计都会变得顺:
- 为什么需要
recordId - 为什么还有
replayId - 为什么有
MainEntry - 为什么依赖调用也要被建模成统一对象
回放质量受制于上下文一致性与噪声治理
真正决定回放质量的,不只是能不能拦到某个组件,而是:
- 上下文能不能跨线程传递
- 本地缓存和时间能不能对齐
- 动态字段能不能降噪
- 依赖调用能不能稳定匹配到正确录制对象
更适合回归验证,不替代全部测试
所以最适合它的使用位置通常是:
- 复杂依赖服务的回归验证
- 线上问题本地复现
- 重要变更前后的真实流量比对
而不是替代:
- 单元测试
- 集成测试
- 压测
- 业务验收
十六、核心结论
回到开头提出的两个关键问题,可以给出更明确的结论。
数据结构、存储与查看路径
可以概括为:
- 结构上:不是单条请求响应,而是
MainEntry + 多个 ArexMocker + replay result + compare result - 主键上:用
recordId串录制基线,用replayId串某轮回放 - 内容上:每个 mocker 至少包含
categoryType、targetRequest、targetResponse、operationName等关键字段 - 存储上:默认由
AREX Storage Service落到MongoDB,并配合Redis做回放缓存 - 查看上:优先在 UI 的
Replay页面看 case 详情,左边请求、右边响应,必要时处理 base64/压缩内容
机制、边界与后续阅读方向
也可以概括为:
- 启动机制:
-javaagent触发premain - 增强机制:
Instrumentation + ByteBuddy - 装配机制:
AgentInitializer + InstrumentationInstaller + SPI 模块化 instrumentation - 录制机制:在入口和依赖调用的 Advice 中记录请求、响应、异常与上下文
- 串联机制:用
recordId和线程上下文把整条链串起来 - 回放机制:在回放态拦截内部依赖调用,直接返回录制期
targetResponse
沿这一主题继续深入时,最值得单独展开的还有三条线:
AREX Agent运行时上下文与多线程传播AREX Agent具体组件增强点逐个拆读AREX Storage的模型、缓存与差异比对链路