Skip to content

Recipes

liyanjin edited this page May 31, 2017 · 3 revisions

下面有一些示例,这些示例展示了如何使用OkHttp解决一些常见的问题。如果阅读这些示例,你可以了解到这一切是怎么运作的。请随意剪切粘贴这些示例。.

下面的例子展示了如何使用OkHttp下载一个文件、打印响应头列表和以字符串形式打印响应体。

对于较小的文档,可以使用string()方法方便、高效的将文档转换成字符串。但是对于体积超过1MiB的文档,请避免使用string()方法,因为这个方法会一次将整个文档加载进内存。对于大体积文档时,建议使用流操作。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    Headers responseHeaders = response.headers();
    for (int i = 0; i < responseHeaders.size(); i++) {
      System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
    }

    System.out.println(response.body().string());
  }

在工作线程下载一个文件,当响应可读时会做一次回调。回调是在发生响应头准备就绪之后。此时去读取响应体可能会被阻塞。OkHttp目前没有提供异步接收部分响应体的API。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    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 {
        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(response.body().string());
      }
    });
  }

HTTP头列表类似 Map<String, String>:每个域可能有一个值或者没有。但是需要注意,有些头允许对应多个值,比如Guava's Multimap。举个例子,对于HTTP响应来说,多个Vary头是合法的,也是很常见的。OkHttp的API则尝试合理的满足各种情况。

当写请求头列表时,namevalue是一对一的情况请使用header(name, value) 这个方法设置,如果name对应的value有值时,会覆盖掉旧的value。当namevalue是一对多的情况时,使用addHeader(name, value) 这个方法。

当读取响应头时,header(name)方法会返回对应name的value列表中最后一个value,如果对应name的value不存在,则返回null。如果想读取对应name的所有value,使用headers(name)这个方法。

Headers这个类支持使用索引访问头列表。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/repos/square/okhttp/issues")
        .header("User-Agent", "OkHttp Headers.java")
        .addHeader("Accept", "application/json; q=0.5")
        .addHeader("Accept", "application/vnd.github.v3+json")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println("Server: " + response.header("Server"));
    System.out.println("Date: " + response.header("Date"));
    System.out.println("Vary: " + response.headers("Vary"));
  }

下面的例子展示了如果像Web服务端post一个markdown文件,并声明以HTML格式渲染这个markdown文件。由于整个请求体都会保存在内存中,并且是并发的(个人理解同时有多少个请求就有多少个请求体保存在内存中),所以要表面使用此API去post大于1Mib的文件。

  public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    String postBody = ""
        + "Releases\n"
        + "--------\n"
        + "\n"
        + " * _1.0_ May 6, 2013\n"
        + " * _1.1_ June 15, 2013\n"
        + " * _1.2_ August 11, 2013\n";

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

这个例子展示了如何以流的形式POST。请求提的内容是通过一边一边产生的。示例中内容是直接流向OkHttp的缓冲 Okio 中。如果你的程序可能需要一个OutputStream,则可以通过 BufferedSink.outputStream()获取。

  public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody requestBody = new RequestBody() {
      @Override public MediaType contentType() {
        return MEDIA_TYPE_MARKDOWN;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.writeUtf8("Numbers\n");
        sink.writeUtf8("-------\n");
        for (int i = 2; i <= 997; i++) {
          sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
        }
      }

      private String factor(int n) {
        for (int i = 2; i < n; i++) {
          int x = n / i;
          if (x * i == n) return factor(x) + " × " + i;
        }
        return Integer.toString(n);
      }
    };

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

十分简单,看下面例子就行,直接把一个file当做请求体

  public static final MediaType MEDIA_TYPE_MARKDOWN
      = MediaType.parse("text/x-markdown; charset=utf-8");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    File file = new File("README.md");

    Request request = new Request.Builder()
        .url("https://api.github.com/markdown/raw")
        .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

使用 FormBody.Builder 去构建一个类似HTML <form> 标题按(tag)样式的请求体。键值都会被URLencode。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    RequestBody formBody = new FormBody.Builder()
        .add("search", "Jurassic Park")
        .build();
    Request request = new Request.Builder()
        .url("https://en.wikipedia.org/w/index.php")
        .post(formBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

可以使用MultipartBody.Builder构建一个复杂、兼容HTML文件上传表单的请求体。请求体的每个部分自身也是一个请求体,每一部分也可以定义自身的请求头列表。如果设置了请求头,则这些请求头应该描述自己属于那部分的请求体,例如通过Content-Disposition描述。Content-LengthContent-Type 请求头会被自动判别、计算并添加到请求头列表中。

  private static final String IMGUR_CLIENT_ID = "...";
  private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("title", "Square Logo")
        .addFormDataPart("image", "logo-square.png",
            RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
        .build();

    Request request = new Request.Builder()
        .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
        .url("https://api.imgur.com/3/image")
        .post(requestBody)
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

使用Gson 工具可以很方便的在JSON字符串和Java对象之间进行转换。下面是一个使用Gson转换GitHub API返回的例子。

需要注意的是,在解码请求体时,ResponseBody.charStream() 这个方法是根据请求头列表中的Content-Type决定使用那种字符集进行解码。如果没有指定字符集,则默认使用 UTF-8

  private final OkHttpClient client = new OkHttpClient();
  private final Gson gson = new Gson();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("https://api.github.com/gists/c2a7c39532239ff261be")
        .build();
    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
    for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
      System.out.println(entry.getKey());
      System.out.println(entry.getValue().content);
    }
  }

  static class Gist {
    Map<String, GistFile> files;
  }

  static class GistFile {
    String content;
  }

为了缓存响应,你需要指定一个有读写权限的的文件夹(路径)、指定缓存空间的大小。缓存文件夹应该是私有的,不被信任的应用应无法读取缓存内存。

不应该出现多个缓存并发的使用一个缓存文件夹的情况。通常来说,应用在其声明周期中应该只调用一次 new OkHttpClient(),并在创建的时候配置好缓存,然后在任何需要发送HTTP网络请求时使用之前创建好的OkHttpClient对象(简言之,应该通过单例模式使用OkHttpClient)。否则的话,多个缓存实例会互相重写、使缓存出现错误,更甚至导致程序崩溃。

响应缓存是根据HTTP请求头列表完成所有配置的。你可以向请求头列表中添加类似Cache-Control: max-stale=3600这样的缓存相关的请求头,OkHttp的缓存则会遵循这些缓存配置。你的Web服务也可以通过类似Cache-Control: max-age=9600这样的响应头来配置缓存的失效时间。可以通过设置头列表强制获取来自缓存的响应、强制获取来自网络的响应或者通过GET强制验证一个网络响应(原文:There are cache headers to force a cached response, force a network response, or force the network response to be validated with a conditional GET.)。

  private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient.Builder()
        .cache(cache)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    Response response1 = client.newCall(request).execute();
    if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

    String response1Body = response1.body().string();
    System.out.println("Response 1 response:          " + response1);
    System.out.println("Response 1 cache response:    " + response1.cacheResponse());
    System.out.println("Response 1 network response:  " + response1.networkResponse());

    Response response2 = client.newCall(request).execute();
    if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

    String response2Body = response2.body().string();
    System.out.println("Response 2 response:          " + response2);
    System.out.println("Response 2 cache response:    " + response2.cacheResponse());
    System.out.println("Response 2 network response:  " + response2.networkResponse());

    System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
  }

使用CacheControl.FORCE_NETWORK避免获得缓存中的响应。使用CacheControl.FORCE_CACHE避免从网络获取响应。注意:如果使用了 FORCE_CACHE的同时,响应又要求必须从网络获得,则OkHttp会返回504 Unsatisfiable Request 这样的响应。

调用Call.cancel()可以立即取消一个即将发生的Call。如果有一个线程已经在写请求或者读响应则会抛出一个 IOException异常。当一个Call不再是必要的时候可以调用这种方法取消,比如在用户离开应用时。同步、异步的Call都可以被取消。

  private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    final long startNanos = System.nanoTime();
    final Call call = client.newCall(request);

    // Schedule a job to cancel the call in 1 second.
    executor.schedule(new Runnable() {
      @Override public void run() {
        System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
        System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
      }
    }, 1, TimeUnit.SECONDS);

    try {
      System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
      Response response = call.execute();
      System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, response);
    } catch (IOException e) {
      System.out.printf("%.2f Call failed as expected: %s%n",
          (System.nanoTime() - startNanos) / 1e9f, e);
    }
  }

当服务不可达时,可以使用超时机制来结束一个Call。客户端连接问题、服务端可用性问题或者任何客户端和服务端之间的问题都可能导致网络不可达。OkHttp支持连接、读、写超时。

  private final OkHttpClient client;

  public ConfigureTimeouts() throws Exception {
    client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
        .build();

    Response response = client.newCall(request).execute();
    System.out.println("Response completed: " + response);
  }

HTTP客户端所有配置都在OkHttpClient中,包括代理设置、超时和缓存。使用 OkHttpClient.newBuilder()当你需要单独配置一个Call时。上述方法会返回一个builder,这个builder与原始的客户端共享同样的连接池、分发器和配置。下面的例子展示了如果创建一个500ms超时的请求和一个3000ms超时的请求。

  private final OkHttpClient client = new OkHttpClient();

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
        .build();

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(500, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println("Response 1 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 1 failed: " + e);
    }

    try {
      // Copy to customize OkHttp for this request.
      OkHttpClient copy = client.newBuilder()
          .readTimeout(3000, TimeUnit.MILLISECONDS)
          .build();

      Response response = copy.newCall(request).execute();
      System.out.println("Response 2 succeeded: " + response);
    } catch (IOException e) {
      System.out.println("Response 2 failed: " + e);
    }
  }

OkHttp可以自动重试未认证的请求。当响应得到的是401 Not Authorized,会向一个Authenticator请求提供证书。 应在Authenticator接口实现中应创建一个包含缺失的证书的请求。如果没有可用的证书,则不会重试直接返回null。

可以通过调用Response.challenges() 以获取认证(authentication challenge)的协议(scheme)和范围(realm)。当进行Basic认证时,使用Credentials.basic(username, password) 对请求头进行编码。

  private final OkHttpClient client;

  public Authenticate() {
    client = new OkHttpClient.Builder()
        .authenticator(new Authenticator() {
          @Override public Request authenticate(Route route, Response response) throws IOException {
            System.out.println("Authenticating for response: " + response);
            System.out.println("Challenges: " + response.challenges());
            String credential = Credentials.basic("jesse", "password1");
            return response.request().newBuilder()
                .header("Authorization", credential)
                .build();
          }
        })
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/secrets/hellosecret.txt")
        .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
  }

当认证无效时,为了避免过多的重试,可以在实现中返回null以表明放弃重试。比如,当所有证书都尝试过后你可能希望跳过重试:

  if (credential.equals(response.request().header("Authorization"))) {
    return null; // If we already failed with these credentials, don't retry.
   }

你也可能希望在达到几次重试失败之后不再重试:

  if (responseCount(response) >= 3) {
    return null; // If we've failed 3 times, give up.
  }

上面对应的responseCount() 方法的代码如下:

  private int responseCount(Response response) {
    int result = 1;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }

Clone this wiki locally