对整个OkHttp框架的介绍,会分为使用篇和源码分析篇两个部分进行介绍:
这里是使用篇的目录:
(一)-基本使用
(二)-常用类介绍
(三)-Interceptor
源码分析篇敬请期待……
在上一篇文章中,我们简要介绍了一下OkHttp中的常用类。但是其实还有个非常重要的概念在之前的文章中都没有进行说明,那就是拦截器(interceptor)。本篇文章,我们继续介绍其Interceptor的用法。在OkHttp框架中,Interceptor算是其强大的原因所在。我们平时很多功能(如下载进度监听、缓存策略设置、日志打印等)都需要通过Interceptor来实现。
Interceptor介绍
介绍具体类之前,还是先弄个图给大家看看Interceptor是怎么工作的:
从图中可以看到,这里Interceptor发挥作用的地方有两处,一处是在进入OkHttp Core之前,一处是再之后。关于在这两个地方的不同,后面会有介绍。但是他们处理流程是一样的,从请求到响应整个过程,Interceptor链会形成一个"U"型结构。
了解完处理流程后,进入到这个类的学习吧。我们先看下下Interceptor这个类的源码:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
……
}
}
可以看到,其非常简单,就是一个接口。我们使用的时候就是实现Interceptor接口,然后重写其intercept(Chain chain)
方法即可。
在该方法中,可以通过Chain
对象获取到原有的Request信息,然后可以对其进行修改、加工,然后调用Chain.process(request)
方法放行往后执行,拿到Response。拿到Response之后,我们也可以在这里对Reponse进行处理。最后返回修改后的Reponse给后续的Interceptor使用,比如官方的提供的日志拦截器HttpLoggingInterceptor
,其实现如下:
public final class HttpLoggingInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
Level level = this.level;
// 拿到之前的Request信息
Request request = chain.request();
String requestStartMessage = "--> "
// 中间经过一系列的处理,从Request中获得需要打印的信息拼接到requestStartMessage
……
// 打印Request的部分日志信息
logger.log(requestStartMessage);
// 又经过一系列的判断,打印出Header等各种日志信息
……
Response response;
try {
// 这里放行往后执行,然后拿到Response对象
response = chain.proceed(request);
} catch (Exception e) {
logger.log("<-- HTTP FAILED: " + e);
throw e;
}
……
// 打印code、message等返回信息
logger.log("<-- "
+ response.code()
+ (response.message().isEmpty() ? "" : ' ' + response.message())
+ ' ' + response.request().url()
+ " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')');
// 中间又打印了一些返回的其他信息
……
// 最后返回reponse对象给上层。
return response;
}
……
}
中间删除了不少代码,但是核心流程逻辑已经体现的很清楚了。Interceptor的使用大体也就是这些步骤。只是针对不同的需求,我们做的处理不太一样而已。
这里我们拿一些经常使用的场景进行举例。
2 使用举例
下面通过两个例子进行说明,一是通过Interceptor做缓存控制,另一个是通过Interceptor添加公共请求参数。不过这两个例子使用Kotlin语音实现的,Kotlin语言这块就各位自己熟悉了。
2.1 缓存控制
添加缓存策略的Interceptor,我们可以获取到Request后,修改Headers信息,加上我们自己的缓存策略,示例如下
class CacheHeaderInterceptor:Interceptor {
override fun intercept(chain: Interceptor.Chain?): Response {
LogUtils.d("CacheHeaderInterceptor---->intercept()")
// 设置缓存配置
val cacheBuilder = CacheControl.Builder()
cacheBuilder.maxAge(0, TimeUnit.SECONDS)
cacheBuilder.maxStale(30, TimeUnit.DAYS)
val cacheControl = cacheBuilder.build()
// 获取到之前的Request
var request = chain!!.request()
val method = request.method()
// 判断请求方法,GET才缓存
if (method.toUpperCase() == HttpConstant.GET_METHOD && !NetWorkUtils.isNetWorkAvailable(BaseApplication.instance)) {
request = request.newBuilder()
.cacheControl(cacheControl)
.build()
}
val originalResponse = chain.proceed(request)
// 根据网络状况,做不同的缓存策略
return if (NetWorkUtils.isNetWorkAvailable(BaseApplication.instance)) {
val maxAge = 0 // read from cache
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public ,max-age=$maxAge")
.build()
} else {
val maxStale = 60 * 60 * 24 * 28 // 4周
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, only-if-cached, max-stale=$maxStale")
.build()
}
}
}
可以看到,步骤上面的处理和之前的日志打印Interceptor一样。具体操作在代码里面已经注释的比较清楚了,中间有些涉及到HTTP协议中头信息的东西,这个不理解的就需要各位自行百度下了。
2.2 添加公共请求参数
实际使用当中,另外一个经常需要用到Interceptor的场景就是对公共参数的封装。比如一般的应用同后台交互的过程中,都会有个类似token的标识用户信息的字段,几乎所有的接口都会需要该参数,这个时候如果我们在每个接口当中去添加该参数就会显得很麻烦,而且也不利于后期维护,如果后台突然哪天把参数的key给变了(如从token变为userToken),又或者需要添加一个新的公共参数(如:当前应用版本appVersion)。如果不是同一管理和封装,那你每个接口都需要去重新修改,就会超级麻烦。
说了这么多,还是撸代码来的现实些:
class TokenInterceptor : Interceptor {
companion object {
private const val TAG = "TokenInterceptor"
private const val TOKEN_KEY = "token"
}
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val url = request.url()
val token = UserStorage.getToken()
if (!TextUtils.isEmpty(token)) {
val method = request.method()
// 不同的请求方法 添加公共参数的方式不一样
if (method.toUpperCase() == GET_METHOD) {
// Get方法 添加到url里面
request = request.newBuilder()
.url(
url.newBuilder()
.addEncodedQueryParameter(TOKEN_KEY, token)
.build()
).build()
} else { // 这里只是假设请求就GET和POST两种方式
// 这里就是针对POST 方法添加到请求体中
val requestBody = request.body()
request = when (requestBody) {
is FormBody -> // 表单形式
request.newBuilder().post(getNewRequestBody(requestBody, token)).build()
is MultipartBody -> // Multipart形式提交
request.newBuilder().post(getNewRequestBody(requestBody, token)).build()
// 其他形式
else -> request.newBuilder().post(
getNewRequestBody(
requestBody,
token
)
).build()
}
}
}
return chain.proceed(request)
}
// 往除了FormBody和MultipatBody的以外的其他RequestBody中添加公共参数
// 这里加入后台需要的封装为JSON格式的数据
private fun getNewRequestBody(request: RequestBody?, userToken: String): RequestBody {
if (request == null) {
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), "")
}
try {
val string = bodyToString(request)
var params: HashMap<String, Any>? = null
if (!TextUtils.isEmpty(string)) {
val jsonObject = JSONObject(string)
// 获取之前的封装的参数
params = Gson().fromJson<HashMap<String, Any>>(jsonObject.toString(), HashMap::class.java)
}
if (params == null) {
params = HashMap()
}
// 将token加入到参数当中
params[TOKEN_KEY] = userToken
// 重新封装为JSON格式数据返回
val toJson = Gson().toJson(params)
return RequestBody.create(MediaType.parse("application/json; charset=utf-8"), toJson)
} catch (e: Exception) {
e.printStackTrace()
}
return request
}
/**
* 往MultipartBody中添加token
*/
private fun getNewRequestBody(multipartBody: MultipartBody, userToken: String): RequestBody {
LogUtils.d("$TAG---->getNewRequestBody(MultipartBody)")
val bodyBuilder = MultipartBody.Builder()
for (part in multipartBody.parts()) {
bodyBuilder.addPart(part)
}
bodyBuilder.addFormDataPart(TOKEN_KEY, userToken)
return bodyBuilder.build()
}
/**
* 往FormBody中添加token
*/
private fun getNewRequestBody(formBody: FormBody, userToken: String): RequestBody {
LogUtils.d("$TAG---->getNewRequestBody(FormBody)")
val formBuilder = FormBody.Builder()
for (i in 0 until formBody.size()) {
formBuilder.addEncoded(formBody.encodedName(i), formBody.encodedValue(i))
}
formBuilder.addEncoded(TOKEN_KEY, userToken)
return formBuilder.build()
}
private fun bodyToString(request: RequestBody?): String {
try {
val buffer = Buffer()
if (request != null)
request.writeTo(buffer)
else
return ""
return buffer.readUtf8()
} catch (e: IOException) {
return "did not work"
}
}
}
这里处理比上面Cache处理稍微复杂点,所以提取了几个独立的方法出来。不过原理也是一样,截取到原本的Request,然后根据请求方式,提交方式的不同,往Request里面添加token参数后,再放行给后面的流程处理。
添加token过程中大部分都是上篇文章介绍的一些API,这里就不做阐述了,不了解的可以出现查看上篇文章。
我想通过上面两个例子,大家对Interceptor的使用应该有一定了解了。处理思路和流程都是那样的,只是针对具体的业务不同,中间处理过程不太一样而已。
上面只是说了我们怎么去自定义Interceptor,定义好之后,最终目的是的使用啊,使用非常简单如下:
OkHttpClient.Builder()
.addInterceptor(TokenInterceptor())
.addInterceptor(CacheHeaderInterceptor())
.addNetworkInterceptor(loggingInterceptor)
就是通过OkHttpClient.Builder
的adInterceptor(Interceptor)
或者addNetworkInterceptor(Interceptor)
添加进去就好了。这里有两种添加方式,那有什么不同呢,这就是我们下面需要讨论的问题,OkHttp中Interceptor的注册分类。
3 Interceptor注册分类
通常,根据上面添加(注册)方式的不同,可以将Interceptor分为Application Interceptor和NetWork Interceptor。
3.1 Application Interceptor
这个是指通过adInterceptor(Interceptor)
进行注册添加的Interceptor,它在整个请求过程中只会调用一次,而且拿到的Response是最终从服务器获得的结果。这里我们以官方指南里面的例子进行对比。
这里先给出自定义的LoggingInterceptor的代码:
class LoggingInterceptor implements Interceptor {
@Override public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
long t1 = System.nanoTime();
logger.info(String.format("Sending request %s on %s%n%s",
request.url(), chain.connection(), request.headers()));
Response response = chain.proceed(request);
long t2 = System.nanoTime();
logger.info(String.format("Received response for %s in %.1fms%n%s",
response.request().url(), (t2 - t1) / 1e6d, response.headers()));
return response;
}
}
自定义Interceptor创建好后,我们通过addInterceptor(new LoggingInterceptor()
注册到框架中:
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();
执行上述请求后,会得到如下日志输出:
INFO: Sending request http://www.publicobject.com/helloworld.txt on null
User-Agent: OkHttp Example
INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
可以看到这个请求过程中有重定向发生,因为Request中的url和最后获取的Response中的url地址不一样。所以可以判断发生了重定向(OkHttp框架支持自动重定向的)。但是这个LoggingInterceptor的拦截方法却只调用了一次。所以通过addInterceptor(new LoggingInterceptor()
注册的拦截器是跟踪不到中间重定向等信息的。
接下来我们再看另外一种注册方式。
3.2 NetWork Interceptor
同样,我们创建并发起一个请求:
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();
这次我们通过addNetworkInterceptor(new LoggingInterceptor())
进行注册。执行后,我们会得到如下日志输出:
INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1}
User-Agent: OkHttp Example
Host: www.publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/html
Content-Length: 193
Connection: keep-alive
Location: https://publicobject.com/helloworld.txt
INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1}
User-Agent: OkHttp Example
Host: publicobject.com
Connection: Keep-Alive
Accept-Encoding: gzip
INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms
Server: nginx/1.4.6 (Ubuntu)
Content-Type: text/plain
Content-Length: 1759
Connection: keep-alive
可以看到这里,完整打印出了整个请求过程的信息,包括中间OkHttp框架自动重定向的。
而且,Network 拦截器调用Chain.connection()
方法后会返回一个非空的Connection对象,它可以用来查询客户端所连接的服务器的IP地址以及TLS配置信息。
到这里,大致了解了两种注册方式的区别,下面再详细列出两者间的区别:
Application interceptors
- 无法操作中间的响应结果,比如当URL重定向发生以及请求重试等,只能操作客户端主动第一次请求以及最终的响应结果。
- 在任何情况下只会调用一次,即使这个响应来自于缓存。
- 可以监听观察这个请求的最原始未经改变的意图(请求头,请求体等),无法操作OkHttp为我们自动添加的额外的请求头,比如If-None-Match。
- 允许short-circuit (短路)并且允许不去调用Chain.proceed()。(这句话的意思是Chain.proceed()不需要一定要调用去服务器请求,但是必须还是需要返回Respond实例。那么实例从哪里来?答案是缓存。如果本地有缓存,可以从本地缓存中获取响应实例返回给客户端。)
- 允许请求失败重试以及多次调用Chain.proceed()。
Network Interceptors
- 允许操作中间响应,比如当请求操作发生重定向或者重试等。
- 不允许调用缓存来short-circuit (短路)这个请求。(意思就是说不能从缓存池中获取缓存对象返回给客户端,必须通过请求服务的方式获取响应,也就是Chain.proceed())
- 可以监听数据的传输
- 允许Connection对象装载这个请求对象。(编者注:Connection是通过Chain.proceed()获取的非空对象)
我们平时在使用的时候,就根据各自的特点和我们自己的业务需求,选择合适的注册方式了。
到这,我们整个OkHttp框架使用篇就介绍完了。整个OkHttp框架还有非常多的东西是没有介绍到的,想要面面俱到几乎是不可能的。这几篇文章也只是简单介绍了日常业务中常用的一些知识点,目的只是让大家对这个框架有个了解,入个门。入门后,随着自己在开发中慢慢去发现和摸索吧。
参考资料:
OkHttp3-拦截器(Interceptor)