本文目录导读:

在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功能就足够了,重复造轮子成本较高。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播