Recipes

我们已经写了一些例子来演示如何解决一些OkHttp的常见问题。仔细阅读来学习它是怎么使用的。可以自由地复制这些例子。

Synchronous Get

下载一个文件,打印响应头并且以String来输出响应体。

响应体的 string() 方法对于小文件来说非常方便和高效。但是如果响应体非常大(大于1MiB),请不要使用 string() ,因为它会把整个文件加载到内存中。这种情况,最好以流的方式来读取响应体。

  1. private final OkHttpClient client = new OkHttpClient();
  2. public void run() throws Exception {
  3. Request request = new Request.Builder()
  4. .url("https://publicobject.com/helloworld.txt")
  5. .build();
  6. try (Response response = client.newCall(request).execute()) {
  7. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  8. Headers responseHeaders = response.headers();
  9. for (int i = 0; i < responseHeaders.size(); i++) {
  10. System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
  11. }
  12. System.out.println(response.body().string());
  13. }
  14. }

Asynchronous Get

在工作线程下载一个文件,然后当响应返回时,会得到一个回调。回调会在响应头好的时候开始。读取响应体可能还是阻塞的。OkHttp通常不提供异步API来分块获取响应体。

  1. private final OkHttpClient client = new OkHttpClient();
  2. public void run() throws Exception {
  3. Request request = new Request.Builder()
  4. .url("http://publicobject.com/helloworld.txt")
  5. .build();
  6. client.newCall(request).enqueue(new Callback() {
  7. @Override public void onFailure(Call call, IOException e) {
  8. e.printStackTrace();
  9. }
  10. @Override public void onResponse(Call call, Response response) throws IOException {
  11. try (ResponseBody responseBody = response.body()) {
  12. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  13. Headers responseHeaders = response.headers();
  14. for (int i = 0, size = responseHeaders.size(); i < size; i++) {
  15. System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
  16. }
  17. System.out.println(responseBody.string());
  18. }
  19. }
  20. });
  21. }

Access Headers

典型的HTTP头像一个 Map<String, String>:每个字段有一个value或没有。但一些headers可以有多个值,比如Guava’s Multimap。例如,对HTTP响应来说,提供多个头是常见合法的。OkHttp的API尽量让两种方式都非常方便。

处理请求头时,使用 header(name, value) 来设置那些唯一的name。如果已经存在了values,他们会在新值添加前被移除。使用 addHeader(name, value) 可以在不移除已经存在的values前提下添加一个header。

读取响应的header时,使用 header(name) 可以返回对应的最后一个value。通常也是唯一的。如果没有值,会返回null。如果想要读取一个字段的所有值,使用 headers(name)

使用 Headers 以索引方式可以访问所有的头。

  1. private final OkHttpClient client = new OkHttpClient();
  2. public void run() throws Exception {
  3. Request request = new Request.Builder()
  4. .url("https://api.github.com/repos/square/okhttp/issues")
  5. .header("User-Agent", "OkHttp Headers.java")
  6. .addHeader("Accept", "application/json; q=0.5")
  7. .addHeader("Accept", "application/vnd.github.v3+json")
  8. .build();
  9. try (Response response = client.newCall(request).execute()) {
  10. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  11. System.out.println("Server: " + response.header("Server"));
  12. System.out.println("Date: " + response.header("Date"));
  13. System.out.println("Vary: " + response.headers("Vary"));
  14. }
  15. }

Posting a String

使用HTTP POST来发送一个请求体。这个例子post一个markdown文件到一个渲染服务器,这个服务可以把markdown渲染成HTML。因为整个请求体是同时在内存中加载,所以避免使用这个API来post大文件(大于1MiB)。

  1. public static final MediaType MEDIA_TYPE_MARKDOWN
  2. = MediaType.parse("text/x-markdown; charset=utf-8");
  3. private final OkHttpClient client = new OkHttpClient();
  4. public void run() throws Exception {
  5. String postBody = ""
  6. + "Releases\n"
  7. + "--------\n"
  8. + "\n"
  9. + " * _1.0_ May 6, 2013\n"
  10. + " * _1.1_ June 15, 2013\n"
  11. + " * _1.2_ August 11, 2013\n";
  12. Request request = new Request.Builder()
  13. .url("https://api.github.com/markdown/raw")
  14. .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
  15. .build();
  16. try (Response response = client.newCall(request).execute()) {
  17. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  18. System.out.println(response.body().string());
  19. }
  20. }

Post Streaming

现在我们来以流的方式POST请求体。请求体是在写的同时生成的。这个流直接写到了Okio的缓存池。你的程序可能想使用 OutputStream,可以通过BufferedSink.outputStream()来获取。

  1. public static final MediaType MEDIA_TYPE_MARKDOWN
  2. = MediaType.parse("text/x-markdown; charset=utf-8");
  3. private final OkHttpClient client = new OkHttpClient();
  4. public void run() throws Exception {
  5. RequestBody requestBody = new RequestBody() {
  6. @Override public MediaType contentType() {
  7. return MEDIA_TYPE_MARKDOWN;
  8. }
  9. @Override public void writeTo(BufferedSink sink) throws IOException {
  10. sink.writeUtf8("Numbers\n");
  11. sink.writeUtf8("-------\n");
  12. for (int i = 2; i <= 997; i++) {
  13. sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
  14. }
  15. }
  16. private String factor(int n) {
  17. for (int i = 2; i < n; i++) {
  18. int x = n / i;
  19. if (x * i == n) return factor(x) + " × " + i;
  20. }
  21. return Integer.toString(n);
  22. }
  23. };
  24. Request request = new Request.Builder()
  25. .url("https://api.github.com/markdown/raw")
  26. .post(requestBody)
  27. .build();
  28. try (Response response = client.newCall(request).execute()) {
  29. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  30. System.out.println(response.body().string());
  31. }
  32. }

Posting a File

把文件作为请求体非常简单。

  1. public static final MediaType MEDIA_TYPE_MARKDOWN
  2. = MediaType.parse("text/x-markdown; charset=utf-8");
  3. private final OkHttpClient client = new OkHttpClient();
  4. public void run() throws Exception {
  5. File file = new File("README.md");
  6. Request request = new Request.Builder()
  7. .url("https://api.github.com/markdown/raw")
  8. .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
  9. .build();
  10. try (Response response = client.newCall(request).execute()) {
  11. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  12. System.out.println(response.body().string());
  13. }
  14. }

Posting form parameters

使用 FormBody.Builder 来创建一个像HTML的 <form> 标签的请求体。键和值都会使用HTML兼容格式的URL加密。

  1. private final OkHttpClient client = new OkHttpClient();
  2. public void run() throws Exception {
  3. RequestBody formBody = new FormBody.Builder()
  4. .add("search", "Jurassic Park")
  5. .build();
  6. Request request = new Request.Builder()
  7. .url("https://en.wikipedia.org/w/index.php")
  8. .post(formBody)
  9. .build();
  10. try (Response response = client.newCall(request).execute()) {
  11. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  12. System.out.println(response.body().string());
  13. }
  14. }

Posting a multipart request

MultipartBody.Builder 可以创建复杂的请求体来兼容HTML上传文件的格式。多媒体请求体的每个部分都是一个请求体,可以定义自己的header。如果设置,这些headers应该是来描述这部分的请求体,比如它的Content-DispositionContent-LengthContent-Type headers会在可用时自动添加。

  1. /**
  2. * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
  3. * these examples, please request your own client ID! https://api.imgur.com/oauth2
  4. */
  5. private static final String IMGUR_CLIENT_ID = "...";
  6. private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
  7. private final OkHttpClient client = new OkHttpClient();
  8. public void run() throws Exception {
  9. // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
  10. RequestBody requestBody = new MultipartBody.Builder()
  11. .setType(MultipartBody.FORM)
  12. .addFormDataPart("title", "Square Logo")
  13. .addFormDataPart("image", "logo-square.png",
  14. RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
  15. .build();
  16. Request request = new Request.Builder()
  17. .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
  18. .url("https://api.imgur.com/3/image")
  19. .post(requestBody)
  20. .build();
  21. try (Response response = client.newCall(request).execute()) {
  22. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  23. System.out.println(response.body().string());
  24. }
  25. }

Parse a JSON Response With Moshi

Moshi 是一个JSON和Java对象转换的简单库。我们使用它来解压转换一个JSON响应。

请注意: 在解压响应体时,ResponseBody.charStream() 使用响应头的 Content-Type 来选择字符集。如果没有指定,使用默认的 UTF-8

  1. private final OkHttpClient client = new OkHttpClient();
  2. private final Moshi moshi = new Moshi.Builder().build();
  3. private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);
  4. public void run() throws Exception {
  5. Request request = new Request.Builder()
  6. .url("https://api.github.com/gists/c2a7c39532239ff261be")
  7. .build();
  8. try (Response response = client.newCall(request).execute()) {
  9. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  10. Gist gist = gistJsonAdapter.fromJson(response.body().source());
  11. for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
  12. System.out.println(entry.getKey());
  13. System.out.println(entry.getValue().content);
  14. }
  15. }
  16. }
  17. static class Gist {
  18. Map<String, GistFile> files;
  19. }
  20. static class GistFile {
  21. String content;
  22. }

Response Caching

为了缓存响应,你需要一个可读可写的缓存目录,并且要限制它的大小。这个目录应该是私有的,不被信任的应用不能读取。

一个缓存目录同时有多个缓存是错误的。多数应用应该只调用一次 new OkHttpClient() ,然后配置它的缓存,最后在其它地方全部使用这个单例。否则,两个缓存实例会相互覆盖,破坏响应缓存,最终可能导致程序崩溃。

响应缓存使用HTTP headers来做所有的配置。可以添加像Cache-Control: max-stale=3600 的请求头,OkHttp缓存会使用它们。你的服务会通过它的像Cache-Control: max-age=9600的响应头来配置响应会被缓存多长时间。也有一些缓存的header可以强制使用响应缓存,强制使用网络响应,或者强制使用通过虚拟的GET来保证的合法的网络响应。

  1. private final OkHttpClient client;
  2. public CacheResponse(File cacheDirectory) throws Exception {
  3. int cacheSize = 10 * 1024 * 1024; // 10 MiB
  4. Cache cache = new Cache(cacheDirectory, cacheSize);
  5. client = new OkHttpClient.Builder()
  6. .cache(cache)
  7. .build();
  8. }
  9. public void run() throws Exception {
  10. Request request = new Request.Builder()
  11. .url("http://publicobject.com/helloworld.txt")
  12. .build();
  13. String response1Body;
  14. try (Response response1 = client.newCall(request).execute()) {
  15. if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
  16. response1Body = response1.body().string();
  17. System.out.println("Response 1 response: " + response1);
  18. System.out.println("Response 1 cache response: " + response1.cacheResponse());
  19. System.out.println("Response 1 network response: " + response1.networkResponse());
  20. }
  21. String response2Body;
  22. try (Response response2 = client.newCall(request).execute()) {
  23. if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
  24. response2Body = response2.body().string();
  25. System.out.println("Response 2 response: " + response2);
  26. System.out.println("Response 2 cache response: " + response2.cacheResponse());
  27. System.out.println("Response 2 network response: " + response2.networkResponse());
  28. }
  29. System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
  30. }

为了避免响应使用缓存,可以用 CacheControl.FORCE_NETWORK。为了避免响应使用网络 ,可以用 CacheControl.FORCE_CACHE。要注意:如果使用 FORCE_CACHE,但是响应需要网络,OkHttp会返回 504 Unsatisfiable Request的响应。

Canceling a Call

使用 Call.cancel() 可以立刻中止一个正在进行的请求。如果线程正在写一个请求或者读一个响应,它会收到 IOException。使用这个可以在请求不需要时保护网络;比如,当用户从一个应用离开时。不论是同步还是异步请求都可以取消。

  1. private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
  2. private final OkHttpClient client = new OkHttpClient();
  3. public void run() throws Exception {
  4. Request request = new Request.Builder()
  5. .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
  6. .build();
  7. final long startNanos = System.nanoTime();
  8. final Call call = client.newCall(request);
  9. // Schedule a job to cancel the call in 1 second.
  10. executor.schedule(new Runnable() {
  11. @Override public void run() {
  12. System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
  13. call.cancel();
  14. System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
  15. }
  16. }, 1, TimeUnit.SECONDS);
  17. System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
  18. try (Response response = call.execute()) {
  19. System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
  20. (System.nanoTime() - startNanos) / 1e9f, response);
  21. } catch (IOException e) {
  22. System.out.printf("%.2f Call failed as expected: %s%n",
  23. (System.nanoTime() - startNanos) / 1e9f, e);
  24. }
  25. }

Timeouts

当对方不可达时,使用timeouts可以让请求失败而停止。网络中断可能是因为客户端链接问题,服务端不可用问题,或者其它两者间的任何问题。OkHttp支持链接,读和写的超时。

  1. private final OkHttpClient client;
  2. public ConfigureTimeouts() throws Exception {
  3. client = new OkHttpClient.Builder()
  4. .connectTimeout(10, TimeUnit.SECONDS)
  5. .writeTimeout(10, TimeUnit.SECONDS)
  6. .readTimeout(30, TimeUnit.SECONDS)
  7. .build();
  8. }
  9. public void run() throws Exception {
  10. Request request = new Request.Builder()
  11. .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
  12. .build();
  13. try (Response response = client.newCall(request).execute()) {
  14. System.out.println("Response completed: " + response);
  15. }
  16. }

Per-call Configuration

所有的HTTP客户端设置全部都在OkHttpClient中,包括代理设置,超时设置和缓存设置。当你需要为一个请求改变设置时,调用OkHttpClient.newBuilder()。这个方法返回一个和原来client共用同一个链接池,同一个dispatcher和同样样配置的builder。下面的例子中,我们一个请求500ms超时,另一个3000ms超时。

  1. private final OkHttpClient client = new OkHttpClient();
  2. public void run() throws Exception {
  3. Request request = new Request.Builder()
  4. .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
  5. .build();
  6. // Copy to customize OkHttp for this request.
  7. OkHttpClient client1 = client.newBuilder()
  8. .readTimeout(500, TimeUnit.MILLISECONDS)
  9. .build();
  10. try (Response response = client1.newCall(request).execute()) {
  11. System.out.println("Response 1 succeeded: " + response);
  12. } catch (IOException e) {
  13. System.out.println("Response 1 failed: " + e);
  14. }
  15. // Copy to customize OkHttp for this request.
  16. OkHttpClient client2 = client.newBuilder()
  17. .readTimeout(3000, TimeUnit.MILLISECONDS)
  18. .build();
  19. try (Response response = client2.newCall(request).execute()) {
  20. System.out.println("Response 2 succeeded: " + response);
  21. } catch (IOException e) {
  22. System.out.println("Response 2 failed: " + e);
  23. }
  24. }

Handling authentication

OkHttp可以自动重试未验证的请求。当响应是 401 Not Authorized,验证器会被要求提供证书。验证器的实现应该创建一个带有证书的新的请求。如果没有证书,返回null来跳过重试。

使用 Response.challenges() 来得到身份验证的策略和边界。如果是完成一个 Basic 的验证,使用 Credentials.basic(username, password)来加密请求头。

  1. private final OkHttpClient client;
  2. public Authenticate() {
  3. client = new OkHttpClient.Builder()
  4. .authenticator(new Authenticator() {
  5. @Override public Request authenticate(Route route, Response response) throws IOException {
  6. if (response.request().header("Authorization") != null) {
  7. return null; // Give up, we've already attempted to authenticate.
  8. }
  9. System.out.println("Authenticating for response: " + response);
  10. System.out.println("Challenges: " + response.challenges());
  11. String credential = Credentials.basic("jesse", "password1");
  12. return response.request().newBuilder()
  13. .header("Authorization", credential)
  14. .build();
  15. }
  16. })
  17. .build();
  18. }
  19. public void run() throws Exception {
  20. Request request = new Request.Builder()
  21. .url("http://publicobject.com/secrets/hellosecret.txt")
  22. .build();
  23. try (Response response = client.newCall(request).execute()) {
  24. if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
  25. System.out.println(response.body().string());
  26. }
  27. }

为了避免当验证不可用时重试太多,你可以返回null来放弃。比如,你可能想在这些证书已经被尝试时跳过重试:

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

当你遇到应用定义好的限制次数时,跳过重试:

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

上边的这些代码依赖于 responseCount() 方法:

  1. private int responseCount(Response response) {
  2. int result = 1;
  3. while ((response = response.priorResponse()) != null) {
  4. result++;
  5. }
  6. return result;
  7. }