OkHttp

Header 设置

@Test
public void whenSetHeader_thenCorrect() throws IOException {
    Request request = new Request.Builder()
      .url(SAMPLE_URL)
      .addHeader("Content-Type", "application/json")
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    response.close();
}Copy to clipboardErrorCopiedCopy to clipboardErrorCopiedCopy to clipboardErrorCopied

在此示例中,我们将看到如何在客户端本身上配置默认标头,而不是在每个请求上都设置默认标头。例如,如果要为每个请求设置内容类型 application/json,则需要为客户端设置拦截器。方法如下:

@Test
public void whenSetDefaultHeader_thenCorrect()
  throws IOException {
    OkHttpClient client = new OkHttpClient.Builder()
      .addInterceptor(
        new DefaultContentTypeInterceptor("application/json"))
      .build();
    Request request = new Request.Builder()
      .url(SAMPLE_URL)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    response.close();
}Copy to clipboardErrorCopiedCopy to clipboardErrorCopiedCopy to clipboardErrorCopied

这是 DefaultContentTypeInterceptor,它是 Interceptor 的扩展版本:

public class DefaultContentTypeInterceptor implements Interceptor {
  public Response intercept(Interceptor.Chain chain) throws IOException {
    Request originalRequest = chain.request();
    Request requestWithUserAgent = originalRequest
      .newBuilder()
      .header("Content-Type", contentType)
      .build();
    return chain.proceed(requestWithUserAgent);
  }
}Copy to clipboardErrorCopiedCopy to clipboardErrorCopiedCopy to clipboardErrorCopied

避免重定向

在此示例中,我们将看到如何配置 OkHttpClient 以停止跟随重定向。默认情况下,如果使用 HTTP 301 永久移动响应了 GET 请求,则会自动遵循重定向。在某些用例中,可能会很好,但是在某些用例中肯定是不需要的。为了实现此行为,在构建客户端时,需要将 followRedirects 设置为 false。请注意,响应将返回 HTTP 301 状态代码:

@Test
public void whenSetFollowRedirects_thenNotRedirected()
  throws IOException {
    OkHttpClient client = new OkHttpClient().newBuilder()
      .followRedirects(false)
      .build();
    Request request = new Request.Builder()
      .url("http://t.co/I5YYd9tddw")
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(301));
}Copy to clipboardErrorCopiedCopy to clipboardErrorCopiedCopy to clipboardErrorCopied

如果我们使用 true 参数打开重定向(或将其完全删除),则客户端将遵循重定向,并且测试将失败,因为返回码将为 HTTP 200。

超时与取消

当对方无法访问时,请使用超时使呼叫失败。网络故障可能是由于客户端连接问题,服务器可用性问题或两者之间的任何原因引起的。OkHttp 支持连接,读取和写入超时。在此示例中,我们以 1 秒的 readTimeout 构建客户端,而以 2 秒的延迟提供 URL:

@Test
public void whenSetRequestTimeout_thenFail()
  throws IOException {
    OkHttpClient client = new OkHttpClient.Builder()
      .readTimeout(1, TimeUnit.SECONDS)
      .build();
    Request request = new Request.Builder()
      .url(BASE_URL + "/delay/2")
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopiedCopy to clipboardErrorCopiedCopy to clipboardErrorCopied

使用 Call.cancel() 立即停止正在进行的呼叫。如果线程当前正在写入请求或读取响应,则将抛出 IOException。当不再需要通话时,使用此功能可以节省网络。例如,当您的用户离开应用程序时:

@Test(expected = IOException.class)
public void whenCancelRequest_thenCorrect()
  throws IOException {
    ScheduledExecutorService executor
      = Executors.newScheduledThreadPool(1);
    Request request = new Request.Builder()
      .url(BASE_URL + "/delay/2")
      .build();
    int seconds = 1;
    long startNanos = System.nanoTime();
    Call call = client.newCall(request);
    executor.schedule(() -> {
        logger.debug("Canceling call: "
            + (System.nanoTime() - startNanos) / 1e9f);
        call.cancel();
        logger.debug("Canceled call: "
            + (System.nanoTime() - startNanos) / 1e9f);
    }, seconds, TimeUnit.SECONDS);
    logger.debug("Executing call: "
      + (System.nanoTime() - startNanos) / 1e9f);
    Response response = call.execute();
    logger.debug(Call was expected to fail, but completed: "
      + (System.nanoTime() - startNanos) / 1e9f, response);
}Copy to clipboardErrorCopiedCopy to clipboardErrorCopiedCopy to clipboardErrorCopied

响应缓存

要创建一个缓存,我们需要一个我们可以读写的缓存目录,以及对缓存大小的限制。客户端将使用它来缓存响应:

@Test
public void  whenSetResponseCache_thenCorrect()
  throws IOException {
    int cacheSize = 10 * 1024 * 1024;
    File cacheDirectory = new File("src/test/resources/cache");
    Cache cache = new Cache(cacheDirectory, cacheSize);
    OkHttpClient client = new OkHttpClient.Builder()
      .cache(cache)
      .build();
    Request request = new Request.Builder()
      .url("http://publicobject.com/helloworld.txt")
      .build();
    Response response1 = client.newCall(request).execute();
    logResponse(response1);
    Response response2 = client.newCall(request).execute();
    logResponse(response2);
}Copy to clipboardErrorCopiedCopy to clipboardErrorCopiedCopy to clipboardErrorCopied

启动测试后,第一个调用的响应将不会被缓存。对方法 cacheResponse 的调用将返回 null,而对方法 networkResponse 的调用将返回来自网络的响应。此外,缓存文件夹中将填充缓存文件。第二次调用执行将产生相反的效果,因为响应已经被缓存了。这意味着对 networkResponse 的调用将返回 null,而对 cacheResponse 的调用将返回来自缓存的响应。为了防止响应使用缓存,请使用 CacheControl.FORCE_NETWORK。若要阻止它使用网络,请使用 CacheControl.FORCE_CACHE。

Get

OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}Copy to clipboardErrorCopied

要发送同步的 GET 请求,我们需要基于 URL 构建一个 Request 对象并进行调用。执行之后,OkHttp 返回一个 Response 实例:

@Test
public void whenGetRequest_thenCorrect() throws IOException {
    Request request = new Request.Builder()
      .url(BASE_URL + "/date")
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

现在,要进行异步 GET,我们需要排队一个 Call。回调使我们可以读取可读的响应,这是在响应头准备好之后发生的。读取响应正文可能仍然会阻塞。OkHttp 当前不提供任何异步 API 来部分接收响应正文:

@Test
public void whenAsynchronousGetRequest_thenCorrect() {
    Request request = new Request.Builder()
      .url(BASE_URL + "/date")
      .build();
    Call call = client.newCall(request);
    call.enqueue(new Callback() {
        public void onResponse(Call call, Response response)
          throws IOException {
            // ...
        }
        public void onFailure(Call call, IOException e) {
            fail();
        }
    });
}Copy to clipboardErrorCopied

最后,要将查询参数添加到 GET 请求中,我们可以利用 HttpUrl.Builder。我们可以将构建好的请求传递给我们的 Request 对象:

@Test
public void whenGetRequestWithQueryParameter_thenCorrect()
  throws IOException {
    HttpUrl.Builder urlBuilder
      = HttpUrl.parse(BASE_URL + "/ex/bars").newBuilder();
    urlBuilder.addQueryParameter("id", "1");
    String url = urlBuilder.build().toString();
    Request request = new Request.Builder()
      .url(url)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

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

表单构建

让我们看一个简单的 POST 请求,在其中构建一个 RequestBody 来发送参数 username 和 password:

@Test
public void whenSendPostRequest_thenCorrect()
  throws IOException {
    RequestBody formBody = new FormBody.Builder()
      .add("username", "test")
      .add("password", "test")
      .build();
    Request request = new Request.Builder()
      .url(BASE_URL + "/users")
      .post(formBody)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

我们也可以构建 MultipartBody 的请求:

@Test
public void whenSendMultipartRequest_thenCorrect()
  throws IOException {
    RequestBody requestBody = new MultipartBody.Builder()
      .setType(MultipartBody.FORM)
      .addFormDataPart("username", "test")
      .addFormDataPart("password", "test")
      .addFormDataPart("file", "file.txt",
        RequestBody.create(MediaType.parse("application/octet-stream"),
          new File("src/test/resources/test.txt")))
      .build();
    Request request = new Request.Builder()
      .url(BASE_URL + "/users/multipart")
      .post(requestBody)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

认证

如果要验证请求,可以使用 Credentials.basic 构建器将凭证添加到标头。在这个简单的示例中,我们还将发送一个 String 作为请求的主体:

@Test
public void whenSendPostRequestWithAuthorization_thenCorrect()
  throws IOException {
    String postBody = "test post";
    Request request = new Request.Builder()
      .url(URL_SECURED_BY_BASIC_AUTHENTICATION)
      .addHeader("Authorization", Credentials.basic("username", "password"))
      .post(RequestBody.create(
        MediaType.parse("text/x-markdown), postBody))
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

JSON

为了在请求正文中发送 JSON,我们必须设置其媒体类型 application/json。我们可以使用 RequestBody.create 构建器来做到这一点:

@Test
public void whenPostJson_thenCorrect() throws IOException {
    String json = "{\"id\":1,\"name\":\"John\"}";
    RequestBody body = RequestBody.create(
      MediaType.parse("application/json"), json);
    Request request = new Request.Builder()
      .url(BASE_URL + "/users/detail")
      .post(body)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

编码

如果我们要使用其他字符编码,可以将其作为 MediaType.parse() 的第二个参数传递:

@Test
public void whenPostJsonWithoutCharset_thenCharsetIsUtf8() throws IOException {
    final String json = "{\"id\":1,\"name\":\"John\"}";
    final RequestBody body = RequestBody.create(
        MediaType.parse("application/json"), json);
    String charset = body.contentType().charset().displayName();
    assertThat(charset, equalTo("UTF-8"));
}
@Test
public void whenPostJsonWithUtf16Charset_thenCharsetIsUtf16() throws IOException {
    final String json = "{\"id\":1,\"name\":\"John\"}";
    final RequestBody body = RequestBody.create(
        MediaType.parse("application/json; charset=utf-16"), json);
    String charset = body.contentType().charset().displayName();
    assertThat(charset, equalTo("UTF-16"));
}

文件上传

在此示例中,我们将看到如何上传文件。我们将使用 MultipartBody.Builder 上传 test.ext 文件:

@Test
public void whenUploadFile_thenCorrect() throws IOException {
    RequestBody requestBody = new MultipartBody.Builder()
      .setType(MultipartBody.FORM)
      .addFormDataPart("file", "file.txt",
        RequestBody.create(MediaType.parse("application/octet-stream"),
          new File("src/test/resources/test.txt")))
      .build();
    Request request = new Request.Builder()
      .url(BASE_URL + "/users/upload")
      .post(requestBody)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

文件上传进度

@Test
public void whenGetUploadFileProgress_thenCorrect()
  throws IOException {
    RequestBody requestBody = new MultipartBody.Builder()
      .setType(MultipartBody.FORM)
      .addFormDataPart("file", "file.txt",
        RequestBody.create(MediaType.parse("application/octet-stream"),
          new File("src/test/resources/test.txt")))
      .build();
    ProgressRequestWrapper.ProgressListener listener
      = (bytesWritten, contentLength) -> {
        float percentage = 100f * bytesWritten / contentLength;
        assertFalse(Float.compare(percentage, 100) > 0);
    };
    ProgressRequestWrapper countingBody
      = new ProgressRequestWrapper(requestBody, listener);
    Request request = new Request.Builder()
      .url(BASE_URL + "/users/upload")
      .post(countingBody)
      .build();
    Call call = client.newCall(request);
    Response response = call.execute();
    assertThat(response.code(), equalTo(200));
}Copy to clipboardErrorCopied

ProgressListener 接口定义如下:

public interface ProgressListener {
  void onRequestProgress(long bytesWritten, long contentLength);
}Copy to clipboardErrorCopied

ProgressRequestWrapper 接口定义如下:

public class ProgressRequestWrapper extends RequestBody {
  @Override
  public void writeTo(BufferedSink sink) throws IOException {
    BufferedSink bufferedSink;
    countingSink = new CountingSink(sink);
    bufferedSink = Okio.buffer(countingSink);
    delegate.writeTo(bufferedSink);
    bufferedSink.flush();
  }
}Copy to clipboardErrorCopied
protected class CountingSink extends ForwardingSink {
  private long bytesWritten = 0;
  public CountingSink(Sink delegate) {
    super(delegate);
  }
  @Override
  public void write(Buffer source, long byteCount) throws IOException {
    super.write(source, byteCount);
    bytesWritten += byteCount;
    listener.onRequestProgress(bytesWritten, contentLength());
  }
}