學習鏈接
Apache httpclient & okhttp(1)
Apache httpclient & okhttp(2)
okhttp github
okhttp官方使用文檔
okhttp官方示例代碼
OkHttp使用介紹
OkHttp使用進階 譯自OkHttp Github官方教程
SpringBoot 整合okHttp okhttp3用法
Java中常用的HTTP客戶端庫:OkHttp和HttpClient(包含請求示例代碼)
深入淺出 OkHttp 源碼解析及應用實踐
文章目錄
- 學習鏈接
- okhttp
- okhttp概述
- 特點
- 快速入門
- pom.xml
- get請求
- Post請求
- 示例代碼
- 同步請求
- 異步請求
- 請求頭&響應頭
- post + 請求體
- 流式傳輸
- 流式傳輸擴展示例
- Test05StreamClient
- StreamController
- 文件傳輸
- 表單提交
- 文件上傳
- 響應緩存
- 取消調用
- 超時
- 每次調用配置
- 處理鑒權
- 攔截器
- 事件監聽
okhttp
okhttp概述
HTTP是現代應用程序的網絡方式。這是我們交換數據和媒體的方式。高效地使用HTTP可以讓您的東西加載更快并節省帶寬。
OkHttp使用起來很方便。它的請求/響應API設計具有流式構建和不可變性。它支持同步阻塞調用
和帶有回調的異步調用
。
特點
OkHttp是一個高效的默認HTTP客戶端
- HTTP/2支持允許對同一主機的所有請求共享一個套接字。
- 連接池減少了請求延時(如果HTTP/2不可用)。
- 透明GZIP縮小了下載大小。
- 響應緩存完全避免了重復請求的網絡。
OkHttp遵循現代HTTP規范,例如
- HTTP語義-RFC 9110
- HTTP緩存-RFC 9111
- HTTP/1.1-RFC 9112
- HTTP/2-RFC 9113
- Websocket-RFC 6455
- SSE-服務器發送的事件
快速入門
pom.xml
注意:okhttp的3.9.0版本用的是java,okhttp4.12.0用的是kotlin。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.zzhua</groupId><artifactId>demo-okhttp</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><!--<okhttp.version>3.9.0</okhttp.version>--><!--<okhttp.version>3.14.9</okhttp.version>--><!-- OkHttp從4.x版本開始轉向Kotlin, Kotlin 代碼可以被編譯成標準的 JVM 字節碼運行,這與 Java 代碼的最終執行形式完全兼容。 --><okhttp.version>4.12.0</okhttp.version></properties><dependencies><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>${okhttp.version}</version></dependency></dependencies></project>
get請求
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;import java.io.IOException;public class Test01 {public static void main(String[] args) {OkHttpClient client = new OkHttpClient();Request request = new Request.Builder().url("http://www.baidu.com").build();try (Response response = client.newCall(request).execute()) {System.out.println(response.body().string());} catch (IOException e) {throw new RuntimeException(e);}}}
Post請求
import okhttp3.*;
public class Test02 {public static void main(String[] args) {OkHttpClient client = new OkHttpClient();RequestBody body = RequestBody.create(MediaType.parse("application/json"), "{\"username\":\"zzhua\"}");Request request = new Request.Builder().url("http://localhost:8080/ok01").post(body).build();try (Response response = client.newCall(request).execute()) {System.out.println(response.body().string());} catch (Exception e) {throw new RuntimeException(e);}}}
示例代碼
這些示例代碼來自:https://square.github.io/okhttp/recipes,
官方也有對應的代碼:https://github.com/square/okhttp/blob/okhttp_3.14.x/samples
同步請求
下載文件,打印headers,并將其響應正文打印為字符串。
- 響應正文上的string()方法對于小文檔來說既方便又高效。但是如果響應正文很大(大于1 MiB),避免string(),因為它會將整個文檔加載到內存中。在這種情況下,更推薦將正文作為流處理。
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;import java.io.IOException;public class TestSynchronous {private final static OkHttpClient client = new OkHttpClient();public static void main(String[] args) {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());} catch (IOException e) {throw new RuntimeException(e);}}}
異步請求
在工作線程中下載文件,并在響應可讀取時觸發回調。該回調會在響應頭準備就緒后觸發,但讀取響應體仍可能阻塞線程。當前OkHttp未提供異步API以分塊接收響應體內容。
@Slf4j
public class Test02Async {public static void main(String[] args) {log.info("main start");OkHttpClient okHttpClient = new OkHttpClient();Request requeset = new Request.Builder().url("http://publicobject.com/helloworld.txt").build();Call call = okHttpClient.newCall(requeset);log.info("call.enqueue");// 執行回調的線程不是main線程call.enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {log.info("=========請求失敗=========: {}", e);}@Overridepublic void onResponse(Call call, Response response) throws IOException {log.info("=========獲得響應=========");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());}}});log.info("main end");}}
請求頭&響應頭
public class TestHeader {public static void main(String[] args) {OkHttpClient okHttpClient = new OkHttpClient();Request request = new Request.Builder().url("https://api.github.com/repos/square/okhttp/issues")// 單個header值.header("User-Agent", "OkHttp Headers.java")// 多個header值.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"));// 多個響應頭使用headers()獲取System.out.println("Vary: " + response.headers("Vary"));} catch (IOException e) {throw new RuntimeException(e);}}}
post + 請求體
使用HTTP POST向服務發送請求正文。此示例將markdown文檔發布到將markdown渲染為html。由于整個請求正文同時在內存中,因此避免使用此API發布大型(大于1 MiB)文檔。
public class TestRequestBody {public static void main(String[] args) {MediaType mediaType = MediaType.parse("text/x-markdown; charset=utf-8");OkHttpClient client = new OkHttpClient();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(mediaType, postBody)).build();try {Response response = client.newCall(request).execute();if (!response.isSuccessful())throw new IOException("Unexpected code " + response);System.out.println(response.body().string());} catch (Exception e) {throw new RuntimeException(e);}}}
流式傳輸
我們在這里將請求體以流的形式進行POST提交。該請求體的內容在寫入過程中動態生成。此示例直接將數據流式寫入Okio的緩沖池(Buffered Sink)。您的程序可能更傾向于使用OutputStream,您可以通過BufferedSink.outputStream()方法獲取它。
- 流式傳輸(Streaming):區別于一次性加載完整數據到內存,流式傳輸允許邊生成數據邊發送,尤其適合:大文件傳輸(如視頻上傳)、實時生成數據(如傳感器數據流)、內存敏感場景(避免OOM)
public class Test05Stream {public static void main(String[] args) {MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");OkHttpClient client = new OkHttpClient();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());} catch (IOException e) {throw new RuntimeException(e);}}}
流式傳輸擴展示例
通過觀察客戶端和服務端的日志,可以看到客戶端發的同時,服務端也在收。
Test05StreamClient
@Slf4j
public class Test05StreamClient {public static void main(String[] args) {OkHttpClient client = new OkHttpClient.Builder().writeTimeout(30, TimeUnit.SECONDS) // 延長寫入超時.build();// 創建流式 RequestBodyRequestBody requestBody = new RequestBody() {@Overridepublic MediaType contentType() {return MediaType.parse("application/octet-stream");}@Overridepublic void writeTo(BufferedSink sink) throws IOException {try (java.io.OutputStream os = sink.outputStream()) {for (int i = 0; i < 50; i++) {// 模擬生成數據塊String chunk = "Chunk-" + i + "\n";log.info("發送數據塊: {}", chunk);os.write(chunk.getBytes());os.flush(); // 立即刷新緩沖區Thread.sleep(100); // 模擬延遲}} catch (InterruptedException e) {Thread.currentThread().interrupt();}}};Request request = new Request.Builder().url("http://localhost:8080/stream-upload-raw").post(requestBody).header("Content-Type", "application/octet-stream") // 強制覆蓋.build();// 異步執行client.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {log.info("[請求失敗]");}@Overridepublic void onResponse(Call call, Response response) throws IOException {log.info("[請求成功]");System.out.println("上傳成功: " + response.code());System.out.println("響應內容: " + response.body().string());response.close();}});}
}
StreamController
@Slf4j
@RestController
public class StreamController {// 通過 HttpServletRequest 獲取原始流@PostMapping("/stream-upload-raw")public String handleRawStream(HttpServletRequest request) {try (InputStream rawStream = request.getInputStream()) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = rawStream.read(buffer)) != -1) { // 按字節讀取String chunk = new String(buffer, 0, bytesRead);log.info("[字節流] {}, {}", chunk.trim(), bytesRead);}return "Raw stream processed";} catch (IOException e) {return "Error: " + e.getMessage();}}
}
文件傳輸
將文件作為請求體。
public class Test06File {public static void main(String[] args) {OkHttpClient client = new OkHttpClient();Request request = new Request.Builder().url("http://localhost:8080/okFile").post(RequestBody.create(null, new File("C:\\Users\\zzhua195\\Desktop\\test.png"))).build();try (Response response = client.newCall(request).execute()) {if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);System.out.println(response.body().string());} catch (IOException e) {throw new RuntimeException(e);}}}
后端代碼
@RequestMapping("okFile")
public Object okFile(HttpServletRequest request) throws Exception {ServletInputStream inputStream = request.getInputStream();org.springframework.util.FileCopyUtils.copy(request.getInputStream(), new FileOutputStream("D:\\Projects\\practice\\demo-boot\\src\\main\\resources\\test.png"));return "ok";
}
表單提交
使用FormBody.Builder構建一個像超文本標記語言<form>標簽一樣工作的請求正文。名稱和值將使用超文本標記語言兼容的表單URL編碼進行編碼。
public class Test07Form {public static void main(String[] args) {OkHttpClient client = new OkHttpClient();RequestBody formBody = new FormBody.Builder().add("username", "Jurassic Park").build();Request request = new Request.Builder().url("http://localhost:8080/okForm").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());} catch (IOException e) {throw new RuntimeException(e);}}}
對應的后端代碼
@RequestMapping("okForm")
public Object okForm(LoginForm loginForm) throws Exception {log.info("[okForm] {}", loginForm);return "ok";}
文件上傳
MultipartBody.Builder可以構建與超文本標記語言文件上傳表單兼容的復雜請求正文。多部分請求正文的每個部分本身就是一個請求正文,并且可以定義自己的標頭。如果存在,這些標頭應該描述部分正文,例如它的Content-Disposition。如果可用,Content-Length和Content-Type標頭會自動添加
public class Test08MultipartFile {public static void main(String[] args) {OkHttpClient client = new OkHttpClient();RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("comment", "Square Logo").addFormDataPart("bin", "logo-square.png", RequestBody.create(null, new File("C:\\Users\\zzhua195\\Desktop\\test.png"))).build();Request request = new Request.Builder().header("Authorization", "Client-ID").url("http://127.0.0.1:8080/multipart02").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());} catch (IOException e) {throw new RuntimeException(e);}}}
對應的后端代碼
@RequestMapping("multipart01")public Object multipart(@RequestPart("bin") MultipartFile file,@RequestPart("comment") String comment) throws InterruptedException, IOException {System.out.println(file.getBytes().length);System.out.println(comment);return "ojbk";}@RequestMapping("multipart02")public Object multipart(MultipartDTO multipartDTO) throws InterruptedException, IOException {System.out.println(multipartDTO.getBin().getBytes().length);System.out.println(multipartDTO.getComment());return "ojbk";}
響應緩存
1、要緩存響應,您需要一個可以讀取和寫入的緩存目錄,以及對緩存大小的限制。緩存目錄應該是私有的,不受信任的應用程序應該無法讀取其內容!
2、讓多個緩存同時訪問同一個緩存目錄是錯誤的。大多數應用程序應該只調用一次new OkHttpClient(),用它們的緩存配置它,并在任何地方使用相同的實例。否則兩個緩存實例會互相踩踏,破壞響應緩存,并可能使您的程序崩潰。
3、響應緩存對所有配置都使用HTTP標頭。您可以添加請求標頭,如Cache-Control: max-stale=3600,OkHttp的緩存將尊重它們。您的網絡服務器使用自己的響應標頭配置緩存響應的時間,如Cache-Control: max-age=9600。有緩存標頭可以強制緩存響應、強制網絡響應或強制使用條件GET驗證網絡響應。
4、要防止響應使用緩存,請使用CacheControl.FORCE_NETWORK。要防止它使用網絡,請使用CacheControl.FORCE_CACHE。請注意:如果您使用FORCE_CACHE并且響應需要網絡,OkHttp將返回504 Unsatisfiable Request響應。
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;import java.io.File;
import java.io.IOException;public class Test09CacheResponse {public static void main(String[] args) {try {Test09CacheResponse test = new Test09CacheResponse(new File("cache"));test.run();} catch (Exception e) {throw new RuntimeException(e);}}private final OkHttpClient client;public Test09CacheResponse(File cacheDirectory) throws Exception {int cacheSize = 10 * 1024 * 1024; // 10 MiBCache 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));}}
/*
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
*/
取消調用
使用Call.cancel()立即停止正在進行的調用。如果線程當前正在寫入請求或讀取響應,它將收到IOException。當不再需要調用時,使用它來節省網絡;例如,當您的用戶導航離開應用程序時。同步和異步調用都可以取消。
public class Test09CancelCall {public static void main(String[] args) throws Exception {new Test09CancelCall().run();}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);}}}
超時
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);}
}
每次調用配置
所有HTTP客戶端配置都存在OkHttpClient中,包括代理設置、超時和緩存。當您需要更改單個調用的配置時,調用OkHttpClient.newBuilder()。這將返回一個與原始客戶端共享相同連接池、調度程序和配置的構建器。在下面的示例中,我們發出一個超時500毫秒的請求,另一個超時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();// 拷貝client的屬性,并作出自定義修改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);}
}
處理鑒權
OkHttp可以自動重試未經身份驗證的請求。當響應為401 Not Authorized時,會要求Authenticator提供憑據。實現應該構建一個包含缺失憑據的新請求。如果沒有可用的憑據,則返回null以跳過重試。
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.
}
攔截器
參考:https://square.github.io/okhttp/features/interceptors/
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;import java.io.IOException;@Slf4j
public class TestInterceptor {public static void main(String[] args) throws IOException {OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new LoggingInterceptor())// .addNetworkInterceptor(new LoggingInterceptor()).build();Request request = new Request.Builder().url("http://www.publicobject.com/helloworld.txt").header("User-Agent", "OkHttp Example").build();Response response = client.newCall(request).execute();response.body().close();}static class LoggingInterceptor implements Interceptor {@Override public Response intercept(Interceptor.Chain chain) throws IOException {Request request = chain.request();long t1 = System.nanoTime();log.info(String.format("Sending request %s on %s%n%s",request.url(), chain.connection(), request.headers()));Response response = chain.proceed(request);long t2 = System.nanoTime();log.info(String.format("Received response for %s in %.1fms%n%s",response.request().url(), (t2 - t1) / 1e6d, response.headers()));return response;}}}