- Recipes
- Synchronous Get
- Asynchronous Get
- Access Headers
- Posting a String
- Post Streaming">Post Streaming
- Posting a File">Posting a File
- Posting form parameters">Posting form parameters
- Posting a multipart request">Posting a multipart request
- Parse a JSON Response With Moshi">Parse a JSON Response With Moshi
- Response Caching">Response Caching
- Canceling a Call">Canceling a Call
- Timeouts">Timeouts
- Per-call Configuration">Per-call Configuration
- Handling authentication">Handling authentication
Recipes
我们已经写了一些例子来演示如何解决一些OkHttp的常见问题。仔细阅读来学习它是怎么使用的。可以自由地复制这些例子。
Synchronous Get
下载一个文件,打印响应头并且以String来输出响应体。
响应体的 string()
方法对于小文件来说非常方便和高效。但是如果响应体非常大(大于1MiB),请不要使用 string()
,因为它会把整个文件加载到内存中。这种情况,最好以流的方式来读取响应体。
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
try (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
在工作线程下载一个文件,然后当响应返回时,会得到一个回调。回调会在响应头好的时候开始。读取响应体可能还是阻塞的。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(Call call, IOException e) {
e.printStackTrace();
}
@Override public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody responseBody = response.body()) {
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(responseBody.string());
}
}
});
}
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
以索引方式可以访问所有的头。
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();
try (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来发送一个请求体。这个例子post一个markdown文件到一个渲染服务器,这个服务可以把markdown渲染成HTML。因为整个请求体是同时在内存中加载,所以避免使用这个API来post大文件(大于1MiB)。
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Post Streaming
现在我们来以流的方式POST请求体。请求体是在写的同时生成的。这个流直接写到了Okio的缓存池。你的程序可能想使用 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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Posting form parameters
使用 FormBody.Builder
来创建一个像HTML的 <form>
标签的请求体。键和值都会使用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();
try (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上传文件的格式。多媒体请求体的每个部分都是一个请求体,可以定义自己的header。如果设置,这些headers应该是来描述这部分的请求体,比如它的Content-Disposition
。 Content-Length
和 Content-Type
headers会在可用时自动添加。
/**
* The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running
* these examples, please request your own client ID! https://api.imgur.com/oauth2
*/
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();
try (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 Moshi
Moshi 是一个JSON和Java对象转换的简单库。我们使用它来解压转换一个JSON响应。
请注意: 在解压响应体时,ResponseBody.charStream()
使用响应头的 Content-Type
来选择字符集。如果没有指定,使用默认的 UTF-8
。
private final OkHttpClient client = new OkHttpClient();
private final Moshi moshi = new Moshi.Builder().build();
private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Gist gist = gistJsonAdapter.fromJson(response.body().source());
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
为了缓存响应,你需要一个可读可写的缓存目录,并且要限制它的大小。这个目录应该是私有的,不被信任的应用不能读取。
一个缓存目录同时有多个缓存是错误的。多数应用应该只调用一次 new OkHttpClient()
,然后配置它的缓存,最后在其它地方全部使用这个单例。否则,两个缓存实例会相互覆盖,破坏响应缓存,最终可能导致程序崩溃。
响应缓存使用HTTP headers来做所有的配置。可以添加像Cache-Control: max-stale=3600
的请求头,OkHttp缓存会使用它们。你的服务会通过它的像Cache-Control: max-age=9600
的响应头来配置响应会被缓存多长时间。也有一些缓存的header可以强制使用响应缓存,强制使用网络响应,或者强制使用通过虚拟的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();
String response1Body;
try (Response response1 = client.newCall(request).execute()) {
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
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());
}
String response2Body;
try (Response response2 = client.newCall(request).execute()) {
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
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()
可以立刻中止一个正在进行的请求。如果线程正在写一个请求或者读一个响应,它会收到 IOException
。使用这个可以在请求不需要时保护网络;比如,当用户从一个应用离开时。不论是同步还是异步请求都可以取消。
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);
System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
try (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
当对方不可达时,使用timeouts可以让请求失败而停止。网络中断可能是因为客户端链接问题,服务端不可用问题,或者其它两者间的任何问题。OkHttp支持链接,读和写的超时。
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();
try (Response response = client.newCall(request).execute()) {
System.out.println("Response completed: " + response);
}
}
Per-call Configuration
所有的HTTP客户端设置全部都在OkHttpClient中,包括代理设置,超时设置和缓存设置。当你需要为一个请求改变设置时,调用OkHttpClient.newBuilder()
。这个方法返回一个和原来client共用同一个链接池,同一个dispatcher和同样样配置的builder。下面的例子中,我们一个请求500ms超时,另一个3000ms超时。
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();
// Copy to customize OkHttp for this request.
OkHttpClient client1 = client.newBuilder()
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
try (Response response = client1.newCall(request).execute()) {
System.out.println("Response 1 succeeded: " + response);
} catch (IOException e) {
System.out.println("Response 1 failed: " + e);
}
// Copy to customize OkHttp for this request.
OkHttpClient client2 = client.newBuilder()
.readTimeout(3000, TimeUnit.MILLISECONDS)
.build();
try (Response response = client2.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
,验证器会被要求提供证书。验证器的实现应该创建一个带有证书的新的请求。如果没有证书,返回null来跳过重试。
使用 Response.challenges()
来得到身份验证的策略和边界。如果是完成一个 Basic
的验证,使用 Credentials.basic(username, password)
来加密请求头。
private final OkHttpClient client;
public Authenticate() {
client = new OkHttpClient.Builder()
.authenticator(new Authenticator() {
@Override public Request authenticate(Route route, Response response) throws IOException {
if (response.request().header("Authorization") != null) {
return null; // Give up, we've already attempted to authenticate.
}
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();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
为了避免当验证不可用时重试太多,你可以返回null来放弃。比如,你可能想在这些证书已经被尝试时跳过重试:
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.
}
上边的这些代码依赖于 responseCount()
方法:
private int responseCount(Response response) {
int result = 1;
while ((response = response.priorResponse()) != null) {
result++;
}
return result;
}