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


前言

参考源码版本 okhttp-3.14.9

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

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


一、拦截器

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

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

这是官方提供的图片,你可以参考下:
在这里插入图片描述

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

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:

》
可以看到,getResponseWithInterceptorChain 真正应用拦截器并执行 http 请求。


2. 拦截器链

okhttp3.RealCall#getResponseWithInterceptorChain:

在这里插入图片描述
看到拦截器的装配了!!!离真相不远了。

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

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

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

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

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

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

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

在这里插入图片描述

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

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


3. 执行请求

okhttp3.internal.http.RealInterceptorChain#proceed:

在这里插入图片描述

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

答案是:index 下标


原理是这样的:

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

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

在这里插入图片描述



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

总结

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

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

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




相关参考:
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
OkHttp是一个用于处理HTTP请求的开源Java库。它提供了一个拦截器机制,可以在发送请求和接收响应之前对它们进行修改和处理。以下是关于OkHttp拦截器的一些介绍和示例: 1. OkHttp拦截器是一个接口,它有一个方法intercept(Chain chain),该方法接收一个Chain对象作为参数,该对象表示当前的拦截器链。 2. 拦截器链是按照添加顺序执行的,每个拦截器都可以选择将请求传递给下一个拦截器或者直接返回响应。 3. 拦截器可以在请求和响应中添加、修改或删除头信息,也可以重试请求或者记录请求和响应的日志等。 以下是一个简单的OkHttp拦截器示例,它会在请求头中添加一个自定义的User-Agent信息: ```java public class UserAgentInterceptor implements Interceptor { private static final String USER_AGENT_HEADER = "User-Agent"; private final String userAgent; public UserAgentInterceptor(String userAgent) { this.userAgent = userAgent; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Request newRequest = request.newBuilder() .header(USER_AGENT_HEADER, userAgent) .build(); return chain.proceed(newRequest); } } ``` 在上面的示例中,我们创建了一个名为UserAgentInterceptor拦截器,它接收一个User-Agent字符串作为参数。在intercept方法中,我们首先获取当前的请求对象,然后使用Request.Builder添加一个自定义的User-Agent头信息,最后使用chain.proceed方法将请求传递给下一个拦截器或者返回响应。 以下是一个使用上面定义的拦截器的示例: ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new UserAgentInterceptor("MyApp/1.0")) .build(); ``` 在上面的示例中,我们创建了一个OkHttpClient对象,并使用addInterceptor方法添加了一个UserAgentInterceptor拦截器。这样,在发送请求时,OkHttp会自动调用我们定义的拦截器,并在请求头中添加一个自定义的User-Agent信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柏油

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值