参考:
Okhttp-wiki 之 Interceptors 拦截器
dingProg /NetworkCaptureSelf :app上监听网络日志
简介
OkHttp 中的拦截器分为 Application Interceptor(应用拦截器) 和 NetWork Interceptor(网络拦截器)两种,
下面以 LoggingInterceptor 为例来展示这两种注册方式的区别:
• Application Interceptor(应用拦截器)
通过调用 OkHttpClient.Builder 的 addInterceptor() 方法来注册应用拦截器
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
上段代码中的URL http://www.publicobject.com/helloworld.txt
被重定向到了 https://publicobject.com/helloworld.txt,
OkHttp会自动跟随此重定向。此时的应用拦截器会被调用一次,
并且返回的 chain.proceed() 响应是重定向后的响应。
• Network Interceptor(网络拦截器)
通过调用 OkHttpClient.Builder 的 addNetworkInterceptor() 方法来注册网络拦截器
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new LoggingInterceptor())
.build();
Request request = new Request.Builder()
.url("http://www.publicobject.com/helloworld.txt")
.header("User-Agent", "OkHttp Example")
.build();
Response response = client.newCall(request).execute();
response.body().close();
当我们运行此代码,拦截器会运行两次,
一次用于初始请求 http://www.publicobject.com/helloworld.txt,
另一次用于重定向 https://publicobject.com/helloworld.txt。
很明显的,网络拦截器的请求包含了更多信息,
比如 OkHttp 为了减少数据的传输时间以及传输流量而自动添加的请求头 Accept-Encoding:gzip 希望服务器能返回已压缩过的响应数据。
网络拦截器下的 Chain 具有一个非空的 Connection 对象,
它可以用来查询客户端所连接的服务器的IP地址以及TLS配置信息。
Chain的源码中也说明了只有在网络拦截器下的 chain 才能使用,
而应用拦截器下的 chain.connection() 总是返回 null。
应用拦截器和网络拦截器的区别
每种拦截器都有各自的优点:
应用拦截器
- 不能操作中间的响应结果,比如重定向和重试,只能操作客户端主动的第一次请求以及最终的响应结果。
- 始终调用一次,即使Http响应是从缓存中提供的。
- 关注原始的request,而不关心注入的headers,比如If-None-Match。
- 允许短路 short-circuit ,并且不调用 chain.proceed()。(注:这句话的意思是Chain.proceed()不需要一定要获取来自服务器的响应,但是必须还是需要返回Respond实例。那么实例从哪里来?答案是缓存。如果本地有缓存,可以从本地缓存中获取响应实例返回给客户端。这就是short-circuit (短路)的意思)
- 允许请求失败重试,并多次调用 chain.proceed();
网络拦截器
- 能够对重定向和重试等中间响应进行操作
- 不允许调用缓存来short-circuit (短路)这个请求。(注:意思就是说不能从缓存池中获取缓存对象返回给客户端,必须通过请求服务的方式获取响应,也就是Chain.proceed())
- 观察网络传输中数据传输和变化(注:比如当发生了重定向时,我们就能通过网络拦截器来确定存在重定向的情况)
- 可以获取 Connection 携带的请求信息(即可以通过chain.connection() 获取非空对象)
常见拦截器
import java.io.IOException;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* 添加缓存
*/
public class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
//第一步,获得chain内的request
Request request = chain.request();
//没网强制从缓存读取
// (必须得写,不然断网状态下,退出应用,或者等待一分钟后,就获取不到缓存)
if (!NetWorkUtils.isNetworkAvailable(MyApplication.getInstance())) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
//第二步,用chain执行request,获取response
Response response = chain.proceed(request);
Response responseLatest;
if (NetWorkUtils.isNetworkAvailable(MyApplication.getInstance())) {
// int maxAge = 0;// 有网络时 设置缓存超时时间0个小时
int maxAge = 60; //有网失效一分钟
responseLatest = response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control",
"public, max-age=" + maxAge)
.build();
} else {
// int maxStale = 60 * 60 * 24 * 28;// 无网络时,设置超时为4周
int maxStale = 60 * 60 * 6; // 没网失效6小时
responseLatest = response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control",
"public, only-if-cached, max-stale=" + maxStale)
.build();
}
//第三步,返回response
return responseLatest;
}
}
import android.util.Log;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* 添加请求头
*/
public class HeaderInterceptor implements Interceptor {
private static final String TAG = HeaderInterceptor.class.getSimpleName();
@Override
public Response intercept(Chain chain) throws IOException {
//第一步,获得chain内的request
Request request = chain.request();
Log.d(TAG, "intercept" + "处理开始");
//第二步,用chain执行request,获取response
Request.Builder builder = request.newBuilder();
builder.addHeader("ROLE", "1")
.addHeader("TOKEN", "")
.addHeader("APPCLIENT", "android");
Request requestBuild = builder.build();
Response response = chain.proceed(requestBuild);
Log.d(TAG, "intercept" + "处理完");
//第三步,返回response
return response;
}
}
import android.util.Log;
import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* 请求日志
*/
class LoggingInterceptor implements Interceptor {
private static final String TAG = LoggingInterceptor.class.getSimpleName();
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
//第一步,获得chain内的request
Request request = chain.request();
//处理request信息
long t1 = System.nanoTime();
Log.d(TAG, String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
//第二步,用chain执行request,获取response
Response response = chain.proceed(request);
//处理response信息
long t2 = System.nanoTime();
Log.d(TAG, String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
//第三步,返回response
return response;
}
}
import java.io.IOException;
import java.util.Iterator;
import java.util.Set;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* 加密query内容
*/
public class EncodeDataInterceptor implements Interceptor {
private String newHost = "127.0.0.1";
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl url = request.url();
//http://127.0.0.1/test/upload/img?userName=xiaoming&userPassword=12345
String scheme = url.scheme();// http https
String host = url.host();// 127.0.0.1
String path = url.encodedPath();// /test/upload/img
String query = url.encodedQuery();// userName=xiaoming&userPassword=12345
StringBuffer sb = new StringBuffer();
sb.append(scheme).append(newHost).append(path).append("?");
Set<String> queryList = url.queryParameterNames();
Iterator<String> iterator = queryList.iterator();
for (int i = 0; i < queryList.size(); i++) {
String queryName = iterator.next();
sb.append(queryName).append("=");
String queryKey = url.queryParameter(queryName);
//对query的key进行加密
sb.append(CommonUtils.getMD5(queryKey));
if (iterator.hasNext()) {
sb.append("&");
}
}
String newUrl = sb.toString();
Request.Builder builder = request.newBuilder()
.url(newUrl);
return chain.proceed(builder.build());
}
}
import java.io.IOException;
import java.util.Iterator;
import java.util.Set;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.Buffer;
/**
* 加密query内容
*/
public class EncodeBodyInterceptor implements Interceptor {
private String newHost = "127.0.0.1";
public static String requestBodyToString(RequestBody requestBody) throws IOException {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
return buffer.readUtf8();
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl url = request.url();
//http://127.0.0.1/test/upload/img?userName=xiaoming&userPassword=12345
String scheme = url.scheme();// http https
String host = url.host();// 127.0.0.1
String path = url.encodedPath();// /test/upload/img
String query = url.encodedQuery();// userName=xiaoming&userPassword=12345
StringBuffer sb = new StringBuffer();
sb.append(scheme).append(newHost).append(path).append("?");
Set<String> queryList = url.queryParameterNames();
Iterator<String> iterator = queryList.iterator();
for (int i = 0; i < queryList.size(); i++) {
String queryName = iterator.next();
sb.append(queryName).append("=");
String queryKey = url.queryParameter(queryName);
//对query的key进行加密
sb.append(CommonUtils.getMD5(queryKey));
if (iterator.hasNext()) {
sb.append("&");
}
}
String newUrl = sb.toString();
RequestBody body = request.body();
String bodyToString = requestBodyToString(body);
TestBean testBean = GsonTools.changeGsonToBean(bodyToString, TestBean.class);
String userPassword = testBean.getUserPassword();
//加密body体中的用户密码
testBean.setUserPassword(CommonUtils.getMD5(userPassword));
String testGsonString = GsonTools.createGsonString(testBean);
RequestBody requestBody = RequestBody.create(
MediaType.parse("application/json"),
testGsonString
);
Request.Builder builder = request.newBuilder()
.post(requestBody)
.url(newUrl);
return chain.proceed(builder.build());
}
}
import java.io.IOException;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* URL重定向
*/
public class HostInterceptor implements Interceptor {
private String newHost = "127.0.0.1";
@Override
public Response intercept(Chain chain) throws IOException {
//第一步,获得chain内的request
Request request = chain.request();
HttpUrl url = request.url();
//http://127.0.0.1/test/upload/img?userName=xiaoming&userPassword=12345
String scheme = url.scheme();// http https
String host = url.host();// 127.0.0.1
String path = url.encodedPath();// /test/upload/img
String query = url.encodedQuery();// userName=xiaoming&userPassword=12345
StringBuffer sb = new StringBuffer();
String newUrl = sb.append(scheme)
.append(newHost)
.append(path)
.append("?")
.append(query)
.toString();
Request.Builder builder = request.newBuilder()
.url(newUrl);
//第二步,用chain执行request,获取response
//第三步,返回response
return chain.proceed(builder.build());
}
}
ByteStrings and Buffers
Okio是围绕这两种类型构建的,它们将大量功能打包到一个简单的API中:
ByteString是不可变的字节序列。
对于字符数据,最基本的就是String。
而ByteString就像是String的兄弟一般,它使得将二进制数据作为一个变量值变得容易。
这个类很聪明:它知道如何将自己编码和解码为十六进制、base64和utf-8。
Buffer是一个可变的字节序列。
像Arraylist一样,你不需要预先设置缓冲区的大小。
你可以将缓冲区读写为一个队列:将数据写到队尾,然后从队头读取。
在内部,ByteString和Buffer做了一些聪明的事情来节省CPU和内存。
如果您将UTF-8字符串编码为ByteString,它会缓存对该字符串的引用,这样,如果您稍后对其进行解码,就不需要做任何工作。
Buffer是作为片段的链表实现的。
当您将数据从一个缓冲区移动到另一个缓冲区时,它会重新分配片段的持有关系,而不是跨片段复制数据。
这对多线程特别有用:与网络交互的子线程可以与工作线程交换数据,而无需任何复制或多余的操作。
Sources and Sinks
java.io设计的一个优雅部分是如何对流进行分层来处理加密和压缩等转换。
Okio有自己的stream类型: Source和Sink,分别类似于java的Inputstream和Outputstream,
但是有一些关键区别:
超时(Timeouts)。
流提供了对底层I/O超时机制的访问。
与java.io的socket字流不同,read()和write()方法都给予超时机制。
易于实施。
source只声明了三个方法:read()、close()和timeout()。
没有像available()或单字节读取这样会导致正确性和性能意外的危险操作。
使用方便。
虽然source和sink的实现只有三种方法可写,
但是调用方可以实现Bufferedsource和Bufferedsink接口, 这两个接口提供了丰富API能够满足你所需的一切。
字节流和字符流之间没有人为的区别。
都是数据。你可以以字节、UTF-8字符串、big-endian的32位整数、little-endian的短整数等任何你想要的形式进行读写;
不再有InputStreamReader!
易于测试。
Buffer类同时实现了BufferedSource和BufferedSink接口,因此测试代码简单明了。
Sources 和 Sinks分别与InputStream 和 OutputStream交互操作。
你可以将任何Source看做InputStream ,也可以将任何InputStream 当做Source。对于Sink和Outputstream也是如此。