Java HTTP/2 客户端:从阻塞到异步 - sanjeevr


一个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();