创建OkHttp自定义Log

原文链接:创建OkHttp自定义Log

背景

本文重点讲解如何在使用OkHttp作为网络请求框架的前提下,如何自定义一个适合自己项目的Http Log,从而提升网络Api开发、调试效率。

Http协议

只有对Http协议有基本的了解,才能更好的调试网络接口。

一个故事

男女主一次偶然的机会,开始信件传情,但他们的信件并不是直接寄给对方,而是先寄到某个地点A,由地点A的主人转发。(好吧,这是《北京遇上西雅图之不二情书》的情节)。我们来看男主发信的过程:

  1. 先购买有效邮票;
  2. 填写信件的收件人、收件地址等信息;
  3. 把信件放到信封里寄出去。

显然,女主收到信后回复信件也是同样的流程。

类比

我们现在将Http协议的消息结构和故事主人公收发信的过程做一个类比:
类比图
下面是使用Fiddler工具抓取www.xitu.io的网络请求和响应图:
稀土网络响应图

Http消息结构

由前面的分析可知,Http请求消息由三部分组成:

  1. 请求行由3部分组成:①请求的方法,POST还是GET等;②请求路径;③Http协议
  2. 请求头,每一行都是name:value的结构,包含各种来自请求客服端的信息
  3. 请求体,提交给服务器的信息,GET方法没有此项。

Http响应体跟请求体格式大致一样。

  1. 请求体有协议和响应码组成,200为响应成功
  2. 响应头,每一行都是name:value的结构,包含各种来自服务器的信息
  3. 响应体,返回客户端需要的数据

自定义LogInterceptor

OkHttp Interceptor

OkHttp的一大特点就是可以在发出请求体和收到响应体之间添加任意个数任意任意功能的拦截器,对请求体或者响应体进行操作。还是用《北京遇上西雅图之不二情书》的故事来说,那么地点A的主人就是充当拦截器的角色,在故事中他不仅转发信件,还阅读了信件的内容。

根据需求确定需要Log的信息

Log的信息和划分两类,一类是跟业务相关的信息,一类是与业务无关。

  1. 业务相关包括:
    请求地址url;
    请求头:token、sessionId;
    请求体:POST提交的内容;
    响应头:token/sessionId;
    响应体:服务器返回数据;

  2. 业务无关包括:
    网络状态码:200为正常反应;
    网络请求时间:从发出网络请求到响应所消耗的时间;
    网络协议:http1、http2等;
    网络Method:POST、GET等;
    不管是业务相关的数据还是业务无关的数据,都是来自于Http请求体和响应体的消息结构中。

Log效果图

以公司项目的测试服务器自动登录接口,log效果如下:

 POST
acid->1075
userId->-1
network code->200
url->http://mobileapi.app100688440.twsapp.com/app/open/open.do?ACID=1075&userId=-1&vendorId=7999&VERS=6.5.1&fromType=1110
time->84.473
request headers->sessionId: 
request->{"data":{"pagenum":0,"uid":-1,"flag":"00000000"},"requeststamp":"20161212151841836466"}
body->{"code":200,"responsestamp":"20161212151841836466","data":{"uid":-1,"nickname":"游客258","uploadUrl":"http://mobileapi.app100688440.twsapp.com/uploadservlet","wxUrl":"http://wx.app100688440.twsapp.com","isEmcee":0,"canLive":0,"goodnum":0,"index":{},"revGift":{},"msgRemind":{},"freshmanMission":{},"account":{},"onlineLimit":"600","userSecretKey":"brjefjzw37ocw46"}}

log解释:
POST:此接口使用POST方法;
acid:标识此接口的id,每个接口有唯一的acid,根据acid可查询到此接口的功能,例如此接口acid = 1075为自动登录接口;
userId:用户id
network code:返回200证明服务器响应成功;
url:此接口请求的url地址;
time:为响应时间,如果某接口响应时间过长,排除网络环境的原因,就可以跟服务端商量是否可优化;
request header:此项目需要传sessionId;
request:此处打印Post方法的请求体,若后台返回参数错误,检查此行log即可;
body:后台数据返回,采用json格式;

代码实现


/**
 * 添加Log
 */
public class LogInterceptor implements Interceptor {

    private static final String TAG = "LogInterceptor";
    private static final Charset UTF8 = Charset.forName("UTF-8"); //urf8编码

    @Override
    public Response intercept(Chain chain) throws IOException {  //实现Interceptor接口方法
        Log.d(TAG,"before chain,request()");
        Request request = chain.request();  //获取request
        String acid = request.url().queryParameter("ACID"); //在url中获取ACID的参数值;
        Response response;
        try {
            long t1 = System.nanoTime();
            response = chain.proceed(request); //OkHttp链式调用
            long t2 = System.nanoTime();
            double time = (t2 - t1) / 1e6d;   //用请求后的时间减去请求前的时间得到耗时

            String userId = request.url().queryParameter("userId");
            String type = "";
            if (request.method().equals("GET")) {    //判断Method类型
                type = "GET";
            } else if (request.method().equals("POST")) {
                type = "POST";
            } else if (request.method().equals("PUT")) {
                type = "PUT";
            } else if (request.method().equals("DELETE")) {
                type = "DELETE";
            }
            BufferedSource source = response.body().source();
            source.request(Long.MAX_VALUE); // Buffer the entire body.
            Buffer buffer = source.buffer();
            String logStr = "\n--------------------".concat(TextUtils.isEmpty(acid) ? "" : acid).concat("  begin--------------------\n")
                    .concat(type)
                    .concat("\nacid->").concat(TextUtils.isEmpty(acid) ? "" : acid)
                    .concat("\nuserId->").concat(TextUtils.isEmpty(userId) ? "" : userId)
                    .concat("\nnetwork code->").concat(response.code() + "")
                    .concat("\nurl->").concat(request.url() + "")
                    .concat("\ntime->").concat(time + "")
                    .concat("\nrequest headers->").concat(request.headers() + "")
                    .concat("request->").concat(bodyToString(request.body()))
                    .concat("\nbody->").concat(buffer.clone().readString(UTF8)); //响应体转String
            Log.i(TAG, logStr);
        } catch (Exception e) {
            Log.d(TAG,e.getClass().toString()+", error:acid = "+acid);  //网络出错,log 出错的acid
            throw e; //不拦截exception,由上层处理网络错误
        }
        return response;
    }

    /**
     * 请求体转String
     * @param request
     * @return
     */
    private static String bodyToString(final RequestBody request) {

        try {
            final Buffer buffer = new Buffer();
            request.writeTo(buffer);
            return buffer.readUtf8();
        } catch (final IOException e) {
            return "did not work";
        }
    }

}

开源项目okhttp-logging-interceptor

如果不想自己编写代码,也可以使用开源项目okhttp-logging-interceptor,支持:1.设置Log的等级;2.使用自定义Log。此开源项目也仅是一个Interceptor。但个人觉得log的样式并不适合调试使用。同时log的内容比较通用,若想突出对应项目的信息,建议还是自定义Http Log。

Okhttp+Retrofit+Rxjava

此Interceptor配合OkHttp使用,关于Okhttp+Retrofit+Rxjava的整合,可查看RxJava+Retrofit+OkHttp封装

引用

okhttp-logging-interceptor
okhttp Interceptors
Http协议详解