拦截器机制与请求响应捕获

这一章咱们聊聊核心基础——拦截器机制。说实话,这是整个回放测试方案的地基。地基不稳,后面全是白搭。

我刚开始接触Retrofit的时候,一直把它当成黑盒来用。直到有一次线上出问题,需要排查一个请求的完整链路,才发现自己根本不了解底层怎么流转的。那之后,我花了整整一周时间啃OkHttp的源码。嗯,值了。

拦截器到底是个啥?

说白了,拦截器就是OkHttp提供的一个钩子机制。它能在你的请求发起前、响应返回后,插入自定义的逻辑。你可以想象成一根水管,中间加了个阀门——关上可以过滤,打开可以观察,甚至还能往里加东西。

举个例子:

class LoggingInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        // 请求前记录
        long t1 = System.nanoTime();
        // 执行真正的请求
        Response response = chain.proceed(request);
        // 响应后记录
        long t2 = System.nanoTime();
        System.out.println("耗时: " + (t2 - t1) / 1e6 + "ms");
        return response;
    }
}

这段代码你应该能看懂。但关键是那行 chain.proceed(request)——它才是真正的请求执行点。拦截器的一切操作,都是围绕着「在它之前」和「在它之后」展开的。

我的习惯:每个拦截器尽量只做一件事。日志拦截器只负责打日志,缓存拦截器只负责读写缓存。拆分清晰,后面出了问题才好定位。

拦截器的执行顺序

这个坑我踩过。拦截器是有顺序的,而且顺序决定了它们能看到什么。

拦截器类型 注册方式 执行时机
应用拦截器 addInterceptor() 最先执行,最后返回
网络拦截器 addNetworkInterceptor() 在网络连接后才执行

你想想看,应用拦截器是在最外层。它能看到完整的请求和最终返回的响应。而网络拦截器是在重定向、重试都完成之后才触发。如果你想捕获「真正发出去的请求」,就得用网络拦截器。

我记得有次排查一个302重定向的问题。用应用拦截器打印出来的URL一直是原来的地址,百思不得其解。后来换成网络拦截器,才发现请求已经被重定向到别处了。嗯,这就是区别。

请求响应的捕获与序列化

讲完了拦截器,我们聊聊怎么把请求和响应「抓」下来。

捕获的核心就三步:

  1. 拦截请求信息:URL、Header、Body
  2. 拦截响应信息:状态码、Header、Body
  3. 序列化成可存储的格式:JSON、ProtoBuf 或自定义文本

我一般用JSON格式来存储。简单、可读、也好解析。下面是我常用的序列化结构:

public class RecordedCall {
    private String url;
    private String method;
    private Map<String, String> requestHeaders;
    private String requestBody;
    private int responseCode;
    private Map<String, String> responseHeaders;
    private String responseBody;
    private long timestamp;
    private long durationMs;
}
我曾经踩过的坑:直接读Response的Body,会导致原来的流被消费掉。后续的业务代码就拿不到数据了。正确的做法是用 peekBody() 或者把Response克隆一份。切记!

捕获之后怎么存?

存起来的方式取决于你打算怎么用。做回放测试的话,我建议分目录存:

  • 每个测试用例一个文件夹
  • 每次请求一个JSON文件
  • 文件名用请求的MD5值,避免重复

这样结构清晰,后续做回放匹配也方便。

关键点:捕获不只是copy一份数据。你还需要记录时间戳、耗时、请求顺序。这些信息在回放时能帮你判断「模拟的准不准」。

核心流程图

下面这张图,概括了整个拦截器捕获的核心流程。从应用发起请求到最终写入录制文件,每一步都绕不开拦截器。

拦截器捕获核心流程 应用请求发起 应用拦截器(addInterceptor) 核心拦截器(捕获逻辑) 捕获 OkHttpCall 请求 捕获返回 Response 写入录制文件(JSON) 注意 应用拦截器能看到完整请求 但需注意不消耗原始Body流 核心操作 clone请求 → 读取Body → 序列化为JSON 请求流向 响应返回

代码实战:搭一个基础捕获拦截器

只看理论肯定不够。我们直接看代码。这是我在生产环境中用过的精简版:

class RecordInterceptor implements Interceptor {
    private final RecordWriter writer;

    RecordInterceptor(RecordWriter writer) {
        this.writer = writer;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        // 捕获请求
        RecordedCall call = new RecordedCall();
        call.setUrl(request.url().toString());
        call.setMethod(request.method());
        call.setRequestHeaders(request.headers().toMultimap());

        // 读取请求体
        if (request.body() != null) {
            Buffer buffer = new Buffer();
            request.body().writeTo(buffer);
            call.setRequestBody(buffer.readUtf8());
        }

        long startNs = System.nanoTime();

        // 执行请求
        Response response = chain.proceed(request);
        long durationMs = (System.nanoTime() - startNs) / 1_000_000;

        // 捕获响应——注意克隆Response
        Response.Builder builder = response.newBuilder();
        Response clonedResponse = builder.build();

        call.setResponseCode(clonedResponse.code());
        call.setResponseHeaders(clonedResponse.headers().toMultimap());
        call.setDurationMs(durationMs);

        // 读响应体(只读一次)
        ResponseBody body = clonedResponse.body();
        if (body != null) {
            call.setResponseBody(body.string());
        }

        // 写入文件
        writer.write(call);

        // 返回原Response,保证业务代码不受影响
        return response;
    }
}

代码不多,但每个细节都有讲究。你看我用了 response.newBuilder().build() 克隆了个Response出来。这就是为了防止把原始流消耗掉。

小技巧:捕获到的文件建议用 yyyy-MM-dd/HH/mm/ 这样的时间目录结构。方便排查问题时按时间线回溯。

拦截器注册的方式

搞定了拦截器本身,还得知道怎么注册到Retrofit里。其实就两步:

// 1. 创建OkHttpClient时添加拦截器
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new RecordInterceptor(writer))
    .build();

// 2. 创建Retrofit实例
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(client)
    .build();

就这么简单。所有通过这个Retrofit实例发出的请求,都会被拦截器捕获到。不需要改任何业务代码。

嗯,这一章的内容就到这。掌握了拦截器机制,你就掌握了整个回放测试方案的钥匙。后面的章节,我们会一步步在这个基础上搭起完整的测试体系。