okhttp 应用之 Interceptors 拦截器,「实践 + 原理」一样都不少

文章目录

  • 前言
  • 一、拦截器
    • 1. 应用拦截器
    • 2. 网络拦截器
  • 二、选择?
    • 1. 应用拦截器
    • 2. 网络拦截器
    • 3. 重写请求
    • 4. 重写响应
  • 三、原理
    • 1. 提交请求:
    • 2. 拦截器链
    • 3. 执行请求
  • 总结


前言

参考源码版本 okhttp-3.14.9

okhttp 是什么?一款封装 HTTP 协议的 HTTP 客户端。

拦截器是 okhttp 提供的一个强有力的工具,我们可以在请求前后做监控、请求/响应进行重写、失败重试等操作。


一、拦截器

Okhttp 的拦截器采用了责任链模式,具体提供了两种拦截器:

  • 应用拦截器:专注于业务层的逻辑处理,比如 添加日志 …
  • 网络拦截器:专注于网络层请求的处理,比如 重定向、重试 …

这是官方提供的图片,你可以参考下:
okhttp 应用之 Interceptors 拦截器,「实践 + 原理」一样都不少_第1张图片

我们通过实际的例子来看看两种的区别,方便起,实现一个公用拦截器:

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;
  }
}

从名字可以看出,这是一个记录日志的拦截器。

官方提供的测试网址,http://www.publicobject.com/helloworld.txt,当我们访问该网址的时候,会重定向到 https://publicobject.com/helloworld.txt 地址。

现在,我们通过 okhttp 请求来看看两种拦截器的差别。


1. 应用拦截器

这里通过 addInterceptor(…) 添加应用拦截器:

    /**
     * 应用层拦截器
     */
    @Test
    public void testApplicationInterceptors() throws IOException {
        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();
        Objects.requireNonNull(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

从结果可以看出,控制台主要输出了我们请求前和请求后的日志信息。


2. 网络拦截器

通过 addNetworkInterceptor(…) 添加网络拦截器:

    /**
     * 网络层拦截器
     */
    @Test
    public void testNetworkInterceptors() throws IOException {
        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();
        Objects.requireNonNull(response.body()).close();
    }

再来看看处理结果:

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

你应该也看出差别了,这次控制台输出了两份 请求 + 响应 信息:

  • 第一次请求地址是:https://www.publicobject.com/helloworld.txt,然后响应中告诉我们要重定向到 https://publicobject.com/helloworld.txt 地址。
  • 根据重定向要求,第二次请求地址是:https://publicobject.com/helloworld.txt,然后返回正常响应结果。

另外,输出里还包含了更多的网络层信息,比如 okhttp 添加的头信息:Accept-Encoding: gzip


二、选择?

两种拦截器都有各自的应用场景,我们该如何选择?


1. 应用拦截器

  • 不需要考虑重定向和重试等中间响应。
  • 仅调用一次,即使 http 响应是从缓存中获取的结果。
  • 主要关注应用程序的原始意图,不关心 okhttp 注入的头,如 If-None-Match …
  • 允许短路操作,即 不调用 Chain.proceed()。
  • 允许重试并多次调用 Chain.proceed()
  • 可以使用 withConnectTimeout, withReadTimeout, withWriteTimeout 来调整 Call 超时时间。

2. 网络拦截器

  • 能够操作中间处理过程,如重定向和重试。
  • 不关注 cache 层拦截的短路操作
  • 关注网络层数据传输
  • 执行 Connection 请求

3. 重写请求

有了拦截器,我们可以对 http 请求头的参数新增、删除和覆盖等操作。

如果你觉得请求体过大,数据传输耗时较高的话,你可以对请求体进行压缩:


final class GzipRequestInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
      return chain.proceed(originalRequest);
    }

    Request compressedRequest = originalRequest.newBuilder()
        .header("Content-Encoding", "gzip")
        .method(originalRequest.method(), gzip(originalRequest.body()))
        .build();
    return chain.proceed(compressedRequest);
  }

  private RequestBody gzip(final RequestBody body) {
    return new RequestBody() {
      @Override public MediaType contentType() {
        return body.contentType();
      }

      @Override public long contentLength() {
        return -1; // We don't know the compressed length in advance!
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
        body.writeTo(gzipSink);
        gzipSink.close();
      }
    };
  }
}

案例中,我们覆盖了请求头参数 Content-Encoding:gzip,也就是告诉服务器,这个一个压缩请求体,你需要用相同的方式来解压数据。

4. 重写响应

这通常比重写请求头更危险,因为它可能违反web服务器的期望!

当然,如果你处于一个棘手的情况,并准备处理结果,重写响应头是解决问题的一种强大方法。

例如,你可以修复服务器错误配置的 Cache-Control 响应头,以启用更好的响应缓存:

/** Dangerous interceptor that rewrites the server's cache-control header. */
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Response originalResponse = chain.proceed(chain.request());
    return originalResponse.newBuilder()
        .header("Cache-Control", "max-age=60")
        .build();
  }
};

通常,当它补充了web服务器上的相应修复时,这种方法工作得最好!


三、原理

老规矩,我们还是回归源码看看底层实现细节。

再回归下我们的测试案例:

    /**
     * 网络层拦截器
     */
    @Test
    public void testNetworkInterceptors() throws IOException {
        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();
        // 执行 okhttp 请求
        Response response = client.newCall(request).execute();
        Objects.requireNonNull(response.body()).close();
    }

1. 提交请求:

入口 okhttp3.RealCall#execute:

okhttp 应用之 Interceptors 拦截器,「实践 + 原理」一样都不少_第2张图片
可以看到,getResponseWithInterceptorChain 真正应用拦截器并执行 http 请求。


2. 拦截器链

okhttp3.RealCall#getResponseWithInterceptorChain:

okhttp 应用之 Interceptors 拦截器,「实践 + 原理」一样都不少_第3张图片
看到拦截器的装配了!!!离真相不远了。

该方法主要做了两件大事:

  • 为请求组装拦截器
  • 定义拦截器链 chain,并通过 chain.proceed(originalRequest) 执行

到这里,其实已经可以解释很多现象了,比如应用拦截器与网络拦截器的核心区别是啥?为什么都是 Interceptor 的实现类,效果却有些不同?

你应该也发现本质原因了,主要区别在于拦截器装配的顺序不一样。拦截器处理顺序:

  • 处理请求时:顺序出场
  • 处理响应时,与处理请求顺序相反

在添加网络拦截器之前,添加了几个固定拦截器,比如 重试、桥接、缓存等拦截器。因此,网络层拦截器展示的效果与应用层拦截器不同,主要是由这几个拦截器在中间发挥作用。

我们再来回顾下这张图,你应该理解的更加深刻了:

okhttp 应用之 Interceptors 拦截器,「实践 + 原理」一样都不少_第4张图片

值得注意的是,最后一个添加的拦截器是 CallServerInterceptor,是真正执行 http 请求的拦截器。

换句话说,okhttp 将所有操作包括 执行 http 调用都封装为拦截器,然后通过拦截器链串联起来执行。


3. 执行请求

okhttp3.internal.http.RealInterceptorChain#proceed:

okhttp 应用之 Interceptors 拦截器,「实践 + 原理」一样都不少_第5张图片

前面提到,拦截器是一个 List 集合,那么如何决定当前使用哪一个拦截器?

答案是:index 下标


原理是这样的:

  • 首先,RealInterceptorChain#proceed 是将所有拦截器串联起来。
  • 另外,我们在每个拦截器里面都会通过 chain.proceed 去处理真正的逻辑(本质是调用下一个拦截器)
  • 而 chain.proceed 进入的就是 RealInterceptorChain#proceed 调用,也就是回到了这个方法,不过 index + 1,即 执行下一个拦截器。
  • 如此往复,直到最后一个拦截器,便执行真正的 http 请求。
  • 拦截器处理响应请求时,则是按照与处理请求相反的顺序执行。

进入我们自定义的拦截器:

okhttp 应用之 Interceptors 拦截器,「实践 + 原理」一样都不少_第6张图片



至此,okhttp 请求处理流程梳理完毕!!!可以发现,okhttp 的核心链路是围绕着拦截器进行设计,我们可以实现任何自定义拦截器并添加至其中,非常方便。

总结

本文带你一起探讨了 okhttp 拦截器的使用方法,及其底层处理原理,相信你也有了自己的想法。

拦截器的应用十分广泛,常见的还用 Spring 拦截器、Mybatis 拦截器以及 Feign 拦截器,相信你在开发过程中也遇到了很多。

你可以进一步探讨拦截器,多对比几款实现,看能否抽象出一款拦截器的通用理论?




相关参考:
  • okhttp-interceptors#docs
  • okhttp # github

你可能感兴趣的:(JAVA,#,Spring系列,okhttp,spring,boot)