我们已经写了一些 Recipes,演示了如何解决 OkHttp 的常见问题。 通过阅读本文了解 OkHttp 一些常见的使用方法,如:
- Synchronous Get
- Asynchronous Get
- Accessing Headers
- Posting a String
- Post Streaming
- Posting a File
- Posting form parameters
- Posting a multipart request
- Parse a JSON Response With Gson
- Response Caching
- Canceling a Call
- Timeouts
- Per-call Configuration
- Handling authentication
一、同步获取(Synchronous Get)
可下载文件,打印它的头部信息,或将其响应实体(response body)当作一个字符串打印出来。
在 response 中的 string()
方法对于小型 documents 来说是十分方便快捷的。 但是,如果response body 过大(>1 MB 以上),应避免使用 string()
,因为它会讲整个 documents 加载到内存中。 在这种情况下,我们应倾向于将 response body 作为流进行处理:
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());
}
二、异步获取(Asynchronous Get)
下载一个在工作线程中的文件,当 response 可读时拿到 callback
.
callback
是在 response headers 准备好之后才创建的。
Reading the response body may still block. “读取响应主体仍可能阻塞”
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(Request request, IOException throwable) {
throwable.printStackTrace();
}
@Override public void onResponse(Response response) throws IOException {
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());
}
});
}
三、使用头部信息(Accessing Headers)
典型的 HTTP
头部信息类似一个 Map<String, String>
:每个字段都有一个值或无
但是,有一些头域时允许多个值的,比如 Guava
的 Multimap .
例如,为一个 HTTP response
提供多个 Vary
头是合法的也是比较常见的
OkHttp的API,试图使以上这两种情况都变得舒适好用:
-
当我们写请求头时:
-
可用
header(name, value)
来 set the only occurrence ofname
tovalue
. 如果存在 values,则在增加新的 value 之前将它们先删除 -
可用
addHeader(name, value)
来增加 header 而不用将已经存在的 headers 先删除 -
当我们读取响应头时:
-
可用
header(name)
返回指定的最后的值。 通常这也是唯一的!
如果没有值,header(name)
将返回null
. -
想读取头部某字段所有的值,可以用
headers(name)
,返回一个list
.
要遍历所有的头部信息,可用支持索引访问的 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"));
}
四、Posting a String
使用 HTTP POST
将请求主体 (request body) 发送到服务端。下面这个例子 post 一个 markdown 文档到一个 web 服务端并将其以 HTML 形式呈现。因为整个 request body 是同时在内存中的,我们应避免使用此 API 来 post 较大 (>1MB) 的文件
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 Streaming
在这里,我们将 request body 作为流进行 post ,因为该 request body 的内容是可以以写的形式生成。This example streams directly into the Okio buffered sink. 如果你的程序更倾向于使用 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());
}
六、Posting a File
It's easy to use a file as a request body.
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
类似 HTML 中的 <form>
标签,我们可以用 FormBody.Builder
来创建一个 **request body **.
Names and values will be encoded using an HTML-compatible form URL encoding. "names 和 values 将使用HTML兼容的表单URL编码进行编码。"
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());
}
八、Posting a multipart request
MultipartBody.Builder
可以建立复杂的请求主体并兼容 HTML文件上传表单。 Each part of a multipart request body is itself a request body,并且可以定义自己的 headers . If present, these headers should describe the part body,such as its Content-Disposition
.
如果 Content-Length
和 Content-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());
}
九、Parse a JSON Response With Gson
Gson 的 API 使得 JSON 和 Java 对象之间的转换变得十分方便。 这里,我们用它来解析从 GitHub 的 API 获得的 JSON 响应
需要注意的是 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;
}
十、Response Caching
若要缓存响应,你需要一个缓存目录来进行读取和写入,并在设置缓存的大小限制。缓存目录应该是 pravite 的,不信任的应用程序不应该能够阅读其内容!
同时读写同一个缓存目录里的多个缓存会导致错误。 大多数应用程序应该恰好调用 new OkHttpClient()
一次,并配置缓存,然后用的时候都是使用相同的实例。 否则,二级缓存实例将互相影响,破坏响应缓存,甚至使你的程序 crash .
响应缓存的所有配置使用的都是 HTTP headers. 你可以添加请求头(如 Cache-Control: max-stale=3600
),OkHttp 的缓存是会应用它的。 你的网络服务器通过配置响应 headers 来配置响应的缓存时间,如 Cache-Control: max-age=9600
.
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
的响应
十一、Canceling a Call
使用 Call.cancel()
立即停止正在进行的 call。 如果一个线程目前正在写请求或读响应,它将收到一个 IOException
. 当一个 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);
}
}
十二、Timeouts
Use timeouts to fail a call when its peer is unreachable. Network partitions can be due to client connectivity problems, server availability problems, or anything between. OkHttp supports connect, read, and write 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);
}
十三、Per-call Configuration
所有的HTTP client 配置都在 OkHttpClient中,包括代理设置,超时和缓存。 当你需要改变单一 call 的配置时,调用 OkHttpClient.newBuilder()
. 这将返回一个的 builder,这个 的 builder 共享与最初的 client 相同的连接池(pool)、调度(dispatcher)和配置(configuration ).
在下面的例子中
- 一个 request 做了500毫秒超时
- 另外一个 request 做了 3000毫秒超时
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);
}
}
十四、处理认证 (Handling authentication)
OkHttp可以自动重试未经授权的请求
- 当响应
401 Not Authorized
,一个Authenticator
被要求提供凭据
实现上应该建立一个含有之前缺少的凭据的新 request。 - 如果没有凭证可用,则返回null跳过重试
使用 Response.challenges()
来获取任何 authentication challenges 的 schemes 和 realms
当完成一个 Basic 的 challenge,通过 Credentials.basic(username,password)
来编码请求 header .
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
停止重试。 For example, you may want to skip the retry when these exact credentials have already been attempted:
if (credential.equals(response.request().header("Authorization"))) {
return null; // If we already failed with these credentials, don't retry.
}
You may also skip the retry when you’ve hit an application-defined attempt limit:
if (responseCount(response) >= 3) {
return null; // If we've failed 3 times, give up.
}
This above code relies on this responseCount()
method:
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}
参考文章:
[1] OkHttp官方wiki: Recipes
网友评论