拦截器机制与请求响应捕获
这一章咱们聊聊核心基础——拦截器机制。说实话,这是整个回放测试方案的地基。地基不稳,后面全是白搭。
我刚开始接触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一直是原来的地址,百思不得其解。后来换成网络拦截器,才发现请求已经被重定向到别处了。嗯,这就是区别。
请求响应的捕获与序列化
讲完了拦截器,我们聊聊怎么把请求和响应「抓」下来。
捕获的核心就三步:
- 拦截请求信息:URL、Header、Body
- 拦截响应信息:状态码、Header、Body
- 序列化成可存储的格式: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;
}
peekBody() 或者把Response克隆一份。切记!
捕获之后怎么存?
存起来的方式取决于你打算怎么用。做回放测试的话,我建议分目录存:
- 每个测试用例一个文件夹
- 每次请求一个JSON文件
- 文件名用请求的MD5值,避免重复
这样结构清晰,后续做回放匹配也方便。
核心流程图
下面这张图,概括了整个拦截器捕获的核心流程。从应用发起请求到最终写入录制文件,每一步都绕不开拦截器。
代码实战:搭一个基础捕获拦截器
只看理论肯定不够。我们直接看代码。这是我在生产环境中用过的精简版:
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实例发出的请求,都会被拦截器捕获到。不需要改任何业务代码。
嗯,这一章的内容就到这。掌握了拦截器机制,你就掌握了整个回放测试方案的钥匙。后面的章节,我们会一步步在这个基础上搭起完整的测试体系。