okhttp(一)-入门使用

3,675 阅读5分钟

okhttp是什么?

它是一个基于HTTP+HTTP/2的java开发的客户端网络访问库,拥有丰富的功能以及高效的性能。由square公司开源,目前github上已有3w+的star,可见人们对它的喜爱。

demo演示

我们先来演示一下其简单的用法,参考官网的例子: 构建Maven项目,引入依赖:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.7.0</version>
</dependency>

执行一个GET请求,代码如下:

OkHttpClient client = new OkHttpClient();

Request request = new Request.Builder()
        .url("http://www.baidu.com")
        .build();
try (Response response = client.newCall(request).execute()) {
    String resStr = response.body().string();
    System.out.println(resStr);
} catch (IOException e) {
    e.printStackTrace();
}

再来看一个POST请求的例子:

public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }

从上面的代码示例可以看出,okhttp的api使用起来还是很简单的,创建一个OkHttpClient的客户端,创建一个Request请求,通过client来执行请求即可。

Okhttp中的组件


  • client

    http客户端的工作说起来就是接受你的请求并且执行请求,返回相应的响应内容。

  • request

    每个请求都包含url,header,请求方式(get,post...),请求体。okhttp提供了重写请求,跟随请求(重定向)以及重试等功能。

  • response

    响应内容包含响应码,响应头和响应体,okhttp提供了自动重写响应的功能。

  • calls

    将请求到获取响应的执行过程抽象成一个call,执行任务的方式有同步和异步的方式: Synchronous:阻塞当前线程,直到返回结果

    Asynchronous:将请求加入队列,通过回调的方式获取响应,异步请求的返回是在一个单独的work线程中。

    异步请求代码示例:

    client.newCall(request).enqueue(new Callback() {
          @Override public void onFailure(Call call, IOException e) {
            e.printStackTrace();
          }
    
          @Override public void onResponse(Call call, Response response) throws IOException {
            try (ResponseBody responseBody = response.body()) {
              if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
    
              Headers responseHeaders = response.headers();
              for (int i = 0, size = responseHeaders.size(); i < size; i++) {
                System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
              }
    
              System.out.println(responseBody.string());
            }
          }
        });
      }
    

更多代码示例参考github:github.com/square/okht…

Interceptors


拦截器是okhttp提供的一个强大功能,它允许你在请求和响应的某个环节上进行相应的操作。按照调用位置的不同,拦截器可以分为两大类APPLICATION和NETWORK,两种类型的分类标准是按照拦截器在核心拦截器(后面章节会提到,它是整个库的核心,也是我认为设计非常棒的地方)之前或之后来分的,如下图所示

拦截器使用实例,下面创建了一个日志拦截器,用于在请求发送前和接收响应后打印日志

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

上面的chain.request()和chain.proceed(request)是关键。chain.request()从链中获取请求,chain.proceed()是执行具体的请求,并将请求传到到下一个链中。将创建的拦截器加入client中

OkHttpClient client = new OkHttpClient().newBuilder()
    .addInterceptor(new LoggingInterceptor())
    .build();

上面的代码默认添加的是一个application类型的拦截器,如果要添加一个netword类型的拦截器,如下面代码所示:

OkHttpClient client = new OkHttpClient.Builder()
    .addNetworkInterceptor(new LoggingInterceptor())
    .build();

拦截器的应用

  • 重写request

    可以添加,删除或替换相关的请求头和请求体,下面的代码展示了一段对请求体进行压缩的示例:

    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 do not 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();
      }
    };
    }
    
    
    
  • 重写response

    同样可以对接收到的响应头和响应体进行修改,但是通常这么做是很危险的,不建议这么做。

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

Events


okhttp提供了许多事件点,供开发程序可以方便的进行扩展和监听。例如你可以利用这些事件点来对调用请求的频率和性能进行监控。事件点如下图所示:

从图中可以看出请求主要分为CONNECTING和CONNECTED两个阶段。如果之前已有相同的链接请求,将会从连接池中复用已有链接,从而提高性能。如果没有连接,则将通过dns解析拿到请求的IP地址,再请求建立连接。你可以利用框架提供的EventListener回调接口来处理你感兴趣的事件,下面的代码示例统计了请求期间,各个阶段所花费的时间:

坑位提醒:EventListener在3.11以上的版本才有提供

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

 ...
}

在client中加入listener

OkHttpClient client = new OkHttpClient().newBuilder()
                .eventListener(new PrintingEventListener())
                .build();

由于EvenetListener是与每一个call绑定的,如果你要区别不同call,可以通过工厂的方式来创建一个EventListener,为每一个listener创建一个唯一ID,这样你就可以在日志中通过该ID来区分不同的调用了。如下所示:

class PrintingEventListener extends EventListener {
  public static final Factory FACTORY = new Factory() {
    final AtomicLong nextCallId = new AtomicLong(1L);

    @Override public EventListener create(Call call) {
      long callId = nextCallId.getAndIncrement();
      System.out.printf("%04d %s%n", callId, call.request().url());
      return new PrintingEventListener(callId, System.nanoTime());
    }
  };

  final long callId;
  final long callStartNanos;

  public PrintingEventListener(long callId, long callStartNanos) {
    this.callId = callId;
    this.callStartNanos = callStartNanos;
  }

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

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

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

  ...
}

最后在client中设置eventFactory

OkHttpClient client = new OkHttpClient().newBuilder()
                .eventListenerFactory(PrintingEventListener.FACTORY)
                .build();