OkHttp: 拦截器和事件监听器

文章目录

    • [1. 拦截器](#1. 拦截器)
      • [1. 拦截器链](#1. 拦截器链)
      • [2. 实际案例](#2. 实际案例)
        • [1. 注册为应用拦截器](#1. 注册为应用拦截器)
        • [2. 注册为网络拦截器](#2. 注册为网络拦截器)
      • [3. 如何选择用哪种拦截器](#3. 如何选择用哪种拦截器)
        • [1. 应用拦截器](#1. 应用拦截器)
        • [2. 网络层拦截器](#2. 网络层拦截器)
        • [3. 重写请求](#3. 重写请求)
        • [4. 重写响应](#4. 重写响应)
      • [4. 可用性](#4. 可用性)
    • [2. 事件监听器](#2. 事件监听器)
      • [1. 请求的生命周期](#1. 请求的生命周期)
      • [2. EventListener使用案例](#2. EventListener使用案例)
      • [3. EventListener.Factory](#3. EventListener.Factory)
      • [4. 调用失败的请求](#4. 调用失败的请求)

1. 拦截器

拦截器是一种强大的机制,可以用来监测、重写、重试调用。下面是一个简单的例子,用来打印请求的输入和输出。

Java 复制代码
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;
  }
}

1. 拦截器链

拦截器可以形成一个拦截器链, 拦截器分为应用层拦截器、网络层拦截器,如下图所示:

2. 实际案例

假设我们访问的是http://www.publicobject.com/helloworld.txt,它实际上会有一个302跳转到https://publicobject.com/helloworld.txt, OkHttp会自动完成跳转。

我们用上面的LoggingInterceptor做为例子来讲解应用拦截器和网络层拦截器的区别。

1. 注册为应用拦截器
Java 复制代码
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();

输出只会有一个Request,一个Response,内部的跳转过程没有感知。以下是输出内容:

Java 复制代码
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. 注册为网络拦截器
Java 复制代码
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();

输出会感知每一个Request、Response对象。以下为输出内容:

Java 复制代码
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

网络层拦截器还能给我们提供更多信息,比如Accept-Encoding、Connection这些OkHttp帮我们自动添加的头信息。

3. 如何选择用哪种拦截器

1. 应用拦截器
  • 不关心重定向、重试
  • 永远只显示一次,即使是从缓存中读取结果
  • 只关心应用的原始意图。不关心OkHttp自动添加的头,比如If-None-Match等
  • 允许短路,不调用Chain.process(request)
  • 允许重试,调用多次Chain.process(request)
2. 网络层拦截器
  • 允许修改和操作中间的请求结果和状态
  • 从缓存中读取时,不调用拦截器
  • 关心中间过程
  • 访问请求的Connection对象
3. 重写请求

拦截器可以添加、删除、修改HTTP头,甚至可以改变请求体。比如你可以在拦截器内对请求体做压缩(如果你知道Web服务器支持的话)。

Java 复制代码
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();
      }
    };
  }
}
4. 重写响应

重写响应可以重写响应的HTTP头、响应内容等,一般来说是不推荐的,可能会违反直觉。

比如为响应自动添加缓存头。

Java 复制代码
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();
  }
};

4. 可用性

需要okhttp 2.2以后的版本,不可以和OkUrlFactory一起工作,不能在Retrofit 1.8及以下版本使用。

2. 事件监听器

你可以通过事件知道OkHttp的内部运行状态。通过时间我们可以监控:

  • HTTP请求的大小、频率
  • 这些请求对应的网络性能

(这里提及的API还不是最终版的API,OkHttp 3.9里这个API只是非稳定的预览版,预计在3.10、3.11会稳定)

你可以通过继承EventListener并覆盖你感兴趣的事件方法来获取通知。

1. 请求的生命周期

一次普通的请求完成,会触发以下事件:

2. EventListener使用案例

Java 复制代码
class PrintingEventListener extends EventListener {
  private long callStartNanos;

  private void printEvent(String name) {
    long nowNanos = System.nanoTime();
    if (name.equals("callStart")) {
      callStartNanos = nowNanos;
    }
    long elapsedNanos = nowNanos - callStartNanos;
    System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name);
  }

  @Override public void callStart(Call call) {
    printEvent("callStart");
  }

  @Override public void callEnd(Call call) {
    printEvent("callEnd");
  }

  @Override public void dnsStart(Call call, String domainName) {
    printEvent("dnsStart");
  }

  @Override public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
    printEvent("dnsEnd");
  }

  ...
}

EventListener会被所有Call共享,所以EventListener本身不能是有状态的,如果需要的话,采用EventListener.Factory

3. EventListener.Factory

Factory可以让相同的Call共用一个EventListener对象,也可以只是随机选一部分Call来监听

Java 复制代码
class MetricsEventListener extends EventListener {
  private static final Factory FACTORY = new Factory() {
    @Override public EventListener create(Call call) {
      if (Math.random() < 0.10) {
        return new MetricsEventListener(call);
      } else {
        return EventListener.NONE;
      }
    }
  };
  ...
}

4. 调用失败的请求

如果是连接阶段,触发事件connectFailed(),否则触发callFailed()。失败发生的时候,有可能存在调用了start方法,但是没有调用end方法的情况。

相关推荐
钢铁男儿15 分钟前
C# 接口(什么是接口)
java·数据库·c#
丶小鱼丶29 分钟前
排序算法之【归并排序】
java·排序算法
上上迁31 分钟前
分布式生成 ID 策略的演进和最佳实践,含springBoot 实现(Java版本)
java·spring boot·分布式
永日4567031 分钟前
学习日记-spring-day42-7.7
java·学习·spring
龙谷情Sinoam1 小时前
扩展若依@Excel注解,使其对字段的控制是否导出更加便捷
java
二十雨辰1 小时前
[尚庭公寓]07-Knife快速入门
java·开发语言·spring
掉鱼的猫1 小时前
Java MCP 实战:构建跨进程与远程的工具服务
java·openai·mcp
我爱Jack2 小时前
时间与空间复杂度详解:算法效率的度量衡
java·开发语言·算法
米饭「」2 小时前
C++AVL树
java·开发语言·c++
Zonda要好好学习2 小时前
Python入门Day4
java·网络·python