OkHttp3使用(三)-Interceptor

对整个OkHttp框架的介绍,会分为使用篇和源码分析篇两个部分进行介绍:
这里是使用篇的目录:
(一)-基本使用
(二)-常用类介绍
(三)-Interceptor
源码分析篇敬请期待……

上一篇文章中,我们简要介绍了一下OkHttp中的常用类。但是其实还有个非常重要的概念在之前的文章中都没有进行说明,那就是拦截器(interceptor)。本篇文章,我们继续介绍其Interceptor的用法。在OkHttp框架中,Interceptor算是其强大的原因所在。我们平时很多功能(如下载进度监听、缓存策略设置、日志打印等)都需要通过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.BuilderadInterceptor(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)

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值