HttpLoggingInterceptor 没有 body 日志的分析

1,771 阅读3分钟

一般为了简单记录 OkHttp 请求内容和返回数据,可以选择自行实现个日志拦截器,比如像下面这样:

/**
 * 日志拦截器
 * Created by baishixian on 2017/3/13.
 */

public class LoggingInterceptor implements Interceptor {

    @Override public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        long startTime = System.currentTimeMillis();
        okhttp3.Response response = chain.proceed(chain.request());
        long endTime = System.currentTimeMillis();
        long duration=endTime-startTime;

        ResponseBody responseBody = response.body();
        if (responseBody == null){
            return response;
        }
        okhttp3.MediaType mediaType = responseBody.contentType();
        String content = responseBody.string();
        LogUtil.d("\n");
        LogUtil.d("----------Start----------------");
        LogUtil.d("| "+request.toString());
        String method=request.method();
        if("POST".equals(method)){
            StringBuilder sb = new StringBuilder();
            if (request.body() instanceof FormBody) {
                FormBody body = (FormBody) request.body();
                if (body != null) {
                    for (int i = 0; i < body.size(); i++) {
                        sb.append(body.encodedName(i)).append("=").append(body.encodedValue(i)).append(",");
                    }
                }
                sb.delete(sb.length() - 1, sb.length());
                LogUtil.d("| RequestParams:{"+sb.toString()+"}");
            }
        }
        LogUtil.d("| Response:" + content);
        LogUtil.d("----------End:"+duration+"毫秒----------");
        return response.newBuilder()
                .body(okhttp3.ResponseBody.create(mediaType, content))
                .build();
    }
}

在构建 OkHttpClient 时添加该拦截器即可:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
            // 日志拦截器
            .addNetworkInterceptor(new LoggingInterceptor())
            // time out
            .connectTimeout(TIMEOUT_CONNECTION, TimeUnit.SECONDS)
            .readTimeout(TIMEOUT_READ, TimeUnit.SECONDS)
            // 失败重连
            .retryOnConnectionFailure(true)
            // 添加UA
            .addInterceptor(new UserAgentInterceptor(NetworkUtil.getUserAgent())
            .build();

但在有些情况需要展示详细的请求数据,不同每次都需要连代理抓包分析也是很麻烦。这时就可以利用 HttpLoggingInterceptor 来实现OkHttp 请求日志输出了,日志数据可以非常详细。

在看到这篇利用logger打印完整的 okhttp 网络请求和响应日志文章后,里面完整展示了一个实例,基本上按照可以满足需求。文章里面也有提到在给 OkHttpClient 添加网络请求拦截器的时候需要注意调用方法addNetworkInterceptor 而不是 addInterceptor。

但是在实际使用时发现,通过 addNetworkInterceptor() 添加日志拦截器有响应头但是没有响应体,换成调用 addInterceptor() 就正常。看到有些评论说没找到原因,于是这里做个简单分析。

使用 addNetworkInterceptor() 添加日志拦截器时,日志输出有响应头但是没有响应体的情况是因为 HttpLoggingInterceptor 不会打印 Gzip 过响应 body。具体代码逻辑体现在:HttpLoggingInterceptor.java

HttpLoggingInterceptor

输出 body 日志前 HttpLoggingInterceptor 使用 bodyEncoded 方法进行了判断,如果 body 内容是已编码过的而且编码方式不是默认的 identity ,则会忽略对 body 解析,所以此时是看不到 body 内容,而且会输出 encoded body omitted 提示。

private boolean bodyEncoded(Headers headers) {
    String contentEncoding = headers.get("Content-Encoding");
    return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
  }

而之所以使用 addInterceptor 添加日志拦截器时不会出现这个问题,其实这篇文章中也有提到 okHttp 拦截链顺序的问题,代码逻辑体现在:RealCall

okHttp

这里需要注意 BridgeInterceptor 这个拦截器,简单的说这是应用程序代码到网络代码的桥梁,做了很多处理网络请求的 header 和请求返回来 body 的工作,所以除了文章中作者说的处理 header 中的 cookie 信息之外,也有处理内容的 gzip。

因此当我们把日志拦截器通过 addInterceptor 添加时,发送请求时日志输出会在 BridgeInterceptor 前进行添加 cookie 数据前输出导致可能看不到部分数据,但是请求返回时日志输出是在 BridgeInterceptor 后,所以此时输出的 body 已经 gzip 解压缩过了。

关于 OkHttp 责任链,如果不想查找源码,可以参考这篇OkHttp的请求拦截链,里面各模块的代码贴得比较全。

这里主要理解责任链模式,记住 OkHttp 的责任链顺序,结合 TCP/IP 网络模型考虑各个拦截器环节所做的事情,区分开发送网络请求和接收数据返回在流程上的相反性。

在这个责任链中主要是考虑数据的流向性,明白从应用数据到网络(应用代码到网络代码)和从网络数据到应用(网络代码到应用代码)的区别。而责任链的顺序也是符合这个规律的,并不是一层不变的,谁先谁后也是按照实际数据流向来考虑的。