Explorando o novo cliente HTTP no Java 9 e 11

Explorando o novo cliente HTTP no Java 9 e 11

1. Introdução

Neste tutorial, vamos explorar a nova incubação dehttps://docs.oracle.com/javase/9/docs/api/jdk/incubator/http/HttpClient.html. do Java 9

Até muito recentemente, o Java fornecia apenas a APIHttpURLConnection - que é de baixo nível e não é conhecida por ser amigável ao usuário and rica em recursos.

Portanto, algumas bibliotecas de terceiros amplamente utilizadas foram comumente usadas - comoApache HttpClient,Jetty eRestTemplate do Spring.

2. Configuração inicial

The HTTP Client module is bundled as an incubator module em JDK 9 e suportaHTTP/2 com compatibilidade com versões anteriores ainda facilitando HTTP / 1.1.

Para usá-lo, precisamos definir nosso módulo usando um arquivomodule-info.java que também indica o módulo necessário para executar nosso aplicativo:

module com.example.java9.httpclient {
  requires jdk.incubator.httpclient;
}

3. Visão geral da API do cliente HTTP

Ao contrário deHttpURLConnection,, o cliente HTTP fornece mecanismos de solicitação síncrona e assíncrona.

A API consiste em 3 classes principais:

  • HttpRequest representa a solicitação a ser enviada por meio doHttpClient

  • HttpClient se comporta como um contêiner para informações de configuração comuns a várias solicitações

  • HttpResponse representa o resultado de uma chamadaHttpRequest

Examinaremos cada um deles com mais detalhes nas seções a seguir. Primeiro, vamos nos concentrar em um pedido.

4. HttpRequest

HttpRequest, como o nomesuggests, é um objeto que representa a solicitação que queremos enviar. Novas instâncias podem ser criadas usandoHttpRequest.Builder.

Podemos obtê-lo chamandoHttpRequest.newBuilder(). A classeBuilder fornece vários métodos que podemos usar para configurar nossa solicitação.

Cobriremos os mais importantes.

4.1. ConfigurandoURI

A primeira coisa que precisamos fazer ao criar uma solicitação é fornecer o URL.

Podemos fazer isso de duas maneiras - usando o construtor paraBuilder com o parâmetroURI ou chamando o métodouri(URI) na instânciaBuilder:

HttpRequest.newBuilder(new URI("https://postman-echo.com/get"))

HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))

A última coisa que precisamos configurar para criar uma solicitação básica é um método HTTP.

4.2. Especificando o método HTTP

Podemos definir o método HTTP que nossa solicitação usará chamando um dos métodos deBuilder:

  • OBTER()

  • POST (corpo BodyProcessor)

  • PUT (BodyProcessor body)

  • DELETE (corpo BodyProcessor)

CobriremosBodyProcessor em detalhes mais tarde. Agora, vamos apenas criara very simple GET request example:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .GET()
  .build();

Esta solicitação tem todos os parâmetros exigidos porHttpClient. No entanto, às vezes precisamos adicionar parâmetros adicionais à nossa solicitação; aqui estão alguns importantes são:

  • a versão do protocolo HTTP

  • cabeçalhos

  • um tempo limite

4.3. Configurando a versão do protocolo HTTP

A API aproveita totalmente o protocolo HTTP / 2 e o usa por padrão, mas podemos definir qual versão do protocolo queremos usar.

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .version(HttpClient.Version.HTTP_2)
  .GET()
  .build();

É importante mencionar aqui que o cliente fará fallback para, por exemplo, HTTP / 1.1 se HTTP / 2 não for compatível.

4.4. Configurando cabeçalhos

Caso desejemos adicionar cabeçalhos adicionais à nossa solicitação, podemos usar os métodos do construtor fornecidos.

Podemos fazer isso de duas maneiras:

  • passando todos os cabeçalhos como pares de valores-chave para o métodoheaders() ou por

  • usando o métodoheader() para o cabeçalho de valor-chave único:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .headers("key1", "value1", "key2", "value2")
  .GET()
  .build();

HttpRequest request2 = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .header("key1", "value1")
  .header("key2", "value2")
  .GET()
  .build();

O último método útil que podemos usar para personalizar nossa solicitação éa timeout().

4.5. Definindo um tempo limite

Vamos agora definir quanto tempo queremos esperar por uma resposta.

Se o tempo definido expirar, umHttpTimeoutException será lançado; o tempo limite padrão é definido como infinito.

O tempo limite pode ser definido com o objetoDuration - chamando o métodotimeout() na instância do construtor:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .timeout(Duration.of(10, SECONDS))
  .GET()
  .build();

5. Definindo um corpo de solicitação

Podemos adicionar um corpo a uma solicitação usando os métodos do construtor de solicitações:POST(BodyProcessor body),PUT(BodyProcessor body)eDELETE(BodyProcessor body).

A nova API fornece várias implementaçõesBodyProcessor prontas para usar, o que simplifica a passagem do corpo da solicitação:

  • StringProcessor (lê o corpo de umString, criado comHttpRequest.BodyProcessor.fromString)

  • InputStreamProcessor (lê o corpo de umInputStream, criado comHttpRequest.BodyProcessor.fromInputStream)

  • ByteArrayProcessor (lê o corpo de uma matriz de bytes, criada comHttpRequest.BodyProcessor.fromByteArray)

  • FileProcessor (lê o corpo de um arquivo no caminho fornecido, criado comHttpRequest.BodyProcessor.fromFile)

No caso de não precisarmos de um corpo, podemos simplesmente passar umHttpRequest.noBody():

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .POST(HttpRequest.noBody())
  .build();

5.1. StringBodyProcessor

Definir um corpo de solicitação com qualquer implementaçãoBodyProcessor é muito simples e intuitivo.

Por exemplo, se quisermos passar umString simples como um corpo, podemos usarStringBodyProcessor.

Como já mencionamos, este objeto pode ser criado com um método de fábricafromString(); leva apenas um objetoString como argumento e cria um corpo a partir dele:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor.fromString("Sample request body"))
  .build();

5.2. InputStreamBodyProcessor

Para fazer isso, oInputStream deve ser passado como umSupplier (para tornar sua criação preguiçosa), então é um pouco diferente do descrito acimaStringBodyProcessor.

No entanto, isso também é bastante direto:

byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor
   .fromInputStream(() -> new ByteArrayInputStream(sampleData)))
  .build();

Observe como usamos umByteArrayInputStream simples aqui; que pode, é claro, ser qualquer implementaçãoInputStream.

5.3. ByteArrayProcessor

Também podemos usarByteArrayProcessore passar uma matriz de bytes como parâmetro:

byte[] sampleData = "Sample request body".getBytes();
HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor.fromByteArray(sampleData))
  .build();

5.4. FileProcessor

Para trabalhar com um arquivo, podemos fazer uso dosFileProcessor fornecidos; seu método de fábrica leva um caminho para o arquivo como um parâmetro e cria um corpo a partir do conteúdo:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/post"))
  .headers("Content-Type", "text/plain;charset=UTF-8")
  .POST(HttpRequest.BodyProcessor.fromFile(
    Paths.get("src/test/resources/sample.txt")))
  .build();

Cobrimos como criarHttpRequest e como definir parâmetros adicionais nele.

Agora é hora de dar uma olhada mais profunda na classeHttpClient, que é responsável por enviar solicitações e receber respostas.

6. HttpClient

Todas as solicitações são enviadas usandoHttpClient, que pode ser instanciado usando o métodoHttpClient.newBuilder() ou chamandoHttpClient.newHttpClient().

Ele fornece muitos métodos úteis e autoexplicativos que podemos usar para lidar com nossa solicitação / resposta.

Vamos cobrir alguns deles aqui.

6.1. Configurando um proxy

Podemos definir um proxy para a conexão. Simplesmente chame o métodoproxy() em uma instânciaBuilder:

HttpResponse response = HttpClient
  .newBuilder()
  .proxy(ProxySelector.getDefault())
  .build()
  .send(request, HttpResponse.BodyHandler.asString());

Em nosso exemplo, usamos o proxy do sistema padrão.

6.2. Definindo a política de redirecionamento

Às vezes, a página que queremos acessar mudou para um endereço diferente.

Nesse caso, receberemos o código de status HTTP 3xx, geralmente com as informações sobre o novo URI. HttpClient can redirect the request to the new URI automatically if we set the appropriate redirect policy.

Podemos fazer isso com o métodofollowRedirects() emBuilder:

HttpResponse response = HttpClient.newBuilder()
  .followRedirects(HttpClient.Redirect.ALWAYS)
  .build()
  .send(request, HttpResponse.BodyHandler.asString());

Todas as políticas são definidas e descritas em enumHttpClient.Redirect.

6.3. ConfigurandoAuthenticator para uma conexão

UmAuthenticator é um objeto que negocia credenciais (autenticação HTTP) para uma conexão.

Ele fornece diferentes esquemas de autenticação (por exemplo, autenticação básica ou digest). Na maioria dos casos, a autenticação requer nome de usuário e senha para conectar-se a um servidor.

Podemos usar a classePasswordAuthentication que é apenas uma detentora destes valores:

HttpResponse response = HttpClient.newBuilder()
  .authenticator(new Authenticator() {
    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
      return new PasswordAuthentication(
        "username",
        "password".toCharArray());
    }
}).build()
  .send(request, HttpResponse.BodyHandler.asString());

No exemplo acima, passamos os valores de nome de usuário e senha como texto sem formatação; é claro que, em um cenário de produção, isso terá que ser diferente.

Observe que nem toda solicitação deve usar o mesmo nome de usuário e senha. A classeAuthenticator fornece uma série de métodosgetXXX (por exemplo,getRequestingSite()) que podem ser usados ​​para descobrir quais valores devem ser fornecidos.

Agora vamos explorar um dos recursos mais úteis do novoHttpClient - chamadas assíncronas para o servidor.

6.4. Solicitações de envio - Sincronização vs. Assíncrono

O novo HttpClient oferece duas possibilidades para enviar uma solicitação para um servidor:

  • send(…) – synchronously (bloqueia até que venha a resposta)

  • sendAsync(…) – asynchronously (não espera pela resposta, não bloqueia)

Até agora, o métodosend(. ..) naturalmente espera por uma resposta:

HttpResponse response = HttpClient.newBuilder()
  .build()
  .send(request, HttpResponse.BodyHandler.asString());

Esta chamada retorna um objetoHttpResponse, e temos certeza de que a próxima instrução de nosso fluxo de aplicativo será executada apenas quando a resposta já estiver aqui.

No entanto, há muitas desvantagens, especialmente quando estamos processando grandes quantidades de dados.

Então, agora, podemos usar o métodosendAsync(. ..) - que retornaCompletableFeature<HttpResponse> -to process a request asynchronously:

CompletableFuture> response = HttpClient.newBuilder()
  .build()
  .sendAsync(request, HttpResponse.BodyHandler.asString());

A nova API também pode lidar com várias respostas e transmitir os corpos de solicitação e resposta:

List targets = Arrays.asList(
  new URI("https://postman-echo.com/get?foo1=bar1"),
  new URI("https://postman-echo.com/get?foo2=bar2"));
HttpClient client = HttpClient.newHttpClient();
List> futures = targets.stream()
  .map(target -> client
    .sendAsync(
      HttpRequest.newBuilder(target).GET().build(),
      HttpResponse.BodyHandler.asString())
    .thenApply(response -> response.body()))
  .collect(Collectors.toList());

6.5. ConfigurandoExecutor para chamadas assíncronas

Também podemos definir umExecutor que fornece threads a serem usados ​​por chamadas assíncronas.

Dessa forma, podemos, por exemplo, limitar o número de threads usados ​​para processar solicitações:

ExecutorService executorService = Executors.newFixedThreadPool(2);

CompletableFuture> response1 = HttpClient.newBuilder()
  .executor(executorService)
  .build()
  .sendAsync(request, HttpResponse.BodyHandler.asString());

CompletableFuture> response2 = HttpClient.newBuilder()
  .executor(executorService)
  .build()
  .sendAsync(request, HttpResponse.BodyHandler.asString());

Por padrão, oHttpClient usa o executorjava.util.concurrent.Executors.newCachedThreadPool().

6.6. Definindo umCookieManager

Com a nova API e construtor, é simples definir umCookieManager para nossa conexão. Podemos usar o método buildercookieManager(CookieManager cookieManager) para definirCookieManager específicos do cliente.

Vamos, por exemplo, definirCookieManager que não permite aceitar cookies de forma alguma:

HttpClient.newBuilder()
  .cookieManager(new CookieManager(null, CookiePolicy.ACCEPT_NONE))
  .build();

No caso de nossoCookieManager permitir que cookies sejam armazenados, podemos acessá-los verificandoCookieManager em nossoHttpClient:

httpClient.cookieManager().get().getCookieStore()

Agora vamos nos concentrar na última classe da API Http - oHttpResponse.

7. HttpResponse Objeto

A classeHttpResponse representa a resposta do servidor. Ele fornece vários métodos úteis - mas dois dos mais importantes são:

  • statusCode() - retorna o código de status (tipoint) para uma resposta (a classeHttpURLConnection contém valores possíveis)

  • body() - retorna um corpo para uma resposta (o tipo de retorno depende do parâmetroBodyHandler de resposta passado para o métodosend())

O objeto de resposta tem outro método útil que cobriremos comouri(),headers(),trailers()eversion().

7.1. URI do objeto de resposta

O métodouri() no objeto de resposta retorna oURI do qual recebemos a resposta.

Às vezes, pode ser diferente deURI no objeto de solicitação, porque pode ocorrer um redirecionamento:

assertThat(request.uri()
  .toString(), equalTo("http://stackoverflow.com"));
assertThat(response.uri()
  .toString(), equalTo("https://stackoverflow.com/"));

7.2. Cabeçalhos de resposta

Podemos obter cabeçalhos da resposta chamando o métodoheaders() em um objeto de resposta:

HttpResponse response = HttpClient.newHttpClient()
  .send(request, HttpResponse.BodyHandler.asString());
HttpHeaders responseHeaders = response.headers();

Ele retorna o objetoHttpHeaders como um tipo de retorno. Este é um novo tipo definido no pacotejdk.incubator.http que representa uma visualização somente leitura dos cabeçalhos HTTP.

Possui alguns métodos úteis que simplificam a busca pelo valor dos cabeçalhos.

7.3. Obtenha trailers da resposta

A resposta HTTP pode conter cabeçalhos adicionais que são incluídos após o conteúdo da resposta. Esses cabeçalhos são chamados de cabeçalhos de reboque.

Podemos obtê-los chamando o métodotrailers() emHttpResponse:

HttpResponse response = HttpClient.newHttpClient()
  .send(request, HttpResponse.BodyHandler.asString());
CompletableFuture trailers = response.trailers();

Observe que o métodotrailers() retorna o objetoCompletableFuture.

7.4. Versão da Resposta

O métodoversion() define qual versão do protocolo HTTP foi usada para conversar com um servidor.

Lembre-se que mesmo se definirmos que queremos usar HTTP / 2, o servidor pode responder via HTTP / 1.1.

A versão na qual o servidor respondeu é especificada na resposta:

HttpRequest request = HttpRequest.newBuilder()
  .uri(new URI("https://postman-echo.com/get"))
  .version(HttpClient.Version.HTTP_2)
  .GET()
  .build();
HttpResponse response = HttpClient.newHttpClient()
  .send(request, HttpResponse.BodyHandler.asString());
assertThat(response.version(), equalTo(HttpClient.Version.HTTP_1_1));

8. Cliente Java 11 Http

A principal mudança no Java 11 foi a padronização deHTTP client API that implements HTTP/2 and Web Socket.. Ele visa substituir a classeHttpUrlConnection legada que está presente no JDK desde os primeiros anos do Java.

A mudança foi implementada como parte do JEP 321.

8.1. Grandes mudanças como parte do JEP 321

  1. A API HTTP incubada do Java 9 agora está incorporada oficialmente à API do Java SE. O novoHTTP APIs pode ser encontrado emjava.net.HTTP. *

  2. A versão mais recente do protocolo HTTP foi projetada para melhorar o desempenho geral do envio de solicitações por um cliente e do recebimento de respostas do servidor. Isso é conseguido através da introdução de várias alterações, como multiplexação de fluxo, compactação de cabeçalho e promessas push.

  3. A partir do Java 11,the API is now fully asynchronous (the previous HTTP/1.1 implementation was blocking). chamadas assíncronas são implementadas usandoCompletableFuture. A implementação deCompletableFuture se encarrega de aplicar cada estágio após o término do anterior, portanto, todo o fluxo é assíncrono.

  4. A nova API do cliente HTTP fornece uma maneira padrão de executar operações de rede HTTP com suporte para recursos modernos da Web, como HTTP / 2, sem a necessidade de adicionar dependências de terceiros.

  5. As novas APIs fornecem suporte nativo para HTTP 1.1 / 2 WebSocket. As classes principais e a interface que fornecem a funcionalidade principal incluem:

    • OHttpClient class, java.net.http.HttpClient

    • A classeHttpRequest,java.net.http.HttpRequest

    • A interfaceHttpResponse , java.net.http.HttpResponse

    • A interfaceWebSocket,java.net.http.WebSocket

8.2. Problemas com o cliente HTTP Pre Java 11

A APIHttpURLConnection existente e sua implementação tiveram vários problemas:

  • O URLConnectionAPI foi projetado com vários protocolos que agora não estão mais funcionando (FTP, Gopher, etc.).

  • A API é anterior ao HTTP / 1.1 e é abstrata demais.

  • Funciona apenas no modo de bloqueio (ou seja, um encadeamento por solicitação / resposta).

  • É muito difícil de manter.

9. Alterações no cliente HTTP com Java 11

9.1. Introdução de classes estáticas de fábrica

Novas classes de fábrica estáticasBodyPublishers,BodySubscriberseBodyHandlers  são introduzidas, incluindo implementações existentes deBodyPublisher,BodySubscribereBodyHandler.

Eles são usados ​​para executar tarefas comuns úteis, como manipular o corpo da resposta como uma String ou transmitir o corpo para um Arquivo.

Por ex. no Pre Java 11, tivemos que fazer algo assim:

HttpResponse response = client.send(request, HttpResponse.BodyHandler.asString());

Agora podemos simplificar como:

HttpResponse response = client.send(request, BodyHandlers.ofString());

Além disso, o nome dos métodos estáticos foi padronizado para maior clareza.

Por ex. nomes de métodos comofromXxx são usados ​​quando os estamos usando como adaptadores ou nomes comoofXxx quando estamos criando manipuladores / assinantes predefinidos.

9.2. Métodos fluentes para tipos comuns de corpo

Foram introduzidos métodos convenientes de fábrica para editores e manipuladores criados para lidar com tipos comuns de corpos.

Por ex. abaixo, temos métodos fluentes para criar editores a partir de bytes, arquivos e strings:

BodyPublishers.ofByteArray
BodyPublishers.ofFile
BodyPublishers.ofString

Da mesma forma, para criar manipuladores a partir desses tipos comuns de corpo, podemos usar:

BodyHandlers.ofByteArray
BodyHandlers.ofString
BodyHandlers.ofFile

9.3. Outras alterações de API

1. Com esta nova API, usaremosBodyHandlers.discarding()eBodyHandlers.replacing(value) em vez dediscard(Object replacement):

HttpResponse response1 = HttpClient.newHttpClient()
            .send(request, BodyHandlers.discarding());
HttpResponse response1 = HttpClient.newHttpClient()
            .send(request, BodyHandlers.replacing(value));

2. Novo métodoofLines() em[.typeNameLabel]#BodyHandlers #is added to fluxo de shandle do corpo de resposta como um fluxo de linhas.

3. O métodofromLineSubscriber é adicionado emBodyHandlers class that can be used as an adapter between a BodySubscriber and a text-based Flow.Subscriber that parses text line by line.

4. Adicionado um novoBodySubscriber.mapping na classeBodySubscribers que pode ser usado para mapear de um tipo de corpo de resposta para outro aplicando a função dada ao objeto de corpo.

5. EmHttpClient.Redirect, constantes de enumSAME_PROTOCOLe políticaSECURE são substituídas por um novo enumNORMAL.

10. Manipulando promessas por push no HTTP / 2

Novo cliente Http suporta promessas push por meio da interfacePushPromiseHandler

Ele permite que o servidor “envie” conteúdo aos recursos adicionais do cliente enquanto solicita o recurso principal, economizando mais viagens de ida e volta e, como resultado, melhora o desempenho na renderização da página.

É realmente o recurso de multiplexação do HTTP / 2 que nos permite esquecer o agrupamento de recursos. Para cada recurso, o servidor envia uma solicitação especial, conhecida como promessa de envio ao cliente.

As promessas push recebidas, se houver, são tratadas pelosPushPromiseHandler fornecidos. Um PushPromiseHnadler com valor nulo rejeita qualquer promessa de envio.

OHttpClient tem um métodosendAsync sobrecarregado que nos permite lidar com tais promessas, conforme mostrado no exemplo abaixo.

Vamos primeiro criar umPushPromiseHandler:

private static PushPromiseHandler pushPromiseHandler() {
    return (HttpRequest initiatingRequest,
        HttpRequest pushPromiseRequest,
        Function,
        CompletableFuture>> acceptor) -> {
        acceptor.apply(BodyHandlers.ofString())
            .thenAccept(resp -> {
                System.out.println(" Pushed response: " + resp.uri() + ", headers: " + resp.headers());
            });
        System.out.println("Promise request: " + pushPromiseRequest.uri());
        System.out.println("Promise request: " + pushPromiseRequest.headers());
    };
}

A seguir, vamos usar o métodosendAsync para lidar com esta promessa push:

httpClient.sendAsync(pageRequest, BodyHandlers.ofString(), pushPromiseHandler())
    .thenAccept(pageResponse -> {
        System.out.println("Page response status code: " + pageResponse.statusCode());
        System.out.println("Page response headers: " + pageResponse.headers());
        String responseBody = pageResponse.body();
        System.out.println(responseBody);
    })
    .join();

11. Conclusão

Neste artigo, exploramosHttpClient API do Java 9, que oferece muita flexibilidade e recursos poderosos. O código completo usado pela API HttpClient do Java 9 está disponívelover on GitHub.

Também exploramos as novas alterações no Java 11 HttpClient, que padronizaram o HttpClient em incubação introduzido no Java 9 com alterações mais poderosas. Os trechos de código usados ​​para Java 11 Http Client também sãoavailable over Github.

Observação: nos exemplos, usamos pontos de extremidade REST de amostra fornecidos porhttps://postman-echo.com.