Java HTTP/2 客戶端:從阻塞到非同步 - sanjeevr

banq發表於2022-05-05

一個HttpClient可以用來通過HTTP訪問網路上的任何資源。

在Java 11之前,開發者不得不使用傳統的HttpUrlConnection類,它被認為是更抽象的,或者使用第三方庫,如Apache HttpClient,或OkHttp。

從JDK11開始,它支援HTTP/1.1和HTTP/2,支援同步和非同步程式設計模型,將請求和響應體作為反應流處理,並遵循熟悉的構建器模式。預設情況下,客戶端將使用HTTP/2傳送請求。傳送到尚不支援HTTP/2的伺服器的請求將自動降級為HTTP/1.1。

新的API現在通過CompletableFutures提供非阻塞的請求和響應處理。其他概念,如反壓和流量控制,已經通過java.uti.consurrent.Flow API由反應式流提供。

讓我們深入瞭解一下使用Java HTTP客戶端執行普通任務的例子和配方。

同步GET
響應主體是一個字串

public void get(String uri) throws Exception {
    HttpClient client = HttpClient.newHttpClient()。
    HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(uri))
          .build()。
    HttpResponse<String> response = client.send(request, BodyHandlers.ofString())。
    System.out.println(response.body())。
}

響應體是一個檔案

public void get(String uri) throws Exception {
    HttpClient client = HttpClient.newHttpClient();
    HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(uri))
          .build()。
    HttpResponse<Path> response =
          client.send(request, BodyHandlers.ofFile(Paths.get("body.txt"))。
    System.out.println("檔案中的響應:" + response.body())。
}


非同步GET
非同步API立即返回一個CompletableFuture,當HttpResponse可用時,它就會完成。CompletableFuture是在Java 8中新增的,支援可組合的非同步程式設計。

響應體是一個字串

public CompletableFuture<String> get(String uri) {
    HttpClient client = HttpClient.newHttpClient()。
    HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(uri))
          .build()。
    return client.sendAsync(request, BodyHandlers.ofString())
          .thenApply(HttpResponse::body)。
}
CompletableFuture.thenApply(Function)方法可用於將HttpResponse對映到其body型別、狀態程式碼等。


POST
一個請求體可以由一個HttpRequest.BodyPublisher提供。

public void post(String uri, String data) throws Exception {
    HttpClient client = HttpClient.newBuilder().build()。
    HttpRequest request = HttpRequest.newBuilder()
            .URI(URI.create(uri))
            .POST(BodyPublishers.ofString(data))
            .build()。
    HttpResponse<?> response = client.send(request, BodyHandlers.discarding())。
    System.out.println(response.statusCode())。
}

上面的例子使用ofString BodyPublisher將給定的字串轉換為請求體位元組。

BodyPublisher是一個反應式流釋出器,按需釋出請求體的流。HttpRequest.Builder有一些允許設定BodyPublisher的方法;Builder::POST, Builder::PUT, 和Builder::method。HttpRequest.BodyPublishers類有一些方便的靜態工廠方法,可以為常見的資料型別建立一個BodyPublisher;ofString、ofByteArray、ofFile。

丟棄的BodyHandler可以用來接收和丟棄響應體,當它不感興趣的時候。

併發請求
結合Java Streams和CompletableFuture API來發出一些請求並等待其響應是很容易的。下面的例子為列表中的每個URI傳送了一個GET請求,並將所有的響應儲存為字串。

public void getURIs(List<URI> uris) {
    HttpClient client = HttpClient.newHttpClient();
    List<HttpRequest> requests = uris.stream()
            .map(HttpRequest::newBuilder)
            .map(reqBuilder -> reqBuilder.build())
            .collect(toList())。
    CompletableFuture.allOf(request.stream()
            .map(request -> client.sendAsync(request, ofString()))
            .toArray(CompletableFuture<?>[]:new))
            .join()。
}


獲取JSON
在許多情況下,響應體將是一些更高階別的格式。可以使用方便的響應體處理程式,同時使用第三方庫將響應體轉換為該格式。

下面的例子演示瞭如何使用Jackson庫,結合BodyHandlers::ofString,將JSON響應轉換為String鍵/值對的Map。

public CompletableFuture<Map<String,String>> JSONBodyAsMap(URI uri) {
    UncheckedObjectMapper objectMapper = new UncheckedObjectMapper();
    HttpRequest request = HttpRequest.newBuilder(uri)
          .header("Accept", "application/json")
          .build();
    return HttpClient.newHttpClient()
          .sendAsync(request, BodyHandlers.ofString())
          .thenApply(HttpResponse::body)
          .thenApply(objectMapper::readValue);
}
class UncheckedObjectMapper extends com.fasterxml.jackson.databind.ObjectMapper {
    /** Parses the given JSON string into a Map. */
    Map<String,String> readValue(String content) {
    try {
        return this.readValue(content, new TypeReference<>(){});
    } catch (IOException ioe) {
        throw new CompletionException(ioe);
    }
}


上面的例子使用ofString,它在記憶體中積累響應體的位元組。另外,也可以使用一個流式訂閱器,比如ofInputStream。

POST JSON
在許多情況下,請求體將是一些更高層次的格式。可以使用方便的請求體處理程式,以及一個第三方庫,將請求體轉換為該格式。

下面的例子演示瞭如何使用Jackson庫,結合BodyPublishers::ofString將String鍵/值對的Map轉換成JSON。

public CompletableFuture<Void> postJSON(URI uri,
                                        Map<String,String> map)
    throws IOException
{
    ObjectMapper objectMapper = new ObjectMapper();
    String requestBody = objectMapper
          .writerWithDefaultPrettyPrinter()
          .writeValueAsString(map);
    HttpRequest request = HttpRequest.newBuilder(uri)
          .header("Content-Type", "application/json")
          .POST(BodyPublishers.ofString(requestBody))
          .build();
    return HttpClient.newHttpClient()
          .sendAsync(request, BodyHandlers.ofString())
          .thenApply(HttpResponse::statusCode)
          .thenAccept(System.out::println);
}


設定一個代理
可以通過客戶端的Builder::proxy方法在HttpClient上配置一個ProxySelector。ProxySelector API為一個給定的URI返回一個特定的代理。在許多情況下,一個單一的靜態代理就足夠了。ProxySelector::of static工廠方法可以用來建立這樣一個選擇器。

響應主體是一個帶有指定代理的字串

public CompletableFuture<String> get(String uri) {
    HttpClient client = HttpClient.newBuilder()
          .proxy(ProxySelector.of(new InetSocketAddress("www-proxy.com", 8080))
          .build()。
    HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(uri))
          .build()。
    return client.sendAsync(request, BodyHandlers.ofString())
          .thenApply(HttpResponse::body)。
}

另外,也可以使用全系統預設的代理選擇器,這在macOS上是預設的。

HttpClient.newBuilder()
      .proxy(ProxySelector.getDefault())
      .build();

相關文章