利用logger打印完整的okhttp网络请求和响应日志

我们公司在项目中使用的网络请求工具是Retrofit,底层封装的是OkHttp,通常调试网络接口时都会将网络请求和响应相关数据通过日志的形式打印出来。OkHttp也提供了一个网络拦截器okhttp-logging-interceptor,通过它能拦截okhttp网络请求和响应所有相关信息(请求行、请求头、请求体、响应行、响应行、响应头、响应体)。


使用okhttp网络日志拦截器:

compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'

定义拦截器中的网络日志工具

public class HttpLogger implements HttpLoggingInterceptor.Logger {
        @Override
        public void log(String message) {
            Log.d("HttpLogInfo", message);
        }
    }

初始化OkHttpClient,并添加网络日志拦截器

/**
* 初始化okhttpclient.
*
* @return okhttpClient
*/
private OkHttpClient okhttpclient() {
    if (mOkHttpClient == null) {
        HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor(new HttpLogger());
        logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        mOkHttpClient = new OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .addNetworkInterceptor(logInterceptor)
            .build();
    }
    return mOkHttpClient;
}

打印出来的日志

拦截的网络请求日志信息

拦截的网络请求日志信息-1

拦截的网络请求日志信息-2.png

在给OkhttpClient添加网络请求拦截器的时候需要注意,应该调用方法addNetworkInterceptor,而不是addInterceptor。因为有时候可能会通过cookieJar在header里面去添加一些持久化的cookie或者session信息。这样就在请求头里面就不会打印出这些信息。
看一下OkHttpClient调用拦截器的源码:

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

在okhttp执行网络请求时,会先构造拦截链,此时是将所有的拦截器都放入一个ArrayList中,看源码就知道添加拦截器的顺序是:
client.interceptors()
BridgeInterceptor
CacheInterceptor
ConnectInterceptor
networkInterceptors
CallServerInterceptor
在通过拦截链执行拦截逻辑是按先后顺序递归调用的。如果是我们调用addInterceptor方法来添加HttpLoggingInterceptor拦截器,那么网络日志拦截器就会被添加到client.networkInterceptors()里面,根据添加到ArrayList中的顺序,执行拦截时会先执行HttpLoggingInterceptor,并打印出日志。然后才会执行CookieJar包装的拦截器BridgeInterceptor。这就导致我们添加header中的cookie等信息不会打印出来。


利用HttpLoggingInterceptor打印网络日志非常完整,但是看到响应的结果数据时,感觉有些混乱,平常在调试时希望一眼就能看清楚json数据的层次结构,所以需要将响应结果的json串进行格式化。

我采用的是开源日志库looger来打印,这个库不但能很方便的帮开发者过滤掉系统日志,而且对打印出来的效果作了优化,更加简洁美观。

关于looger的详细的API:传送门

加入logger的依赖:

 compile 'com.orhanobut:logger:1.15'

在使用looger库的时候我通常都会先封装一层,作为一个工具类。

public class LogUtil {
    /**
     * 初始化log工具,在app入口处调用
     *
     * @param isLogEnable 是否打印log
     */
    public static void init(boolean isLogEnable) {
        Logger.init("LogHttpInfo")
                .hideThreadInfo()
                .logLevel(isLogEnable ? LogLevel.FULL : LogLevel.NONE)
                .methodOffset(2);
    }

    public static void d(String message) {
        Logger.d(message);
    }

    public static void i(String message) {
        Logger.i(message);
    }

    public static void w(String message, Throwable e) {
        String info = e != null ? e.toString() : "null";
        Logger.w(message + ":" + info);
    }

    public static void e(String message, Throwable e) {
        Logger.e(e, message);
    }

    public static void json(String json) {
        Logger.json(json);
    }
}

在应用入口调用初始化方法

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化Looger工具
        LogUtil.init(BuildConfig.LOG_DEBUG);
    }
}

如果直接在LoggerHttp的log方法中调用LogUtil.d(message),打印出来的日志是分散的,因为log方法是将一个网络请求的请求\响应行、header逐条打印的。但想要的效果是将同一个网络请求和响应的所有信息合并成一条日志,这样才方便调试时查看。
所以需要在LoggerHttp的log方法中做一些逻辑处理:

private class HttpLogger implements HttpLoggingInterceptor.Logger {
    private StringBuilder mMessage = new StringBuilder();

    @Override
    public void log(String message) {
        // 请求或者响应开始
        if (message.startsWith("--> POST")) {
            mMessage.setLength(0);
        }
        // 以{}或者[]形式的说明是响应结果的json数据,需要进行格式化
        if ((message.startsWith("{") && message.endsWith("}"))
            || (message.startsWith("[") && message.endsWith("]"))) {
            message = JsonUtil.formatJson(JsonUtil.decodeUnicode(message));
        }
        mMessage.append(message.concat("\n"));
        // 响应结束,打印整条日志
        if (message.startsWith("<-- END HTTP")) {
            LogUtil.d(mMessage.toString());
        }
    }
}

这里之所以没有采用looger库的Looger.json(String json)方法去打印json数据,是因为这个方法调用也会打印成单独的一条日志,不能实现将请求的所有信息在一条日志中。
JsonUtil是单独封装的一个将json格式化的工具,通过formatJson(String json)将json串格式化出清晰的层次结构。

/**
 * 格式化json字符串
 *
 * @param jsonStr 需要格式化的json串
 * @return 格式化后的json串
 */
public static String formatJson(String jsonStr) {
    if (null == jsonStr || "".equals(jsonStr)) return "";
    StringBuilder sb = new StringBuilder();
    char last = '\0';
    char current = '\0';
    int indent = 0;
    for (int i = 0; i < jsonStr.length(); i++) {
        last = current;
        current = jsonStr.charAt(i);
        //遇到{ [换行,且下一行缩进
        switch (current) {
            case '{':
            case '[':
                sb.append(current);
                sb.append('\n');
                indent++;
                addIndentBlank(sb, indent);
                break;
            //遇到} ]换行,当前行缩进
            case '}':
            case ']':
                sb.append('\n');
                indent--;
                addIndentBlank(sb, indent);
                sb.append(current);
                break;
            //遇到,换行
            case ',':
                sb.append(current);
                if (last != '\\') {
                    sb.append('\n');
                    addIndentBlank(sb, indent);
                }
                break;
            default:
                sb.append(current);
        }
    }
return sb.toString();
}

/**
 * 添加space
 *
 * @param sb
 * @param indent
 */
private static void addIndentBlank(StringBuilder sb, int indent) {
    for (int i = 0; i < indent; i++) {
        sb.append('\t');
    }
}

decodeUnicode(String json)是将json中的Unicode编码转化为汉字编码(unicode编码的json中的汉字打印出来有可能是\u开头的字符串,所以需要处理)。

/**
 * http 请求数据返回 json 中中文字符为 unicode 编码转汉字转码
 *
 * @param theString
 * @return 转化后的结果.
 */
public static String decodeUnicode(String theString) {
    char aChar;
    int len = theString.length();
    StringBuffer outBuffer = new StringBuffer(len);
    for (int x = 0; x < len; ) {
        aChar = theString.charAt(x++);
        if (aChar == '\\') {
            aChar = theString.charAt(x++);
            if (aChar == 'u') {
                int value = 0;
                for (int i = 0; i < 4; i++) {
                    aChar = theString.charAt(x++);
                    switch (aChar) {
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                            value = (value << 4) + aChar - '0';
                            break;
                        case 'a':
                        case 'b':
                        case 'c':
                        case 'd':
                        case 'e':
                        case 'f':
                            value = (value << 4) + 10 + aChar - 'a';
                            break;
                        case 'A':
                        case 'B':
                        case 'C':
                        case 'D':
                        case 'E':
                        case 'F':
                            value = (value << 4) + 10 + aChar - 'A';
                            break;
                        default:
                            throw new IllegalArgumentException(
                                    "Malformed   \\uxxxx   encoding.");
                    }

                }
                outBuffer.append((char) value);
            } else {
                if (aChar == 't')
                    aChar = '\t';
                else if (aChar == 'r')
                    aChar = '\r';
                else if (aChar == 'n')
                    aChar = '\n';
                else if (aChar == 'f')
                    aChar = '\f';
                outBuffer.append(aChar);
            }
        } else
            outBuffer.append(aChar);
    }
    return outBuffer.toString();
}

最终效果(不能将图全部截出来,所以我就把日志贴成代码段了)

D/LogHttpInfo: ╔════════════════════════════════════════════════════════════════════════════════════════
D/LogHttpInfo: ║ RealInterceptorChain.proceed  (RealInterceptorChain.java:92)
D/LogHttpInfo: ║    HttpLoggingInterceptor.intercept  (HttpLoggingInterceptor.java:266)
D/LogHttpInfo: ╟────────────────────────────────────────────────────────────────────────────────────────
D/LogHttpInfo: ║ --> POST http://op.juhe.cn/onebox/movie/video http/1.1
D/LogHttpInfo: ║ Content-Type: application/x-www-form-urlencoded
D/LogHttpInfo: ║ Content-Length: 95
D/LogHttpInfo: ║ Host: op.juhe.cn
D/LogHttpInfo: ║ Connection: Keep-Alive
D/LogHttpInfo: ║ Accept-Encoding: gzip
D/LogHttpInfo: ║ User-Agent: okhttp/3.5.0
D/LogHttpInfo: ║ 
D/LogHttpInfo: ║ key=a3d3a43fcc149b6ed8268b8fa41d27b7&dtype=json&q=%E9%81%97%E8%90%BD%E7%9A%84%E4%B8%96%E7%95%8C
D/LogHttpInfo: ║ --> END POST (95-byte body)
D/LogHttpInfo: ║ <-- 200 OK http://op.juhe.cn/onebox/movie/video (760ms)
D/LogHttpInfo: ║ Server: nginx
D/LogHttpInfo: ║ Date: Mon, 16 Jan 2017 09:36:35 GMT
D/LogHttpInfo: ║ Content-Type: application/json;charset=utf-8
D/LogHttpInfo: ║ Transfer-Encoding: chunked
D/LogHttpInfo: ║ Connection: keep-alive
D/LogHttpInfo: ║ X-Powered-By: PHP/5.6.23
D/LogHttpInfo: ║ 
D/LogHttpInfo: ║ {
D/LogHttpInfo: ║    "reason":"查询成功",
D/LogHttpInfo: ║    "result":{
D/LogHttpInfo: ║        "title":"遗失的世界",
D/LogHttpInfo: ║        "tag":"动作 \/ 科幻",
D/LogHttpInfo: ║        "act":"詹妮弗·奥黛尔 威尔·斯诺 拉塞尔·布雷克利",
D/LogHttpInfo: ║        "year":"1999",
D/LogHttpInfo: ║        "rating":null,
D/LogHttpInfo: ║        "area":"美国",
D/LogHttpInfo: ║        "dir":"理查德·富兰克林",
D/LogHttpInfo: ║        "desc":"本剧取材于制造出福尔摩斯这个人物形象的英国著名作家亚瑟.柯南道尔的经典小说。故事讲述的是在一块未开发的土地上遭遇恐龙的危险经历。 一名孤独的探险家死去了,他那破旧的、包有皮边的笔记本便成为因时间而被淡忘了的史前高原探险活动的惟一的线索。 在伦敦,爱德华·查林杰教授召集了擅长不同领域的冒险家,组建了一支探险队,决心证实遗失的世界的存在,在地图上未标明的丛林中探险。 在亚马逊丛林一片被时间遗忘的高原土地上,科学探险队的几位成员在寻找离开高原的路径。他们必须防御来自原始部落猎人们的袭击。他们在野外的高原上遇阻,无法返回,而这里又是一个令人害怕的世界,时常出没一些史前的食肉动物、原始的猿人、奇特的植物和吸血的蝙蝠。为了生存,这群命运不济的人必须团结起来,拋弃个人之间的喜好和偏见,随时准备应付任何可能突发的情况。在野性丛林美女维罗尼卡的帮助下,手中只有几只猎枪的他们用智能一次又一次摆脱了死亡的威胁。",
D/LogHttpInfo: ║        "cover":"http:\/\/p6.qhimg.com\/t0160a8a6f5b768034a.jpg",
D/LogHttpInfo: ║        "vdo_status":"play",
D/LogHttpInfo: ║        "playlinks":{
D/LogHttpInfo: ║            "tudou":"http:\/\/www.tudou.com\/programs\/view\/KVeyWojke1M\/?tpa=dW5pb25faWQ9MTAyMjEzXzEwMDAwMV8wMV8wMQ"
D/LogHttpInfo: ║        },
D/LogHttpInfo: ║        "video_rec":[
D/LogHttpInfo: ║            {
D/LogHttpInfo: ║                "cover":"http:\/\/p2.qhimg.com\/d\/dy_4dc349a3bf8c1b267d3236f3b74c8ea2.jpg",
D/LogHttpInfo: ║                "detail_url":"http:\/\/www.360kan.com\/tv\/PrRoc3GoSzDpMn.html",
D/LogHttpInfo: ║                "title":"阿尔法战士 第一季"
D/LogHttpInfo: ║            },
D/LogHttpInfo: ║            {
D/LogHttpInfo: ║                "cover":"http:\/\/p7.qhimg.com\/t01513514907831e055.jpg",
D/LogHttpInfo: ║                "detail_url":"http:\/\/www.360kan.com\/tv\/Q4Frc3GoRmbuMX.html",
D/LogHttpInfo: ║                "title":"浩劫余生 第一季"
D/LogHttpInfo: ║            }
D/LogHttpInfo: ║        ],
D/LogHttpInfo: ║        "act_s":[
D/LogHttpInfo: ║            {
D/LogHttpInfo: ║                "name":"詹妮弗·奥黛尔",
D/LogHttpInfo: ║                "url":"http:\/\/baike.so.com\/doc\/5907024-6119928.html",
D/LogHttpInfo: ║                "image":"http:\/\/p2.qhmsg.com\/dmsmty\/120_110_100\/t0154caf60f6fa2dc56.jpg"
D/LogHttpInfo: ║            },
D/LogHttpInfo: ║            {
D/LogHttpInfo: ║                "name":"威尔·斯诺",
D/LogHttpInfo: ║                "url":"http:\/\/baike.so.com\/doc\/204403-216173.html",
D/LogHttpInfo: ║                "image":"http:\/\/p8.qhmsg.com\/dmsmty\/120_110_100\/t018d2ce8920050594f.jpg"
D/LogHttpInfo: ║            },
D/LogHttpInfo: ║            {
D/LogHttpInfo: ║                "name":"拉塞尔·布雷克利",
D/LogHttpInfo: ║                "url":"http:\/\/baike.so.com\/doc\/1057636-1118829.html",
D/LogHttpInfo: ║                "image":"http:\/\/p2.qhmsg.com\/dmsmty\/120_110_100\/t01aa727c49da3edc79.jpg"
D/LogHttpInfo: ║            }
D/LogHttpInfo: ║        ]
D/LogHttpInfo: ║    },
D/LogHttpInfo: ║    "error_code":0
D/LogHttpInfo: ║ }
D/LogHttpInfo: ║ <-- END HTTP (2994-byte body)
D/LogHttpInfo: ╚════════════════════════════════════════════════════════════════════════════════════════

通过这样的方式打印出来的网络请求日志包含了所有的网络信息, 并且结构层次非常清晰。
源码:https://github.com/xiaoyanger0825/LogHttpInfo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,825评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,778评论 6 342
  • 开篇 一直觉得应该写一些东西给自己,我并不是所谓的文艺青年,也没有多么的矫情,只是觉得应该给自己做一下总结或者是感...
    齿轮小二阅读 191评论 0 0
  • 微信小程序已经如期发布,首发当天凌晨,还有很多人去关注她,去朋友圈刷屏小程序。在中国互联网史上,从来没有一个功能的...
    更好时代阅读 411评论 0 0