OkHttp Interceptor 入门到进阶

一、拦截器的作用

拦截器可以拿到网络请求的 Request 对象和 Response 对象,有了这两个对象我们就可以对网络请求进行监听(打印日志)、缓存、修改 HTTP 的请求报文和响应报文。

二、如何使用拦截器

  1. 实现 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;
    }
}
  1. 在建造 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 的区别

  1. Application Interceptors
  • 使用 addInterceptor() 注册
  • 有无网络都会被调用到
  • application 拦截器只会被调用一次,调用 chain.proceed() 得到的是重定向之后最终的响应信息,不会通过 chain.connection() 获得中间过程的响应信息
  • 允许 short-circuit (短路) 并且允许不去调用 chain.proceed() 请求服务器数据,可通过缓存来返回数据。
  1. 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();

四、拦截器的调用的顺序

  1. OkHttp 利用 List 集合去跟踪并且保存这些拦截器,并且会依次遍历调用。调用顺序是先按 addInterceptor() 设置的顺序遍历,再按 addNetworkInterceptor() 设置的顺序遍历,返回的结果则是反着来,这里看官方文档的图会直观一些。
  1. 调用顺序实践代码

需注意的是使用 addNetworkInterceptor() 设置的 Network Interceptors 在请求发生重定向时会调用两次。

这里设置的 OfflineCacheInterceptorNetCacheInterceptorLoggingInterceptor 分别是无网缓存拦截器日志拦截器有网缓存拦截器

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new OfflineCacheInterceptor())
                .addNetworkInterceptor(new NetCacheInterceptor())
                .addInterceptor(new LoggingInterceptor())
                .cache(cache)
                .build();

运行结果如下

无网缓存拦截器: 开始
日志拦截器: 开始
有网缓存拦截器: 开始
有网缓存拦截器: 结束
有网缓存拦截器: 开始
有网缓存拦截器: 结束
日志拦截器: 结束
无网缓存拦截器: 结束
  1. 调用顺序总结:由以上代码运行结果可知。
  • 应该先设置完 Application Interceptors 再去设置 Network Interceptors 混着设置不便于代码阅读。
  • 日志拦截器的最佳位置是设置在 addInterceptor() 的第一个,这个位置的拦截器可以拿到最原始的请求信息以及最终的响应信息。
  • 无网的缓存逻辑应使用 addInterceptor() 设置
  • 有网的缓存逻辑应使用 addNetworkInterceptor() 设置

五、拦截器进阶之-网络缓存

在创建 OkHttpClient 对象的时候是可以设置 Cache 对象以实现缓存的,但这个方法是需要服务器返回的响应报文里面含有 Cache-Control 信息的,如果服务器未设置该值则需要我们自己去增加该信息,以下是服务器不支持缓存的情况下又想实现的需求。

  1. 项目需求
  • 有网状态下的缓存过期时间可控。如设置为6秒可防止用户暴力刷新以减轻服务器压力,也可设置为更小的值以保证数据的实时性。
  • 无网状态下的缓存过期时间可控。如设置为一天,或者永久(Integer.MAX_VALUE,21亿秒左右)
    和无网状态下缓存的时间不一样,例如有网的时候10秒内读取缓存数据,无网的时候则长期读取缓存数据。
  • 可以缓存 get 和 post 请求

注意: OkHttp 默认只支持 Get 请求缓存。

  1. 前置知识-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 : 用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
  1. 定义无网请求的拦截器
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)

  1. 定义有网请求的拦截器
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();
    }
}
  1. 设置拦截器
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new LoggingInterceptor())
                .addInterceptor(new OfflineCacheInterceptor())
                .addNetworkInterceptor(new NetCacheInterceptor())
                .cache(cache)
                .build();
  1. 增加对 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();

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容