Vert.x學習之 Web Client
Vert.x Web Client
- 原文檔
- 組件源碼
- 組件示例
中英對照表
- Pump:泵(平滑流式數據讀入內存的機制,防止一次性將大量數據讀入內存導致內存溢出)
- Response Codec:響應編解碼器(編碼及解碼工具)
- Body Codec:響應體編解碼器
組件介紹
Vert.x Web Client(Web客戶端)是一個異步的 HTTP 和 HTTP/2 客戶端。
Web Client使得發送 HTTP 請求以及從 Web 服務器接收 HTTP 響應變得更加便捷,同時提供了額外的高級功能,例如:
-
JSON體的編碼和解碼
-
請求和響應泵
-
請求參數的處理
-
統一的錯誤處理
-
提交表單
制作Web Client的目的並非為了替換Vert.x Core中的 HttpClient
,而是基於該客戶端,擴展並保留其便利的設置和特性,例如請求連接池(Pooling),HTTP/2的支持,流水線/管線的支持等。當您需要對 HTTP 請求和響應做細微粒度控制時,您應當使用 HttpClient
。
另外Web Client並未提供 WebSocket API,此時您應當使用 HttpClient
。
使用Web Client
如需使用Vert.x Web Client,請先加入以下依賴:
-
Maven(在
pom.xml
文件中):<dependency> <groupId>io.vertx</groupId> <artifactId>vertx-web-client</artifactId> <version>3.4.2</version> </dependency>
-
Gradle(在
build.gradle
文件中):dependencies { compile ‘io.vertx:vertx-web-client:3.4.2‘ }
對Vert.x Core HTTP Client的回顧
Vert.x Web Client使用Vert.x Core的API,如您對此還不熟悉,請先熟悉 HttpClient
的一些基本概念。
創建Web Client
您可使用缺省設置創建一個 WebClient
:
WebClient client = WebClient.create(vertx);
您亦可使用配置選項來創建客戶端:
WebClientOptions options = newWebClientOptions() .setUserAgent("My-App/1.2.3"); options.setKeepAlive(false); WebClient client = WebClient.create(vertx, options);
Web Client配置選項繼承自 HttpClient
配置選項,使用時可根據實際情況選擇。
如已在程序中創建 HttpClient
,可用以下方式復用:
WebClient client = WebClient.wrap(httpClient);
發送請求
無請求體的簡單請求
一般情況下,HTTP GET,OPTIONS以及HEAD請求沒有請求體,可用以下方式發送無請求體的HTTP Requests(HTTP請求):
WebClient client = WebClient.create(vertx); // 發送GET請求 client .get(8080, "myserver.mycompany.com", "/some-uri") .send(ar -> { if (ar.succeeded()) { // 獲取響應 HttpResponse<Buffer> response = ar.result(); System.out.println("Received response with status code" + response.statusCode()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } }); //發送HEAD請求 client .head(8080, "myserver.mycompany.com", "/some-uri") .send(ar -> { if (ar.succeeded()) { // 獲取響應 HttpResponse<Buffer> response = ar.result(); System.out.println("Received response with status code" + response.statusCode()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
您可用以下鏈式方式向請求URI添加查詢參數
client .get(8080, "myserver.mycompany.com", "/some-uri") .addQueryParam("param", "param_value") .send(ar -> {});
在請求URI中的參數將會被預填充
HttpRequest<Buffer> request = client.get(8080, "myserver.mycompany.com", "/some-uri?param1=param1_value¶m2=param2_value"); // 添加param3(參數3) request.addQueryParam("param3", "param3_value"); // 覆蓋param2(參數2) request.setQueryParam("param2", "another_param2_value");
設置請求URI將會自動清除已有的查詢參數
HttpRequest<Buffer> request = client.get(8080, "myserver.mycompany.com", "/some-uri"); // 添加param1(參數1) request.addQueryParam("param1", "param1_value"); // 覆蓋param1(參數1)同時新增param2(參數2) request.uri("/some-uri?param1=param1_value¶m2=param2_value");
填充請求體
如需要發送請求體,可使用相同的API並在最後加上 sendXXX
方法發送相應的請求體。
例如用 sendBuffer
方法發送一個緩沖體:
client .post(8080, "myserver.mycompany.com", "/some-uri") .sendBuffer(buffer, ar -> { if (ar.succeeded()) { // Ok } });
client .post(8080, "myserver.mycompany.com", "/some-uri") .sendBuffer(buffer, ar -> { if (ar.succeeded()) { // Ok } });
有時候我們並不希望將所有數據一次性全部讀入內存,因為文件太大或希望同時處理多個請求,希望每個請求僅使用最小的內存。出於此目的,Web Client可用 sendStream
方法發送流式數據 ReadStream<Buffer>
(例如 AsyncFile
便是一個 ReadStream<Buffer>
):
client .post(8080, "myserver.mycompany.com", "/some-uri") .sendStream(stream, resp -> {});
Web Client會為您設置好傳輸泵以平滑傳輸流。如果流長度未知則使用分塊傳輸(chunked transfer)。
如已知流的大小,可在HTTP協議頭中設置 content-length
屬性
fs.open("content.txt", new OpenOptions(), fileRes -> { if (fileRes.succeeded()) { ReadStream<Buffer> fileStream = fileRes.result(); String fileLen = "1024"; // 用POST方法發送文件 client .post(8080, "myserver.mycompany.com", "/some-uri") .putHeader("content-length", fileLen) .sendStream(fileStream, ar -> { if (ar.succeeded()) { // Ok } }); } });
此時POST方法不會使用分塊傳輸。
JSON體
有時您需要在請求體中使用JSON格式,可使用 sendJsonObject
方法發送 JsonObject
:
client .post(8080, "myserver.mycompany.com", "/some-uri") .sendJsonObject(new JsonObject() .put("firstName", "Dale") .put("lastName", "Cooper"), ar -> { if (ar.succeeded()) { // Ok } });
在Java,Groovy以及Kotlin語言中,您亦可使用 sendJson
方法發送POJO(Plain Old Java Object),該方法會自動調用 Json.encode
方法將 POJO 映射為 JSON:
client .post(8080, "myserver.mycompany.com", "/some-uri") .sendJson(new User("Dale", "Cooper"), ar -> { if (ar.succeeded()) { // Ok } });
請註意:
Json.encode
方法使用Jackson的 mapper將 POJO 映射成 JSON。
表單提交
您可使用 sendForm
方法發送HTTP表單。
MultiMap form = MultiMap.caseInsensitiveMultiMap(); form.set("firstName", "Dale"); form.set("lastName", "Cooper"); // 用URL編碼方式提交表單 client .post(8080, "myserver.mycompany.com", "/some-uri") .sendForm(form, ar -> { if (ar.succeeded()) { // Ok } });
缺省情況下,提交表單的請求頭中的 content-type
屬性值為 application/x-www-form-urlencoded
,您亦可將其設置為 multipart/form-data
:
MultiMap form = MultiMap.caseInsensitiveMultiMap(); form.set("firstName", "Dale"); form.set("lastName", "Cooper"); // 用分塊方式編碼提交表單 client .post(8080, "myserver.mycompany.com", "/some-uri") .putHeader("content-type", "multipart/form-data") .sendForm(form, ar -> { if (ar.succeeded()) { // Ok } });
請註意:當前版本並不支持分塊文件編碼(multipart files,即文件上傳),該功能可能在將來版本中予以支持。
填充請求頭
您可使用以下方式填充請求頭:
HttpRequest<Buffer> request = client.get(8080, "myserver.mycompany.com", "/some-uri"); MultiMap headers = request.headers(); headers.set("content-type", "application/json"); headers.set("other-header", "foo");
此處 Headers 是一個 MultiMap
對象,提供了增加、設置以及刪除頭屬性操作的入口。HTTP頭的某些特定屬性允許設置多個值。
您亦可通過 putHeader
方法寫入頭屬性:
HttpRequest<Buffer> request = client.get(8080, "myserver.mycompany.com", "/some-uri"); request.putHeader("content-type", "application/json"); request.putHeader("other-header", "foo");
重用請求
send
方法可被重復多次調用,這使得配置以及重用 HttpRequest
對象變得更加便捷:
HttpRequest<Buffer> get = client.get(8080, "myserver.mycompany.com", "/some-uri"); get.send(ar -> { if (ar.succeeded()) { // Ok } }); // 再次發送同樣的請求 get.send(ar -> { if (ar.succeeded()) { // Ok } });
請註意,HttpRequest
對象是可變的。 所以在修改緩存中的對象之前,您應當使用 copy
方法先復制一份拷貝:
HttpRequest<Buffer> get = client.get(8080, "myserver.mycompany.com", "/some-uri"); get.send(ar -> { if (ar.succeeded()) { // Ok } }); // 獲取同樣的請求 get.copy() .putHeader("an-header", "with-some-value") .send(ar -> { if (ar.succeeded()) { // Ok }
超時
您可通過 timeout
方法設置超時時間。
client .get(8080, "myserver.mycompany.com", "/some-uri") .timeout(5000) .send(ar -> { if (ar.succeeded()) { // Ok } else { // 此處可填入超時處理部分代碼 } });
若請求在設定時間內沒返回任何數據,則一個超時異常將會傳遞給響應處理代碼。
處理HTTP響應
Web Client請求發送之後,返回的結果將會被包裝在異步結果 HttpResponse
中。
當響應被成功接收到之後,相應的回調函數將會被觸發。
client .get(8080, "myserver.mycompany.com", "/some-uri") .send(ar -> { if (ar.succeeded()) { HttpResponse<Buffer> response = ar.result(); System.out.println("Received response with status code" + response.statusCode()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
警告:缺省狀況下,響應會被完全緩沖讀入內存,請用
BodyCodec.pipe
方法將響應寫入流。
響應編解碼器
缺省狀況下,響應以緩沖形式提供,並不提供任何形式的解碼。
可用 BodyCodec
將響應定制成以下類型:
- 普通字符串
- JSON對象
- 將JSON映射成POJO
WriteStream
響應體編解碼器對二進制數據流解碼,以節省您在響應處理中的代碼。
使用 BodyCodec.jsonObject
將結果解碼為JSON對象:
client .get(8080, "myserver.mycompany.com", "/some-uri") .as(BodyCodec.jsonObject()) .send(ar -> { if (ar.succeeded()) { HttpResponse<JsonObject> response = ar.result(); JsonObject body = response.body(); System.out.println("Received response with status code" + response.statusCode() + " with body " + body); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
在Java,Groovy以及Kotlin語言中,JSON對象可被解碼映射成POJO:
client .get(8080, "myserver.mycompany.com", "/some-uri") .as(BodyCodec.json(User.class)) .send(ar -> { if (ar.succeeded()) { HttpResponse<User> response = ar.result(); User user = response.body(); System.out.println("Received response with status code" + response.statusCode() + " with body " + user.getFirstName() + " " + user.getLastName()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
當響應結果較大時,請使用 BodyCodec.pipe
方法。響應體編解碼器將響應結果壓入 WriteStream
並在最後發出成功或失敗的信號。
client .get(8080, "myserver.mycompany.com", "/some-uri") .as(BodyCodec.pipe(writeStream)) .send(ar -> { if (ar.succeeded()) { HttpResponse<Void> response = ar.result(); System.out.println("Received response with status code" + response.statusCode()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
最後,如您對響應結果不感興趣,可用 BodyCodec.none
廢棄響應體。
client .get(8080, "myserver.mycompany.com", "/some-uri") .as(BodyCodec.none()) .send(ar -> { if (ar.succeeded()) { HttpResponse<Void> response = ar.result(); System.out.println("Received response with status code" + response.statusCode()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
若無法預知響應內容類型,您依舊可以在獲取結果之後,用 bodyAsXXX()
方法將其轉換成特定的類型
client .get(8080, "myserver.mycompany.com", "/some-uri") .send(ar -> { if (ar.succeeded()) { HttpResponse<Buffer> response = ar.result(); // 將結果解碼為Json對象 JsonObject body = response.bodyAsJsonObject(); System.out.println("Received response with status code" + response.statusCode() + " with body " + body); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
警告:這種方式僅對響應結果為緩沖體有效。
處理30x重定向
缺省狀況下,客戶端將會依照30x狀態碼自動重定向,您可使用 WebClientOptions
予以配置:
WebClient client = WebClient.create(vertx, new WebClientOptions().setFollowRedirects(false));
客戶端將會執行最多達16
次重定向,該參數亦可在 WebClientOptions
配置:
WebClient client = WebClient.create(vertx, new WebClientOptions().setMaxRedirects(5));
使用HTTPS
Vert.x Web Client可用與 HttpClient
相同方式配置HTTPS協議。
您可對每個請求單獨設置:
client .get(443, "myserver.mycompany.com", "/some-uri") .ssl(true) .send(ar -> { if (ar.succeeded()) { // 獲取響應 HttpResponse<Buffer> response = ar.result(); System.out.println("Received response with status code" + response.statusCode()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
或使用絕對路徑:
client .getAbs("https://myserver.mycompany.com:4043/some-uri") .send(ar -> { if (ar.succeeded()) { // 獲取響應 HttpResponse<Buffer> response = ar.result(); System.out.println("Received response with status code" + response.statusCode()); } else { System.out.println("Something went wrong " + ar.cause().getMessage()); } });
RxJava API
RxJava的 HttpRequest
提供了原版API的響應式版本,rxSend
方法返回一個可被訂閱的 Single<HttpResponse<Buffer>>
,故單個 Single
可被多次訂閱。
Single<HttpResponse<Buffer>> single = client .get(8080, "myserver.mycompany.com", "/some-uri") .rxSend(); // 發送一次請求,並處理其響應,rx通常通過訂閱觸發各種響應 single.subscribe(response -> { System.out.println("Received 1st response with status code" + response.statusCode()); }, error -> { System.out.println("Something went wrong " + error.getMessage()); }); // 再次發送請求 single.subscribe(response -> { System.out.println("Received 2nd response with status code" + response.statusCode()); }, error -> { System.out.println("Something went wrong " + error.getMessage()); });
獲取到的 Single
可與其它RxJava API自然組合成鏈式處理
Single<String> url = client .get(8080, "myserver.mycompany.com", "/some-uri") .rxSend() .map(HttpResponse::bodyAsString); // 用flatMap將返回值內的鏈接作為參數傳入lambda,在lambda中將其設置成發送請求,並返回Single,在下一步訂閱中予以觸發 url .flatMap(u -> client.getAbs(u).rxSend()) .subscribe(response -> { System.out.println("Received response with status code" + response.statusCode()); }, error -> { System.out.println("Something went wrong " + error.getMessage()); });
之前的例子可寫成
Single<HttpResponse<JsonObject>> single = client .get(8080, "myserver.mycompany.com", "/some-uri") .putHeader("some-header", "header-value") .addQueryParam("some-param", "param value") .as(BodyCodec.jsonObject()) .rxSend(); single.subscribe(resp -> { System.out.println(resp.statusCode()); System.out.println(resp.body()); });
當發送請求體為 Observable<Buffer>
時,應使用 sendStream
:
Observable<Buffer> body = getPayload(); Single<HttpResponse<Buffer>> single = client .post(8080, "myserver.mycompany.com", "/some-uri") .rxSendStream(body); single.subscribe(resp -> { System.out.println(resp.statusCode()); System.out.println(resp.body()); });
當訂閱時,body
將會被訂閱,其內容將會被用於請求中。
Vert.x學習之 Web Client