本文目录导读:

在Java中实现“服务录制”(Service Recording/Replay)通常有两种含义:
- 录制HTTP请求:类似商业API网关(如阿里云API、Kong)或工具(如GoReplay)的功能——记录生产上真实的HTTP请求流量,然后在测试环境重放。
- 录制方法调用:录制某个Java对象/服务的所有方法调用(入参和返回值),用于测试(Mock)或调试。
下面分别给出这两种场景的常见实现方案和核心思路。
录制HTTP API请求(流量录制与回放)
这是最典型的“服务录制”需求,目标是:将发往A服务的请求全部复制一份存下来,之后能在B服务或本地调试时重放这些请求。
核心架构(基于Filter/Interceptor与消息队列)
客户端 -> 网关/反向代理
|
├── 正常业务请求 -> 目标服务
└── 录制器 (Async) -> 写入消息队列/Kafka -> 存储到文件/数据库
Java实现步骤(以Spring Boot为例)
-
创建一个过滤器 (Filter) 拦截请求
@Component @Order(Ordered.HIGHEST_PRECEDENCE) public class RecordingFilter implements Filter { @Autowired private RecordService recordService; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; // 1. 包装请求体,使其可重复读取 (使用ContentCachingRequestWrapper) ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); // 2. 执行原始请求链 chain.doFilter(wrappedRequest, servletResponse); // 3. 请求完成后,从Response中获取返回值 ContentCachingResponseWrapper wrappedResponse = (ContentCachingResponseWrapper) servletResponse; // 4. 异步录制请求 & 响应 (不阻塞主线程) recordService.record(wrappedRequest, wrappedResponse); } } -
异步录制备用服务 (RecordService)
@Service public class RecordService { public void record(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) { // 异步处理,避免增加主线路延迟 CompletableFuture.runAsync(() -> { RecordRecord record = new RecordRecord(); record.setUri(request.getRequestURI()); record.setMethod(request.getMethod()); record.setHeaders(getHeadersAsMap(request)); record.setRequestBody(new String(request.getContentAsByteArray(), StandardCharsets.UTF_8)); record.setResponseBody(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8)); record.setStatusCode(response.getStatus()); // 存储到文件、数据库或Kafka saveToStorage(record); }); } } -
回放逻辑 读取存储的
RecordRecord,构建一个RestTemplate或WebClient请求,注意:回放时可能需要替换Session、Token等动态参数。public class ReplayEngine { public void replay(String recordId) { RecordRecord record = loadRecord(recordId); HttpHeaders headers = new HttpHeaders(); // 这里需要处理动态参数替换,比如从context获取新token headers.putAll(record.getHeaders()); HttpEntity<String> entity = new HttpEntity<>(record.getRequestBody(), headers); ResponseEntity<String> response = restTemplate.exchange( record.getUri(), request.getMethod(), entity, String.class ); // 返回值与录制的值对比或直接断言 } }
常见工具与框架
- GoReplay (推荐用于生产录制到测试环境):不是Java项目,但可以直接部署在Java服务前,实时抓包。
- Arthas (阿里巴巴):可以录制线上方法的入参和返回值,但更适合排查问题时小批量查看。
- Hoverfly Java:Java版HTTP录制/回放库,支持录制为DSL文件,适用于集成测试。
录制Java方法调用(Mock录制备用)
如果你需要录制一个特定Service类或DAO的方法调用及返回值,用于单元测试或回归测试,常用方案是 代理模式 + 序列化。
实现思路
- 利用动态代理或字节码增强:在目标类的方法执行前后拦截。
- 记录入参和返回值:记录
Method签名、参数列表、返回值(或异常)。 - 存储到文件/数据库。
- 回放:再次调用时匹配入参,返回录制的数据。
Java示例(基于CGLIB或JDK Proxy)
假设有一个接口:
public interface UserService {
User getUserById(Long id);
}
录制器示例 (基于AspectJ或Spring AOP)
@Aspect
@Component
public class MethodRecorderAspect {
private Map<String, List<InvocationRecord>> recordStore = new ConcurrentHashMap<>();
@Around("execution(* com.example.service.UserService.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
Exception exception = null;
long start = System.currentTimeMillis();
// 1. 先执行真实方法
try {
result = pjp.proceed();
} catch (Exception e) {
exception = e;
}
// 2. 录制入参和返回值
InvocationRecord record = new InvocationRecord();
record.setClassName(pjp.getSignature().getDeclaringTypeName());
record.setMethodName(pjp.getSignature().getName());
record.setArgs(pjp.getArgs());
record.setResult(result);
record.setException(exception);
record.setDuration(System.currentTimeMillis() - start);
// 3. 存储(按方法签名区分)
String key = record.getMethodSignature();
recordStore.computeIfAbsent(key, k -> new ArrayList<>()).add(record);
if (exception != null) {
throw exception;
}
return result;
}
}
回放器示例 (模拟MOCK)
public class ReplayInterceptor implements MethodInterceptor {
private Map<String, List<InvocationRecord>> recordStore;
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
String key = method.getDeclaringClass().getName() + "." + method.getName() + "(" + Arrays.toString(args) + ")";
List<InvocationRecord> records = recordStore.get(key); // 简化匹配逻辑
if (records != null && !records.isEmpty()) {
InvocationRecord matched = findBestMatch(records, args);
if (matched.getException() != null) {
throw matched.getException();
}
return matched.getResult();
}
// 如果没有录制数据,抛出异常
throw new RuntimeException("No recorded data for: " + key);
}
}
注意事项
- 序列化问题:录制的方法返回值(Result)必须实现
Serializable,否则不能持久化到文件。 - 匹配精度:如果入参包含时间戳、随机ID,仅靠equals匹配会失败,一般需要定义一个
Matcher。 - 线程安全:多线程录制时,存储结构需使用
ConcurrentHashMap。
两种场景的选择
| 需求 | 推荐实现方案 | 推荐工具 |
|---|---|---|
| 录制整个服务的HTTP请求 | 使用Filter + 双向流复制,结果写入Kafka/磁盘 | GoReplay、Hoverfly、自定义Filter |
| 录制某个Java方法的调用 | Spring AOP或CGLIB实现环绕通知,序列化入参/返回值到文件 | Arthas(临时)、EasyMock(测试)、Mockito Spying(测试) |
| 全链路录制(服务间调用) | 基于OpenTelemetry的Span + 录制Spans | OpenTelemetry + 自定义Exporter |
建议:
- 如果是性能测试或线上流量复现需求,优先考虑 GoReplay 或 OpenTelemetry的商业版,它们经过了生产验证。
- 如果是接口测试录制生成用例,可以自己写一个简单的Filter录制器,使用
CompletableFuture异步写入,对主流程影响极小。 - 如果是单元测试Mock录制,直接使用Mockito或EasyMock的Spy功能就足够了,重复造轮子成本较高。