敞开成长之旅!这是我参与「日新计划 2 月更文挑战」的第 4 天,点击查看活动概况
OkHttp 是 Android 最盛行的网络恳求库,由 square 公司开发。除此之外,square 还开源了众多受欢迎的库,如 okio,picasso,retrofit,moshi 等。总归,square 是一个特别神奇的安排,成员都是职业威望,经验丰富的开发者。
本文演示了14个比方,覆盖了 OkHttp 一切可能运用到的用法,能够让咱们快速入门。这些在日常开发中彻底够用。
运用过程
OkHttp 的运用有一定的过程,大体如下。
- 初始化
OkHttpClient
,官方主张整个应用只运用一个OkHttpClient
对象。OkHttpClient
能够设置缓存和超时时刻等。 - 关于一个恳求,实例化一个
Request
对象,构建恳求的url
,恳求体
等信息。 - 运用
OkHttpClient
发送同步或异步恳求,并获取呼应成果。
能够看见运用 OkHttp 发送网络恳求十分的简略,只需求简略的3个过程。本文OkHttpClient
的装备如下所示。设置衔接超时为10s,写入超时为10s,读取超时为 30s,调用超时为 10s。
private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.build();
同步 Get 恳求
同步恳求的意思是“履行恳求”和“获取回来成果”是同步的,有先后顺序的。关于同步恳求,OkHttp 内部不会敞开线程去履行。恳求和成果回来都在调用线程中履行。下面代码首先经过Builder
形式创建了一个Request
,url
是https://publicobject.com/helloworld.txt
,然后运用okHttpClient
发送同步恳求,调用线程会堵塞等待直到成果回来或者产生异常而退出。
public void synchronousGet() throws IOException {
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
// 运用 execute 发送同步恳求,会堵塞直到成果回来
try(Response response = okHttpClient.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));
}
// 假如 呼应体 > 1MB,应避免运用 string() 办法,由于它会将整个文档加载到内存中。这种情况下,应该运用流的办法来处理 body
System.out.println(response.body().string());
}
}
上述代码打印出呼应报文的一切头字段。
Server: nginx/1.10.3 (Ubuntu)
Date: Fri, 03 Feb 2023 09:18:44 GMT
Content-Type: text/plain
Content-Length: 1759
Last-Modified: Tue, 27 May 2014 02:35:47 GMT
Connection: keep-alive
ETag: "5383fa03-6df"
Accept-Ranges: bytes
异步 Get 恳求
异步恳求和同步恳求刚好相反。关于异步恳求来说,OkHttp 内部会敞开线程去履行,回来成果会在线程中回调。所以不要在回调函数onFailure
或onResponse
**中去操作UI。**如有必要,能够配合运用Handler
或RxJava
切换到主线程操作。
public void asynchronousGet(){
Request request = new Request.Builder()
.url("https://publicobject.com/helloworld.txt")
.build();
// 运用 enqueue 发送异步恳求,调用线程不会堵塞
okHttpClient.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 {
System.out.println("Current thread: " + Thread.currentThread().getName());
try(ResponseBody responseBody = response.body()) {
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(responseBody.string());
}
}
});
}
一个头字段设置多个值
OkHttp 支撑运用addHeader
办法对一个头字段设置多个值。
public void accessHeaders() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
// OkHttp 支撑一个头字段设置多个值
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
try (Response response = okHttpClient.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"));
}
}
程序打印成果。
Server: GitHub.com
Date: Mon, 06 Feb 2023 07:16:13 GMT
Vary: [Accept, Accept-Encoding, Accept, X-Requested-With]
Post 办法提交 String
OkHttp 中但凡 Post 恳求必须指定恳求体的媒体类型,下面程序运用 Post 办法向服务器提交一个字符串,媒体类型是text/x-markdown; charset=utf-8
。
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");
public void postString() 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 办法恳求,并指定恳求体的媒体类型
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
程序打印成果。
<h2><a id="user-content-releases" class="anchor" aria-hidden="true" href="#releases"><span aria-hidden="true" class="octicon octicon-link"></span></a>Releases</h2>
<ul>
<li>
<em>1.0</em> May 6, 2013</li>
<li>
<em>1.1</em> June 15, 2013</li>
<li>
<em>1.2</em> August 11, 2013</li>
</ul>
Post 办法提交流
OkHttp 答应咱们将恳求体的内容以流的办法写入,只需重写RequestBody
的contentType
和writeTo
办法。下面代码在writeTo
办法中向输出流写入了2到997对应的素因子乘积。
public void postStreaming() throws Exception {
// 重写 contentType 和 writeTo 办法
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 = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
打印出呼应报文的呼应体。
<h2><a id="user-content-numbers" class="anchor" aria-hidden="true" href="#numbers"><span aria-hidden="true" class="octicon octicon-link"></span></a>Numbers</h2>
<ul>
<li>2 = 2</li>
<li>3 = 3</li>
<li>4 = 2 2</li>
<li>5 = 5</li>
<li>6 = 3 2</li>
<li>7 = 7</li>
<li>8 = 2 2 2</li>
...
<li>992 = 31 2 2 2 2 2</li>
<li>993 = 331 3</li>
<li>994 = 71 7 2</li>
<li>995 = 199 5</li>
<li>996 = 83 3 2 2</li>
<li>997 = 997</li>
</ul>
Post 办法提交文件
OkHttp 提交文件很简略。和提交字符串一样,只需将文件作为恳求体。
public void postFile() throws Exception {
File file = new File("test.txt");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
// 直接传入 file 即可
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
程序打印成果。
<p>this is a test.</p>
Post 办法提交表单
FormBody
是RequestBody
的子类,运用FormBody
能够轻松的将恳求参数以表单编码办法提交。FormBody
默认的媒体类型是application/x-www-form-urlencoded
。
public void postForm() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
// 该 url 被墙,暂时无法访问
// java.net.SocketTimeoutException: connect timed out
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
Post 办法提交分块恳求
当你要在一个恳求中同时提交多种类型的数据时,能够运用MultipartBody
来完成这个任务。下面程序提交了字符串和图片两种不同的数据。
public void postMultipartBody() 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("logo-square.png")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
// 该 url 被墙,暂时无法访问
// java.net.SocketTimeoutException: connect timed out
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
运用 Moshi 解析 Json 呼应
Moshi 是一个序列/反序列化库。下面代码简略演示了它的用法,当然你还能够运用 Gson 等其他盛行的框架。
// Moshi
private final Moshi moshi = new Moshi.Builder().build();
private final JsonAdapter<Gist> gistJsonAdapter = moshi.adapter(Gist.class);
public void moshiParse() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
// 运用 moshi 反序列化回来成果
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;
}
缓存呼应
OkHttp 能够以文件的办法缓存来自服务器的呼应,节约网络流量。修正OkHttpClient
的装备,添加缓存功用。设置缓存巨细为 10M,缓存文件目录为okhttpcache
。
// Cache, 装备 OkHttp 本地缓存
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(new File("okhttpcache"), cacheSize);
// client
private final OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(cache)
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(10, TimeUnit.SECONDS)
.build();
下面程序恳求了同一个url
两次。调用cacheResponse
两次来调查打印成果,咱们能够得出如下结论。
- 由于本地没有缓存,第一次调用的第一个恳求会恳求网络,并将呼应缓存到本地。
- 第一次调用的第二个恳求直接从本地缓存拿到呼应成果,不会走网络恳求。
- 第2次调用的一切恳求均会从本地缓存拿到呼应成果。
public void cacheResponse() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
// 第一次恳求
String response1Body;
try (Response response1 = okHttpClient.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());
}
// 第2次恳求
String response2Body;
try (Response response2 = okHttpClient.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));
}
程序打印成果。
// 第一次调用打印
Response 1 response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 cache response: null
Response 1 network response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 cache response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 network response: null
Response 2 equals Response 1? true
// 第2次调用打印
Response 1 response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 cache response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 1 network response: null
Response 2 response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 cache response: Response{protocol=http/1.1, code=200, message=OK, url=https://publicobject.com/helloworld.txt}
Response 2 network response: null
Response 2 equals Response 1? true
撤销正在履行的恳求
运用 call.cancel()
能够当即停止正在进行的恳求,同步和异步恳求都能够撤销。这种功用能够节约网络,例如当用户开应用程序时。下面恳求的url
会有2s的推迟,在一个线程中咱们会在1s后撤销这个恳求,随后程序抛出了异常。
public void cancelCall() 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 = okHttpClient.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);
}
}
程序打印成果。
0.00 Executing call.
1.02 Canceling call.
1.02 Canceled call.
1.02 Call failed as expected: java.io.IOException: Canceled
设置超时
OkHttp 支撑设置衔接、写入、读取和彻底调用超时。下面程序恳求的url
有10s的推迟。而咱们OkHttpClient
设置的调用超时为10s,所以下面代码将会抛出异常。
public void timeout() throws Exception {
Request request = new Request.Builder()
.url("http://httpbin.org/delay/10") // This URL is served with a 10 second delay.
.build();
try (Response response = okHttpClient.newCall(request).execute()) {
System.out.println("Response completed: " + response);
}
}
程序打印成果。
java.io.InterruptedIOException: timeout
at okhttp3.internal.connection.RealCall.timeoutExit(RealCall.kt:398)
at okhttp3.internal.connection.RealCall.callDone(RealCall.kt:360)
改动单个 Call 的装备
OkHttpClient
的装备对一切的恳求都生效,有时候咱们想对某个恳求单独装备,比方改动超时时刻等。这个时候就能够运用newBuilder
办法,newBuilder
会复制全局OkHttpClient
成员变量的值重新生成一个OkHttpClient
实例回来。
public void perCallConfiguration() 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 = okHttpClient.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 = okHttpClient.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);
}
}
程序打印成果。
Response 1 failed: java.net.SocketTimeoutException: Read timed out
Response 2 succeeded: Response{protocol=http/1.1, code=200, message=OK, url=http://httpbin.org/delay/1}
身份验证
在恳求需求身份验证的资源时,若没有权限服务器通常会回来401
来告知客户端身份验证的计划和受保护的资源范。OkHttp 能够主动重试未经身份验证的恳求,当呼应为401
未授权时,咱们构建一个包括凭证的新恳求。下面程序恳求的url
需求身份验证,在authenticator
办法中第一次回来了401
,所以咱们添加了头字段Authorization
,并依照服务器要求的身份验证计划生成了凭证再次发送恳求。
public void authenticate() throws Exception {
OkHttpClient okHttpClient1 = okHttpClient.newBuilder()
.authenticator(new Authenticator() {
@Nullable
@Override
public Request authenticate(@Nullable 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();
Request request = new Request.Builder()
.url("http://publicobject.com/secrets/hellosecret.txt")
.build();
try (Response response = okHttpClient1.newCall(request).execute()) {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
}
程序打印成果。
Authenticating for response: Response{protocol=http/1.1, code=401, message=Unauthorized, url=https://publicobject.com/secrets/hellosecret.txt}
Challenges: [Basic authParams={realm=OkHttp Secrets}]
写在最终
假如你对我感兴趣,请移步到 blogss.cn ,或关注公众号:程序员小北,进一步了解。
- 假如本文协助到了你,欢迎点赞和关注,这是我持续创造的动力 ❤️
- 由于作者水平有限,文中假如有过错,欢迎在评论区指正 ✔️
- 本文首发于,未经许可禁止转载 ️