一、拦截器的作用
拦截器可以拿到网络请求的 Request 对象和 Response 对象,有了这两个对象我们就可以对网络请求进行监听(打印日志)、缓存、修改 HTTP 的请求报文和响应报文。
二、如何使用拦截器
- 实现
Interceptor
接口,重写intercept()
函数获取并返回响应Response
的对象。下面是一个日志打印的例子。
public class TestInterceptor implements Interceptor {
private static final String TAG = "TestInterceptor";
@Override
public Response intercept(Chain chain) throws IOException {
Log.d(TAG, "拦截器开始");
// 获取请求对象
Request request = chain.request();
long t1 = System.nanoTime();
Log.d(TAG, String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
// 发起HTTP请求,并获取响应对象
Response response = chain.proceed(request);
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()));
return response;
}
}
- 在建造
OkHttpClient
类的对象时调用addInterceptor()
或addNetworkInterceptor()
方法设置拦截器,下面的例子先使用addInterceptor()
。
public void httpGetClick(View view) {
String url = "https://publicobject.com/helloworld.txt";
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new TestInterceptor())
.build();
final Request request = new Request.Builder()
.url(url)
.get()
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
showData("onFailure: " + e.getMessage());
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
String responseStr = (response.body() == null) ? "返回结果为空" : response.body().string();
showData(responseStr);
}
});
}
三、addInterceptor 和 addNetworkInterceptor 的区别
- Application Interceptors
- 使用
addInterceptor()
注册 - 有无网络都会被调用到
-
application
拦截器只会被调用一次,调用chain.proceed()
得到的是重定向之后最终的响应信息,不会通过chain.connection()
获得中间过程的响应信息 - 允许 short-circuit (短路) 并且允许不去调用
chain.proceed()
请求服务器数据,可通过缓存来返回数据。
- Network Interceptors
- 使用
addNetworkInterceptor()
注册 - 无网络时不会被调用
- 可以显示更多的信息,比如 OkHttp 为了减少数据的传输时间以及传输流量而自动添加的请求头 Accept-Encoding: gzip 希望服务器能返回经过压缩过的响应数据。
-
chain.connection()
返回不为空的Connection
对象可以查询到客户端所连接的服务器的IP地址以及TLS配置信息。
以下是使用 Network Interceptors
的代码
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(new TestInterceptor())
.build();
final Request request = new Request.Builder()
.url(url)
.header("User-Agent", "OkHttp Example")
.build();
四、拦截器的调用的顺序
- OkHttp 利用 List 集合去跟踪并且保存这些拦截器,并且会依次遍历调用。调用顺序是先按
addInterceptor()
设置的顺序遍历,再按addNetworkInterceptor()
设置的顺序遍历,返回的结果则是反着来,这里看官方文档的图会直观一些。
- 调用顺序实践代码
需注意的是使用
addNetworkInterceptor()
设置的Network Interceptors
在请求发生重定向时会调用两次。
这里设置的 OfflineCacheInterceptor
、NetCacheInterceptor
、LoggingInterceptor
分别是无网缓存拦截器、日志拦截器、有网缓存拦截器。
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new OfflineCacheInterceptor())
.addNetworkInterceptor(new NetCacheInterceptor())
.addInterceptor(new LoggingInterceptor())
.cache(cache)
.build();
运行结果如下
无网缓存拦截器: 开始
日志拦截器: 开始
有网缓存拦截器: 开始
有网缓存拦截器: 结束
有网缓存拦截器: 开始
有网缓存拦截器: 结束
日志拦截器: 结束
无网缓存拦截器: 结束
- 调用顺序总结:由以上代码运行结果可知。
- 应该先设置完
Application Interceptors
再去设置Network Interceptors
混着设置不便于代码阅读。 -
日志拦截器的最佳位置是设置在
addInterceptor()
的第一个,这个位置的拦截器可以拿到最原始的请求信息以及最终的响应信息。 - 无网的缓存逻辑应使用
addInterceptor()
设置 - 有网的缓存逻辑应使用
addNetworkInterceptor()
设置
五、拦截器进阶之-网络缓存
在创建 OkHttpClient 对象的时候是可以设置 Cache 对象以实现缓存的,但这个方法是需要服务器返回的响应报文里面含有 Cache-Control 信息的,如果服务器未设置该值则需要我们自己去增加该信息,以下是服务器不支持缓存的情况下又想实现的需求。
- 项目需求
- 有网状态下的缓存过期时间可控。如设置为6秒可防止用户暴力刷新以减轻服务器压力,也可设置为更小的值以保证数据的实时性。
- 无网状态下的缓存过期时间可控。如设置为一天,或者永久(Integer.MAX_VALUE,21亿秒左右)
和无网状态下缓存的时间不一样,例如有网的时候10秒内读取缓存数据,无网的时候则长期读取缓存数据。 - 可以缓存 get 和 post 请求
注意: OkHttp 默认只支持 Get 请求缓存。
- 前置知识-HTTP消息报头
- Expires 首部
HTTP/1.0+ 时服务器用来指定过期日期的首部,其使用的是绝对日期。现在更常用的是使用 HTTP/1.1 的 Cache-Control 首部代替,其使用的是相对日期。 - Pragma 首部
用来包含实现特定的指令,最常用的是 Pragma:no-cache。在 HTTP/1.1 协议中,它的含义和 Cache-Control:no-cache 相同。 - Cache-Control 首部
位于通用报头,用来指定缓存的指令。缓存指令是单向的(响应时出现的缓存指令在请求是未必会出现)并且也是独立的(一个消息处理的缓存指令不会影响另一个消息处理的缓存机制)。下面是一些会用到的指令。- public : 指示响应可被任何缓存区缓存。
- private : 指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当前用户的部分响应消息,此响应消息对于其他用户的请求无效。
- max-age : 定义了缓存过期时间(从第一次生成文档到无法使用为止)单位为秒,没有超出则不管怎么样都是返回缓存数据,超出了则发起新的请求获取数据更新,请求失败返回缓存数据。
- max-stale : 定义了强制使用缓存的时间,单位为秒。没有超过则不管怎么样都返回缓存数据,超过了则发起请求获取更新数据,请求失败返回失败 (response.code() 为 504)。
- only-if-cached : 只使用缓存
- no-cache : 指示请求或响应消息不能缓存
- no-store : 用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
- 定义无网请求的拦截器
public class OfflineCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String method = request.method();
if (!NetState.isNetworkConnected(MyApplication.getContext())) {
if ("GET".equals(method)) {
// 缓存过期时间,超过时则 response.body().contentLength() 为 0
int offlineCacheTime = Integer.MAX_VALUE;
request = request.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=" + offlineCacheTime)
.build();
} else if ("POST".equals(method)) {
}
}
return chain.proceed(request);
}
}
okhttp 官方文档建议使用 CacheControl 构造缓存方法,里面也有一些配置好的缓存策略,例如修改上面的代码,使用 cacheControl(CacheControl.FORCE_CACHE)
也可达到无网时强制读取缓存的效果。
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
不想使用缓存时也可设置 cacheControl(CacheControl.FORCE_NETWORK)
- 定义有网请求的拦截器
public class NetCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
// 在线的时候的缓存过期时间,如果想要不缓存,直接时间设置为0
int onlineCacheTime = 30;
return response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public, max-age=" + onlineCacheTime)
.build();
}
}
- 设置拦截器
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.addInterceptor(new OfflineCacheInterceptor())
.addNetworkInterceptor(new NetCacheInterceptor())
.cache(cache)
.build();
- 增加对 POST 请求的缓存
这里使用比较简单的数据库存储的方法实现,主要思路如下。
- 在应用拦截器里取得 POST 请求的地址以及请求体。
- 使用请求的地址以及请求体生成一个请求的唯一标识,以区分不同的请求。
- 有网状态下查询缓存是否命中,未命中则存入响应信息;命中则判断缓存是否过期,过期则重新存入,未过期则取出。
- 无网状态下查询缓存是否命中,未命中则回调错误信息;命中则判断缓存是否过期,过期回调错误信息,未过期则取出。
public class OfflineCacheInterceptor implements Interceptor {
private static final CacheControl FORCE_MY_CACHE = new CacheControl.Builder()
.onlyIfCached()
.maxStale(InterceptorConfiguration.OFFLINE_CACHE_TIME, TimeUnit.SECONDS)
.build();
@Override
public Response intercept(Chain chain) throws IOException {
InterceptorConfiguration.debugLog("无网缓存拦截器开始");
Request request = chain.request();
Response response = null;
String requestUrl = InterceptorConfiguration.getUrl(request);
String method = request.method();
if (!NetState.isNetworkConnected(MyApplication.getContext())) {
request = request.newBuilder().cacheControl(FORCE_MY_CACHE).build();
if ("GET".equals(method)) {
response = chain.proceed(request);
} else if ("POST".equals(method)) {
InterceptorConfiguration.debugLog("读取POST缓存");
HttpDbBean httpDbBean = HttpDbBean.getByUrl(requestUrl);
if (httpDbBean != null && !TextUtils.isEmpty(httpDbBean.getResponse())) {
String b = httpDbBean.getResponse();
byte[] bs = b.getBytes();
response = new Response.Builder()
.request(request)
.body(ResponseBody.create(MediaType.parse("application/json"), bs))
.message(httpDbBean.getMessage())
.code(httpDbBean.getCode())
.protocol(Protocol.HTTP_1_1)
.build();
}
}
} else {
response = chain.proceed(request);
String responseBodyString = InterceptorConfiguration.getResponseBody(response);
if ("POST".equals(method)) {
// 保存请求信息
LitePal.deleteAll(HttpDbBean.class, "url = ?", requestUrl);
HttpDbBean saveHttpBean = new HttpDbBean(requestUrl, responseBodyString, response.message(), response.code());
saveHttpBean.save();
}
}
InterceptorConfiguration.debugLog("无网缓存拦截器结束");
// 为空则抛异常交给调用层的回掉处理
if (response == null) throw new IOException() {
@Override
public String getMessage() {
return "The requested address is empty in the database";
}
};
return response;
}
}
注意:!!!
response.body() 使用完之后应调用 response.body().close() 源码文档强制要求
reponse.body().string() 只能被调用一次,因为调用完之后源码会关闭 IO 流Util.closeQuietly(source);
。
未知问题,在无网时从数据库中读取数据拼接成响应体时不论设置请求还是设置响应的缓存过期时间都没用。
response = new Response.Builder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public, only-if-cached, max-stale=" + 6)
.request(request.newBuilder().cacheControl(FORCE_MY_CACHE).build())
.body(ResponseBody.create(MediaType.parse("application/json"), bs))
.message(httpDbBean.getMessage())
.code(httpDbBean.getCode())
.protocol(Protocol.HTTP_1_1)
.build();