我們公司在項目中使用的網(wǎng)絡(luò)請求工具是Retrofit,底層封裝的是OkHttp,通常調(diào)試網(wǎng)絡(luò)接口時都會將網(wǎng)絡(luò)請求和響應(yīng)相關(guān)數(shù)據(jù)通過日志的形式打印出來。OkHttp也提供了一個網(wǎng)絡(luò)攔截器okhttp-logging-interceptor,通過它能攔截okhttp網(wǎng)絡(luò)請求和響應(yīng)所有相關(guān)信息(請求行、請求頭、請求體、響應(yīng)行、響應(yīng)行、響應(yīng)頭、響應(yīng)體)。
使用okhttp網(wǎng)絡(luò)日志攔截器:
compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
定義攔截器中的網(wǎng)絡(luò)日志工具
public class HttpLogger implements HttpLoggingInterceptor.Logger {
@Override
public void log(String message) {
Log.d("HttpLogInfo", message);
}
}
初始化OkHttpClient,并添加網(wǎng)絡(luò)日志攔截器
/**
* 初始化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;
}
打印出來的日志



在給OkhttpClient添加網(wǎng)絡(luò)請求攔截器的時候需要注意,應(yīng)該調(diào)用方法
addNetworkInterceptor,而不是addInterceptor。因為有時候可能會通過cookieJar在header里面去添加一些持久化的cookie或者session信息。這樣就在請求頭里面就不會打印出這些信息。看一下OkHttpClient調(diào)用攔截器的源碼:
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執(zhí)行網(wǎng)絡(luò)請求時,會先構(gòu)造攔截鏈,此時是將所有的攔截器都放入一個ArrayList中,看源碼就知道添加攔截器的順序是:
client.interceptors(),
BridgeInterceptor,
CacheInterceptor,
ConnectInterceptor,
networkInterceptors,
CallServerInterceptor。
在通過攔截鏈執(zhí)行攔截邏輯是按先后順序遞歸調(diào)用的。如果是我們調(diào)用addInterceptor方法來添加HttpLoggingInterceptor攔截器,那么網(wǎng)絡(luò)日志攔截器就會被添加到client.networkInterceptors()里面,根據(jù)添加到ArrayList中的順序,執(zhí)行攔截時會先執(zhí)行HttpLoggingInterceptor,并打印出日志。然后才會執(zhí)行CookieJar包裝的攔截器BridgeInterceptor。這就導致我們添加header中的cookie等信息不會打印出來。
利用HttpLoggingInterceptor打印網(wǎng)絡(luò)日志非常完整,但是看到響應(yīng)的結(jié)果數(shù)據(jù)時,感覺有些混亂,平常在調(diào)試時希望一眼就能看清楚json數(shù)據(jù)的層次結(jié)構(gòu),所以需要將響應(yīng)結(jié)果的json串進行格式化。
我采用的是開源日志庫looger來打印,這個庫不但能很方便的幫開發(fā)者過濾掉系統(tǒng)日志,而且對打印出來的效果作了優(yōu)化,更加簡潔美觀。
關(guān)于looger的詳細的API:傳送門。
加入logger的依賴:
compile 'com.orhanobut:logger:1.15'
在使用looger庫的時候我通常都會先封裝一層,作為一個工具類。
public class LogUtil {
/**
* 初始化log工具,在app入口處調(diào)用
*
* @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);
}
}
在應(yīng)用入口調(diào)用初始化方法
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化Looger工具
LogUtil.init(BuildConfig.LOG_DEBUG);
}
}
如果直接在LoggerHttp的log方法中調(diào)用LogUtil.d(message),打印出來的日志是分散的,因為log方法是將一個網(wǎng)絡(luò)請求的請求\響應(yīng)行、header逐條打印的。但想要的效果是將同一個網(wǎng)絡(luò)請求和響應(yīng)的所有信息合并成一條日志,這樣才方便調(diào)試時查看。
所以需要在LoggerHttp的log方法中做一些邏輯處理:
private class HttpLogger implements HttpLoggingInterceptor.Logger {
private StringBuilder mMessage = new StringBuilder();
@Override
public void log(String message) {
// 請求或者響應(yīng)開始
if (message.startsWith("--> POST")) {
mMessage.setLength(0);
}
// 以{}或者[]形式的說明是響應(yīng)結(jié)果的json數(shù)據(jù),需要進行格式化
if ((message.startsWith("{") && message.endsWith("}"))
|| (message.startsWith("[") && message.endsWith("]"))) {
message = JsonUtil.formatJson(JsonUtil.decodeUnicode(message));
}
mMessage.append(message.concat("\n"));
// 響應(yīng)結(jié)束,打印整條日志
if (message.startsWith("<-- END HTTP")) {
LogUtil.d(mMessage.toString());
}
}
}
這里之所以沒有采用looger庫的Looger.json(String json)方法去打印json數(shù)據(jù),是因為這個方法調(diào)用也會打印成單獨的一條日志,不能實現(xiàn)將請求的所有信息在一條日志中。
JsonUtil是單獨封裝的一個將json格式化的工具,通過formatJson(String json)將json串格式化出清晰的層次結(jié)構(gòu)。
/**
* 格式化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編碼轉(zhuǎn)化為漢字編碼(unicode編碼的json中的漢字打印出來有可能是\u開頭的字符串,所以需要處理)。
/**
* http 請求數(shù)據(jù)返回 json 中中文字符為 unicode 編碼轉(zhuǎn)漢字轉(zhuǎn)碼
*
* @param theString
* @return 轉(zhuǎn)化后的結(jié)果.
*/
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":"本劇取材于制造出福爾摩斯這個人物形象的英國著名作家亞瑟.柯南道爾的經(jīng)典小說。故事講述的是在一塊未開發(fā)的土地上遭遇恐龍的危險經(jīng)歷。 一名孤獨的探險家死去了,他那破舊的、包有皮邊的筆記本便成為因時間而被淡忘了的史前高原探險活動的惟一的線索。 在倫敦,愛德華·查林杰教授召集了擅長不同領(lǐng)域的冒險家,組建了一支探險隊,決心證實遺失的世界的存在,在地圖上未標明的叢林中探險。 在亞馬遜叢林一片被時間遺忘的高原土地上,科學探險隊的幾位成員在尋找離開高原的路徑。他們必須防御來自原始部落獵人們的襲擊。他們在野外的高原上遇阻,無法返回,而這里又是一個令人害怕的世界,時常出沒一些史前的食肉動物、原始的猿人、奇特的植物和吸血的蝙蝠。為了生存,這群命運不濟的人必須團結(jié)起來,拋棄個人之間的喜好和偏見,隨時準備應(yīng)付任何可能突發(fā)的情況。在野性叢林美女維羅尼卡的幫助下,手中只有幾只獵槍的他們用智能一次又一次擺脫了死亡的威脅。",
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":"阿爾法戰(zhàn)士 第一季"
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: ╚════════════════════════════════════════════════════════════════════════════════════════
通過這樣的方式打印出來的網(wǎng)絡(luò)請求日志包含了所有的網(wǎng)絡(luò)信息, 并且結(jié)構(gòu)層次非常清晰。
源碼:https://github.com/xiaoyanger0825/LogHttpInfo