我们公司在项目中使用的网络请求工具是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
网友评论
OkHttpClient.Builder builder = new OkHttpClient.Builder();
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
HttpLoggingInterceptor logInterceptor = new HttpLoggingInterceptor(new HttpLogger());
logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
// builder.addNetworkInterceptor(new MyLoggerInterceptor(this.getClass().getSimpleName()));
builder.readTimeout(10_000L, TimeUnit.MILLISECONDS);
builder.writeTimeout(10_000L, TimeUnit.MILLISECONDS);
builder.connectTimeout(10_000L, TimeUnit.MILLISECONDS);
builder.addNetworkInterceptor(logInterceptor);
mClient = builder.build();
这样配置完了,为什么我的打印不出reponse的响应体来,响应结果是这个
D/HttpLogInfo: Server: nginx/1.0.15
D/HttpLogInfo: Date: Wed, 18 Jan 2017 11:20:48 GMT
D/HttpLogInfo: Content-Type: application/json;charset=utf-8
D/HttpLogInfo: Transfer-Encoding: chunked
D/HttpLogInfo: Connection: keep-alive
D/HttpLogInfo: Vary: Accept-Encoding
D/HttpLogInfo: Content-Encoding: gzip
D/HttpLogInfo: <-- END HTTP (encoded body omitted)