OkHttp3-使用进阶(Recipes)

转载请注明出处 http://www.jianshu.com/p/25e89116847c (作者:韩栋)
本文为译文,由于译者水平有限,欢迎拍砖,读者也可以阅读原文
OkHttp3-基本用法,OkHttp3-使用进阶(Recipes),OkHttp3-请求器(Calls)OkHttp3-连接(Connections)OkHttp3-拦截器(Interceptor)


我们写了一些例子用来演示如何解决在OkHttp遇到的常见问题。通过这些例子去学习关于OkHttp中的组件是如何一起工作的。可以随意复制粘贴所需要的代码。

Synchronous Get(Get方式同步请求)

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

这个例子我们演示了一个下载文件并且以字符串的形式打印出它的响应头和响应主体的例子。

这个response.body().string()中的string()方法在对于小文档来说是非常方便和高效的。但是如果这个响应主体比较大(超过1MiB),那么应该避免使用string()方法,因为它将会把整个文档一次性加载进内存中(容易造成oom)。在这种情况下,建议将这个响应主体作为数据流来处理。

Asynchronous Get(Get方式异步请求)

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

在这个例子中,我们在子线程中下载一个文件,并且在对应的回调方法在主线程中读取响应。注意:读取大量的响应主体可能会堵塞主线程。OkHttp当前不提供异步Api用来分段获取响应主体。

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

通常Http的以一个Map<String, String>数据结构来存储Http头部信息:每一个Key所对应的Value是可空的,但是有一些字段是允许存在多个的 (可以使用Multimap,简单说下,MultimapMap最大的不同就是前者的Key可以重复)。比如,一个Http响应提供了多个合法并且常用的Key为Vary响应头信息。那么OkHttp的Api将会生成多个合适的方案。(Vary 字段用于列出一个响应字段列表,告诉缓存服务器遇到同一个 URL 对应着不同版本文档的情况时,如何缓存和筛选合适的版本。)

为请求添加请求头字段有两种方式,header(name, value)addHeader(name, value)。唯一不同的是前者会覆盖掉原有的字段(如果原来存在此字段),后者则是在原来的字段信息进行添加,不会覆盖。

读取响应头信息也有两种方式,header(name)headers(name)。前者只会返回对应的字段最后一次出现的值,后者则是将对应的字段所有的值返回。当然,值是允许为null的。

如果你想得到所有的头部信息,使用Headers类是个很好的主意。它支持通过索引来获取头部信息。

Post a String(上传一个字符串)

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的请求方式,将一个字符串放在HTML的格式的文本中上传到服务器。这种方式的上传数据方式是将整个请求体一次性放入内存中,所以当所需上传的数据大小超过1Mib时,应当避免使用这种方式上传数据,因为会对程序的性能损害,甚至oom。当上传的数据大小超过此值时,可以以数据流的形式上传。

Post Streaming(以数据流的形式上传)

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

这个例子展示了通过数据流的方式上传一个和上个例子同样的字符串。如果你是以依赖的方式使用OkHttp这个库,那么就无需再手动为它添加Okio库了。因为OkHttp默认依赖于OkioOkio为OkHttp提供了一个可控大小的缓存池,这样我们就不用担心因为上传大数据而会出现的问题。如果你更倾向使用OutputStream,你可以通过BufferedSink.outputStream()获取到OutputStream对象。

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

这个例子很简单展示了如何将一个文件上传至服务器。

Posting form parameters(上传表单参数)

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

使用 FormBody.Builder去构建一个像HTML<form>标签一样的请求体。

Posting a multipart request(上传携带有多种表单数据的主体)

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

通过MultipartBody.Builder可以创建拥有复杂的请求主体的请求,比如有多种需要上传的数据格式的表单数据。它们每一种表单数据都是一个请求主体,你可以为它们分别定义属于每个请求主体的请求头信息,比如Content-Disposition,并且OkHttp会自动为它们添加Content-LengthContent-Type请求头。

Parse a JSON Reponse With Gson(用Gson来解析Json数据)

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

Gson是一种可将Json数据序列化成Java对象,或者将Java对象反序列化为Json数据。这个例子中我们将从GitHub Api返回响应的Json数据序列化为Gist.class对象。
  需要注意的是,我们这里使用的ResponseBody.charStream()使用的是响应头中Content-Type的字段为编码格式,如果此响应头中没有对应的字段,那么默认为UTF-8编码。

Response Caching(响应缓存)

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

为了缓存响应,你必须先有一个可以读写以及大小确定的缓存目录。这个缓存目录应该是私有的,并且通常情况下其他程序是无法对这个目录进行读取的。

多个缓存无法同时访问一个相同的缓存目录。否则可能会造成响应数据发生错误,甚至程序Crash。为了避免这种情况,我们一般在程序中只调用new OkHttpClient()创建OkHttpClient一次,并且对它进行缓存配置,并且在全局中使用这个实例(我们可以通过单例模式来创建它,不过很多人开发Android的人喜欢在Application中创建它)。

OkHttp会根据响应头的配置配置信息对响应数据进行缓存。服务器会在返回的响应数据中配置这个响应数据在你的程序中应该被缓存多久,比如Cache-Control: max-age=9600,它缓存配置时间为9600秒,9600秒后它将会过期。但是我们是否可以自定义缓存时间呢,答案是可以的。我们可以在请求头中添加缓存配置,比如Cache-Control: max-stale=3600,当服务器返回响应时,OkHttp会使用此配置进行缓存。

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.这句话不知道怎么翻译。。求读者指教。。囧。。
  
  通常在存在请求的响应缓存并且缓存数据没有过期的情况下,那么当你再次发送这个请求时,OkHttp并不会去服务器上获取数据,而是直接在本地缓存目录中取得数据返回给你。当然,如果你想避免从缓存中获取数据,那么你可以在构建Request的时候使用cacheControl()方法以CacheControl.FORCE_NETWORK为参数进行配置。或者当缓存过期,但你还是不想去服务器请求,而是再次使用缓存。你也可以配置为CacheControl.FORCE_CACHE。注意:如果此时本地缓存中并没有缓存数据,或者因为其他原因(不包括缓存过期)而必须去服务器请求,那么OkHttp将会返回一个504 Unsatisfiable Request的响应。

Canceling a 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.cancel()方法将会立即停止此Call正在运行中的网络工作(为什么是工作呢,因为此时Call可能在发送请求,或者读取响应等),此时它可能会抛出一个IOException异常。在适当的时候取消不再需要的请求有利于我们减少程序的工作以及流量的损耗。比如在Android开发中,用户点击了返回键离开了这个页面(假如这个Activity或者Fragment被销毁),那么我们就可以在相应的回调(一般在onDestroy())中取消掉在这个页面所有的Call的网络工作,包括异步和同步的都可以取消。

Timeouts(超时)

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

OkHttp支持配置三种超时情况,分别是连接超时、读取超时以及写入超时。当发生超时情况时,OkHttp将会调用Call.cancel()来取消掉此Call的所有网络工作。

Per-call Configuration(为个别的Call添加特别的配置)

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

在前面我们说过最好以单例模式来创建一个OkHttpClient实例,你可以在这个单例中进行比如代理设置、超时以及缓存等配置,以后当我们需要的时候直接获取这个单例来使用,达到一种全局配置的效果。现在存在一种需求情况,有一个特殊的Call请求的配置需要发生一些改变,我们首先可以通过OkHttpClient.newBuilder()的方法来复制一个和原来全局单例相同的OkHttpClient对象,注意!是复制,也就是说这个新的复制对象会和原来的单例共享同一个连接池,调度器,以及相同的配置信息。然后再进行对新的复制对象进行自定义的配置,最后让这个特殊的Call使用。

在这个例子中,我们client.newBuilder()复制了一个新的OkHttpClient对象,并且将它的读取超时时间重新设置了为500秒。

Handing authentication(配置认证信息)

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

当我们向一个需要进行身份认证的服务器发送请求时(假设此时我们尚未配置身份认证信息),服务器就会返回一个401 Not Authorized的响应。它意味着我们需要进行身份认证才可以获取到想要的数据。那么我们如何进行配置呢。其实当OkHttp获取到401 Not Authorized的响应时,OkHttp会向Authenticator对象获取证书。Authenticator是一个接口,在这个例子中,我们通过向authenticator()方法添加了一个Authenticator对象为参数。在匿名内部类的实现中我们可以看到,authenticate()方法返回了一个设置了header("Authorization", credential)的请求头的新的Request对象,这个请求头中的credential就是身份验证信息。OkHttp使用这个Request自动帮我们再次发送请求。如果我们没有添加身份认证信息配置,那么OkHttp会自动中断此次请求,不会再次帮我们重新发送请求。

当服务器返回401 Not Authorized的响应时,我们可以通过Response.challenges()方法获取所需要的认证信息要求信息。如果只是简单需要账号和密码时候,我们可以使用Credentials.basic(username, password)对请求头进行编码。

为了避免当你提供的身份验证信息错误使服务器一直返回401 Not Authorized的响应而导致程序陷入死循环(无限地重试),你可以在Authenticator接口的实现方法authenticate()中返回null来告诉OkHttp放弃重新请求。

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

private int responseCount(Response response) {
    int result = 1;
    while ((response = response.priorResponse()) != null) {
      result++;
    }
    return result;
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容