我們已經寫了一些 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