AREX Agent 流量录制与回放

从数据模型、存储协作、字节码增强到源码启动链路,梳理 Java 微服务中的流量回归验证机制

Posted by Ekko on June 11, 2026

本文围绕 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 EditionFramework SupportedRecord and Replay ConfigForced RecordNoise Reduction

官方技术文章:Technical Implementation of AREX AgentAREX Agent Source Code AnalysisFull Stack Tracing and Mock Data I/O

官方仓库:arextest/arex-agent-javaarextest/arex-storage

扩展开发:AREX Agent 插件开发指南

[TOC]


一、AREX Agent 的问题域与分析对象

梳理 AREX Agent 时,至少需要先回答下面五个问题:

  1. 一次录制到底录了什么,数据长什么样?
  2. 这些数据是怎么组织、怎么存、怎么查、怎么在 UI 里看的?
  3. AREX Agent 为什么能在不改业务代码的前提下拦到请求和依赖调用?
  4. 录制与回放在源码里分别从哪里切进去?
  5. 它在 Spring Boot / Dubbo / Redis / Caffeine / Elasticsearch / MyBatis 这类 Java 栈里,到底是怎么串成一条完整链路的?

可以先给出一个压缩结论:

AREX Agent 本质上不是“流量抓包工具”,也不是“接口平台增强版”,而是一个基于 Java Instrumentation + ByteBuddy 的运行时增强代理。它把入口请求和依赖调用都包装成统一的录制对象,用 recordId 串成一条完整 case,在回放时再按同一条 case 的依赖快照去短路真实调用、返回录制结果,最后做差异比对。

所以真正要理解 AREX,重点不是记住“它支持哪些中间件”,而是要看清三层:

  • 数据层:录制 case 由哪些对象组成
  • 机制层:为什么它能录、能回放
  • 源码层:入口、上下文、增强点、存储协作分别落在哪些模块

二、AREX 平台工作流总览

先把 AREX 的完整工作流压缩为一条主链:

  1. 应用通过 -javaagent 挂载 arex-agent.jar
  2. JVM 启动时执行 premain
  3. Agent 用 Instrumentation + ByteBuddy 给入口类和依赖类织入 Advice
  4. 真实请求进来后,入口 Advice 创建上下文,生成 recordId
  5. 请求执行期间,Redis、Dubbo、MyBatis、HTTP、Elasticsearch、动态类、本地时间等拦截点把自己的请求与响应记录成 mocker
  6. Agent 把这些录制对象发给 AREX Storage Service
  7. Storage 持久化到 MongoDB,并在回放阶段配合 Redis 做缓存
  8. 回放时,调度服务按 recordId 取回入口请求并重新发给目标环境
  9. 同时,目标应用中的 Agent 发现这是回放请求,于是拦截内部依赖调用,直接返回已录制的响应
  10. 目标应用跑完后返回新的主响应,再和录制期的主响应做比对,生成报告

从这条链路看,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 组织起来的资源对象。

两个标识:recordIdreplayId

理解数据结构前,先记住两个标识:

  • recordId:一次录制 case 的主标识
  • replayId:一次回放执行的主标识

这两个标识分别承担不同职责:

  • recordId 解决“原始基线是谁”
  • replayId 解决“这次验证跑的是哪一轮”

一个 recordId 可以被重复回放很多次,因此会对应多个 replayId

一条 case 是一组资源对象

arex-storage 的 README 和 Agent 侧模型命名看,AREX 会把录制资源组织成类似下面的结构:

  • 一个主入口对象 MainEntry
  • 多个依赖调用对象 MockItem / ArexMocker
  • 回放产生的结果对象
  • 差异比较结果对象

换句话说,一条 case 的核心不是“一个 request-response”,而是:

一个入口请求,外加它在执行过程中碰到的所有关键依赖调用快照。

主入口对象 MainEntry

arex-storage README 里给出的 MainEntry 接口大致有这些关键字段:

  • recordId
  • replayId
  • createTime
  • request
  • categoryType
  • format
  • method
  • requestHeaders
  • path

换成业务视角,可以把这组字段理解为:

  • 这是哪条录制
  • 这是哪次回放
  • 它什么时候生成
  • 主入口请求体是什么
  • 它属于哪类入口
  • 入口格式、方法、请求头、路径分别是什么

也就是说,MainEntry 更像“这次 case 的封面页”。

依赖调用对象 ArexMocker

从源码中的 ArexMocker 看,核心字段大致包括:

  • id
  • categoryType
  • replayId
  • recordId
  • appId
  • recordEnvironment
  • recordVersion
  • creationTime
  • targetRequest
  • targetResponse
  • operationName
  • tags
  • request
  • response
  • accurateMatchKey
  • fuzzyMatchKey
  • eigenMap

这组字段基本已经覆盖了依赖 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:它属于哪条主 case
  • replayId:这次是否已经进入某轮回放

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

targetRequesttargetResponse

真正支撑回放的不是 recordId 本身,而是每个依赖调用都被序列化成了“请求-响应对”。

因为回放时 Agent 要做的并不是“重新执行一遍录制期逻辑”,而是:

  1. 识别当前内部调用是什么
  2. 根据当前上下文找到与之匹配的录制对象
  3. targetResponse 里取回原始响应
  4. 直接返回给业务代码

这也是 AREX 能在回放时避免真实下游调用的根本原因。

accurateMatchKeyfuzzyMatchKeyeigenMap

这几个字段很容易被忽略,但它们恰恰说明 AREX 不是简单地按“先来后到”匹配 mock。

这几个字段可以看作匹配层的辅助索引:

  • accurateMatchKey:偏精确匹配
  • fuzzyMatchKey:偏模糊匹配
  • eigenMap:偏特征值匹配

官方文档在动态类配置里明确提到:

  • 先按请求参数做精确匹配
  • 找不到再做模糊匹配
  • 同签名情况下找不到精确命中,则可能退化到按录制时间选最新结果

这就解释了一个很关键的问题:

AREX 回放不是只靠 recordId 把整条依赖链“按顺序播录像”,而是每个依赖点都要基于上下文和请求特征找到最合适的录制数据。

原始 requestresponse 字段

ArexMocker 里还有两个容易被忽视的字段:

  • request
  • response

源码注释写得很直接:

  • 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 都有共性字段
  • 但不同组件的 targetRequesttargetResponse 内部内容差异很大

例如:

  • 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

官方流量回放文档给出的查看路径大致是:

  1. 进入 Replay 页面
  2. 选择已经接入 Agent 的应用
  3. 进入某个接口
  4. 再点进某条具体 case

进入 case 详情后,页面会展示两部分信息:

  • 左侧:录制过程中的请求内容,包括主入口请求、动态类请求、外部依赖调用请求
  • 右侧:对应的响应内容

下图为 AREX Replay 页面录制用例列表截图:

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 编码保存的

这意味着页面中展示的内容有时需要额外解码或解压,才能恢复为可直接阅读的结构。

因此如果要做深度排查,常见顺序通常是:

  1. 先在 UI 里确认主链路和依赖快照是否齐
  2. 再看具体 body 是否需要解码或解压
  3. 如果仍然不清楚,再下沉到存储层或源码层排查

强制录制场景下如何获取 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 录制与回放的整体流程(来自官方技术文章):

AREX 录制回放流程

图中展示了一次请求的调用链:入口(Entry)和依赖调用(Dependencies)通过 RecordId 串成一条完整的录制 case。录制时拦截并保存请求/响应;回放时拦截内部调用,直接返回录制期数据。

回放能力来自“拦截后短路真实调用”

它为什么能回放?

因为被织入 Advice 的方法,在运行时不只会判断“要不要录制”,还会判断“当前是不是回放态”。

如果是回放态,Advice 不一定继续执行原始逻辑,而是可以:

  1. 根据当前上下文找到对应的 mocker
  2. 取出已录制的 targetResponse
  3. 直接作为方法返回值交还给业务代码

所以回放的本质不是“重新访问下游”,而是:

在应用内部,把原本应该发出去的下游调用拦住,然后用录制期的结果替代。

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 支持的线程/异步框架

类别 支持项
基础线程 ThreadThreadPoolExecutorForkJoinTask
Future 体系 FutureTaskFutureCallbackCompletableFuture
响应式 Reactor Framework
RPC 线程切换 Dubbo Provider/Consumer 线程传播
异步 HTTP Apache AsyncClient callback

时间与本地缓存为何也要录制

如果只录外部调用,不录本地时间和本地缓存,回放成功率会大幅下降。

原因很直接:

  • 时间不同,业务分支可能不同
  • 本地缓存不同,执行路径可能不同

例如:

  • 录制时 Caffeine 命中
  • 回放时本地缓存为空
  • 代码就可能从“直接返回缓存”变成“回源查库”

这样即使数据库 mock 没问题,整条链路的执行顺序也变了。

所以 AREX 才会支持:

  • 时间类 mock
  • 动态类配置
  • 本地缓存相关录制与替换

七、AREX Agent 的启动主线

这一段建议按真正的启动链来读。

下图为 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

官方源码分析文章给出的启动过程是:

  • premain
  • agentmain
  • init

也就是说,AREX 的启动入口和普通 Java Agent 一样,先进入 Agent 主类,再进入初始化流程。

Bootstrap ClassLoader 注入

源码分析里提到一个很关键的动作:

  • installBootstrapJar()

它会把包含 AgentInitializer 的 jar 追加到 Bootstrap ClassLoader 的搜索路径中。

这一层的意义是:

  • 某些基础能力必须在更底层类加载器可见
  • 否则应用类加载器在运行 Advice 时可能找不到 Agent 需要的类

AgentInitializer.initialize()

AgentInitializer 这层做的事情,从源码可以概括成:

  1. 初始化日志目录
  2. 查找 extensions/ 目录下的扩展 jar
  3. 创建 AgentClassLoader
  4. 记录 Instrumentation 和 agent 文件
  5. 通过自定义类加载器加载 InstrumentationInstaller
  6. 把 agent jar 和 extension jar 交给 Advice 类收集器
  7. 调用 installer 的 install()

这一层有两个非常重要的设计点:

  • 自定义类加载器隔离 Agent 和业务应用依赖
  • 扩展 jar 机制允许后续按插件补充增强能力

AgentClassLoader 的隔离意义

因为 Agent 往往依赖很多自己的库,比如:

  • ByteBuddy
  • 序列化库
  • 比对库
  • 各类运行时支持类

如果直接和业务应用共用类路径,很容易发生:

  • 版本冲突
  • 类覆盖
  • NoClassDefFoundError
  • LinkageError

所以 AREX 的做法是:

  • Agent 自己用一套类加载器
  • 需要给应用使用的 Advice 相关字节码,再按受控方式注入应用类加载器可见范围

这就是为什么官方文章专门把”ClassLoader 隔离与互通”单独拿出来讲。

下图为 AREX Agent ClassLoader 隔离模型(来自官方技术文章):

ClassLoader 隔离模型

AgentClassLoader 加载 Agent 核心代码,通过 ByteBuddy ClassInjector 将录制/回放所需字节码注入应用 ClassLoader,从而既保证隔离又保证运行时可见。


八、字节码增强的装配方式

真正把录制和回放逻辑织进目标类的,不是 ArexJavaAgent,而是后面的安装器。

InstrumentationInstaller 是增强总装配器

从源码看,InstrumentationInstaller 做的事情可以概括为:

  • 构建 ByteBuddy AgentBuilder
  • 通过 SPI 加载所有 ModuleInstrumentation
  • 遍历每个模块里的 TypeInstrumentation
  • 再遍历具体的 MethodInstrumentation
  • 把 Advice 织进目标方法

这说明 AREX 的增强机制不是“写死在一个大类里”,而是模块化组织的。

三层抽象:Module / Type / Method

AREX 对增强点做了三层抽象:

  • ModuleInstrumentation
  • TypeInstrumentation
  • MethodInstrumentation

可以这样理解:

  • ModuleInstrumentation:定义一个组件模块,例如 servlet、dubbo、redis、mybatis
  • TypeInstrumentation:定义这个模块要匹配哪些类
  • 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 状态整个倒回去,而是:

  1. 调度服务拿到某条 recordId
  2. 取回主入口请求
  3. 把同样的请求重新发到目标验证环境

所以主流程依然是真实执行当前代码。

内部依赖的短路返回

区别就在这里。

当目标环境里的业务代码再次执行到 Redis、Dubbo、MyBatis、Elasticsearch 等依赖点时,Agent 会判断当前请求处于回放态。

如果是回放态,就不会真的执行业务下游调用,而是:

  1. 根据 recordId + 当前调用特征 找 mocker
  2. 命中后取出 targetResponse
  3. 直接把响应对象返回给当前方法

因此从业务代码角度看,它以为自己调了下游;
但从系统真实行为看,这次调用已经被 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 Boot
  • Spring Cloud OpenFeign
  • Dubbo
  • RedisTemplate
  • Caffeine
  • Elasticsearch Client
  • MyBatis

它暴露一个接口:

GET /api/orders/detail?userId=10&orderId=2001

执行链大致是:

  1. 先查 Caffeine
  2. 未命中查 Redis
  3. 再查 MyBatis
  4. Dubbo 履约域
  5. Feign 风控服务
  6. Elasticsearch 搜索补充信息
  7. 聚合返回订单详情

录制期生成的对象

结构上可以近似理解成下面这样:

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

  • 为啥能看出改动影响的是缓存、搜索还是聚合逻辑?
    因为主响应之外,还能下钻看到每个依赖点。

回放期的执行过程

假设改动后重新回放:

  1. 调度服务用原始入口请求再次访问测试环境
  2. Servlet 入口 Advice 检测到这次是回放
  3. 业务代码查 Caffeine 时,命中动态类 mock 数据
  4. 查 Redis/MyBatis/Dubbo/Feign/Elasticsearch 时,都由 Agent 返回已录制的 targetResponse
  5. 新代码完成聚合
  6. 返回新的 OrderDetailVO
  7. 与录制期基线响应做差异比对

例如出现下面这种 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 技术栈中的落地风险

官方支持矩阵已经覆盖 ServletDubboMyBatisRedisCaffeineSpring CacheRestTemplateOpenFeignWebClientElasticsearch 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 ArexJavaAgentAgentInitializerInstrumentationInstaller 谁启动?谁装配?
② 运行时上下文 arex-instrumentation-api ArexContext、配置类、运行时模型 recordId 怎么传?回放态怎么判?
③ 组件增强模块 arex-instrumentation-* 匹配类、拦截方法、enter/exit 逻辑 每个组件怎么被录?怎么被回放?
④ 存储协作层 arex-storage MockItemMainEntry、持久化策略 录完发给谁?怎么分层存?

入口与装配

  • arex-agent
  • arex-agent-bootstrap
  • arex-agent-core

重点理解:

  • ArexJavaAgent
  • AgentInitializer
  • InstrumentationInstaller

这三层分别回答:

  • 谁起进程入口
  • 谁做类加载与扩展初始化
  • 谁真正把增强模块装配进去

运行时上下文

  • arex-instrumentation-api

重点看:

  • ArexContext
  • 配置类
  • 运行时模型

这一层负责解释:

  • recordId 怎么跟着请求走
  • 回放态怎么判断
  • 当前线程上下文怎么保存

具体组件增强模块

按自己最关心的技术栈看对应 instrumentation:

  • servlet
  • executors
  • redis
  • mybatis
  • dubbo
  • http client
  • elasticsearch

阅读重点不是把每行代码抠完,而是看三个问题:

  1. 匹配了哪个类?
  2. 拦了哪个方法?
  3. 在 enter/exit 阶段做了什么?

存储协作层

  • arex-storage

重点看:

  • MockItem
  • MainEntry
  • 持久化与缓存职责

这一层负责回答:

  • Agent 录完之后把什么对象发给谁
  • 这些对象在存储侧如何分层
  • 为什么 MongoDB + Redis 是默认组合

十五、AREX Agent 的工程化定位

前文可以压缩为几条工程判断:

核心能力不是“录流量”,而是“录执行上下文”

只录入口流量不难,真正难的是把依赖调用、时间、本地缓存、异步线程上下文一起带上。
AREX 的价值,主要就体现在这一层。

核心抽象是围绕 recordId 的 mock 资源集合

一旦理解这一点,很多设计都会变得顺:

  • 为什么需要 recordId
  • 为什么还有 replayId
  • 为什么有 MainEntry
  • 为什么依赖调用也要被建模成统一对象

回放质量受制于上下文一致性与噪声治理

真正决定回放质量的,不只是能不能拦到某个组件,而是:

  • 上下文能不能跨线程传递
  • 本地缓存和时间能不能对齐
  • 动态字段能不能降噪
  • 依赖调用能不能稳定匹配到正确录制对象

更适合回归验证,不替代全部测试

所以最适合它的使用位置通常是:

  • 复杂依赖服务的回归验证
  • 线上问题本地复现
  • 重要变更前后的真实流量比对

而不是替代:

  • 单元测试
  • 集成测试
  • 压测
  • 业务验收

十六、核心结论

回到开头提出的两个关键问题,可以给出更明确的结论。

数据结构、存储与查看路径

可以概括为:

  • 结构上:不是单条请求响应,而是 MainEntry + 多个 ArexMocker + replay result + compare result
  • 主键上:用 recordId 串录制基线,用 replayId 串某轮回放
  • 内容上:每个 mocker 至少包含 categoryTypetargetRequesttargetResponseoperationName 等关键字段
  • 存储上:默认由 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 的模型、缓存与差异比对链路