Java案例如何实现服务录制?

wen python案例 2

本文目录导读:

Java案例如何实现服务录制?

  1. 场景一:录制HTTP API请求(流量录制与回放)
  2. 场景二:录制Java方法调用(Mock录制备用)
  3. 两种场景的选择

在Java中实现“服务录制”(Service Recording/Replay)通常有两种含义:

  1. 录制HTTP请求:类似商业API网关(如阿里云API、Kong)或工具(如GoReplay)的功能——记录生产上真实的HTTP请求流量,然后在测试环境重放。
  2. 录制方法调用:录制某个Java对象/服务的所有方法调用(入参和返回值),用于测试(Mock)或调试。

下面分别给出这两种场景的常见实现方案和核心思路。


录制HTTP API请求(流量录制与回放)

这是最典型的“服务录制”需求,目标是:将发往A服务的请求全部复制一份存下来,之后能在B服务或本地调试时重放这些请求。

核心架构(基于Filter/Interceptor与消息队列)

客户端 -> 网关/反向代理
    |
    ├── 正常业务请求 -> 目标服务
    └── 录制器 (Async) -> 写入消息队列/Kafka -> 存储到文件/数据库

Java实现步骤(以Spring Boot为例)

  1. 创建一个过滤器 (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);
        }
    }
  2. 异步录制备用服务 (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); 
            });
        }
    }
  3. 回放逻辑 读取存储的RecordRecord,构建一个RestTemplateWebClient请求,注意:回放时可能需要替换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的方法调用及返回值,用于单元测试或回归测试,常用方案是 代理模式 + 序列化

实现思路

  1. 利用动态代理或字节码增强:在目标类的方法执行前后拦截。
  2. 记录入参和返回值:记录Method签名参数列表返回值(或异常)。
  3. 存储到文件/数据库
  4. 回放:再次调用时匹配入参,返回录制的数据。

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);
    }
}

注意事项

  1. 序列化问题:录制的方法返回值(Result)必须实现Serializable,否则不能持久化到文件。
  2. 匹配精度:如果入参包含时间戳、随机ID,仅靠equals匹配会失败,一般需要定义一个Matcher
  3. 线程安全:多线程录制时,存储结构需使用ConcurrentHashMap

两种场景的选择

需求 推荐实现方案 推荐工具
录制整个服务的HTTP请求 使用Filter + 双向流复制,结果写入Kafka/磁盘 GoReplay、Hoverfly、自定义Filter
录制某个Java方法的调用 Spring AOP或CGLIB实现环绕通知,序列化入参/返回值到文件 Arthas(临时)、EasyMock(测试)、Mockito Spying(测试)
全链路录制(服务间调用) 基于OpenTelemetry的Span + 录制Spans OpenTelemetry + 自定义Exporter

建议

  • 如果是性能测试或线上流量复现需求,优先考虑 GoReplayOpenTelemetry的商业版,它们经过了生产验证。
  • 如果是接口测试录制生成用例,可以自己写一个简单的Filter录制器,使用CompletableFuture异步写入,对主流程影响极小。
  • 如果是单元测试Mock录制,直接使用Mockito或EasyMock的Spy功能就足够了,重复造轮子成本较高。

抱歉,评论功能暂时关闭!