HttpClient: A API Moderna do java.net.http
Objective
O HttpClient (em java.net.http, desde o Java 11) substituiu o antigo HttpURLConnection por uma API baseada em builders, construída especificamente para HTTP: uma requisição e sua configuração são montadas com builders fluentes em vez de mutar um objeto de conexão propriedade por propriedade, o client fala HTTP/2 por padrão, e toda requisição pode ser enviada de forma síncrona (send(), bloqueante) ou assíncrona (sendAsync(), retornando um CompletableFuture).
Use Cases
- Chamar uma API REST e obter o corpo da resposta de volta como
String, sem conectar manualmente umInputStreamReadersobre o stream de entrada da conexão. - Disparar várias chamadas HTTP independentes concorrentemente e compor seus resultados com
CompletableFuture, em vez de bloquear uma thread por chamada. - Baixar um arquivo direto para o disco entregando ao corpo da resposta um
Pathde destino, sem fazer streaming de bytes manualmente pelo código da aplicação. - Configurar um client (timeouts, política de redirecionamento, proxy) uma única vez e reutilizá-lo para toda requisição na aplicação, já que instâncias de
HttpClientsão imutáveis e thread-safe.
Deep Dive
Construindo um client
javaHttpClient client = HttpClient.newHttpClient(); // default settingsjavaHttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();newHttpClient() é um atalho para o caso comum; newBuilder() expõe os botões de configuração — versão do protocolo, política de redirecionamento (ALWAYS, NEVER, ou NORMAL, que segue redirecionamentos exceto downgrades de HTTPS para HTTP), timeout de conexão, seletor de proxy, autenticador. Sem configuração, o client prefere HTTP/2 e recua para HTTP/1.1 quando o servidor ou proxy não o suporta. Um HttpClient já construído é imutável e seguro para compartilhar/reutilizar entre muitas requisições em vez de construir um por chamada.
Construindo uma requisição
javaHttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users/42"))
.header("Accept", "application/json")
.GET() // default method is GET; also POST/PUT/DELETE(...) etc.
.build();Assim como o client, HttpRequest é construído uma vez via HttpRequest.newBuilder() e é imutável depois disso — headers, método, URI e corpo ficam todos fixos no momento da construção.
Enviando: síncrono vs. assíncrono
javaHttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body());
send() bloqueia a thread chamadora até a resposta chegar — direto ao ponto para scripts e cadeias de chamada simples.
javaclient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept(System.out::println);
sendAsync() retorna um CompletableFuture<HttpResponse<T>> imediatamente; a requisição de fato roda sem bloquear a thread chamadora, e .thenApply()/.thenAccept()/.thenCombine() encadeiam processamento adicional assim que a resposta chega — o mesmo estilo de composição de qualquer outro CompletableFuture.
BodyHandlers: escolhendo o formato do corpo da resposta
javaHttpResponse.BodyHandlers.ofString(); // response.body() is a String
HttpResponse.BodyHandlers.ofByteArray(); // response.body() is a byte[]
HttpResponse.BodyHandlers.ofFile(Path.of("out.bin")); // streams straight to a file
HttpResponse.BodyHandlers.ofInputStream(); // response.body() is an InputStreamO BodyHandler passado para send()/sendAsync() determina o parâmetro de tipo de HttpResponse<T> — escolha ofFile() para um download grande, de modo que os bytes sejam gravados em disco em stream em vez de bufferizar a resposta inteira em memória, e ofString() para uma resposta típica de API JSON.
Trade-offs
HttpURLConnectionainda existe e ainda funciona — oHttpClientnão o substitui em nível de linguagem, ele é simplesmente a API para a qual a documentação oficial e a maioria dos tutoriais atuais apontam em código novo, graças à ergonomia dos builders e ao suporte assíncrono nativo que oHttpURLConnectionnunca teve.- HTTP/2 por padrão significa conexões multiplexadas, o que muda algumas suposições carregadas de código que só conhecia HTTP/1.1 — por exemplo, contar com uma conexão por requisição para raciocínios de ordenação ou rate-limiting deixa de valer da mesma forma.
- O
CompletableFuturedosendAsync()compõe bem, mas exceções surgem de forma diferente dosend(). Umsend()síncrono lançaIOExceptiondiretamente; uma cadeia assíncrona embrulha falhas dentro do próprio future, e esquecer um estágio.exceptionally()/.handle()significa que uma requisição falha pode silenciosamente não produzir nenhum erro visível até que algo chame.get()ou.join().javaclient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .exceptionally(ex -> { System.err.println("request failed: " + ex); return null; }) .thenAccept(System.out::println); - Escolher o
BodyHandlererrado para o tamanho da resposta importa.ofString()/ofByteArray()bufferizam o corpo inteiro em memória antes de retornar — bom para um payload JSON pequeno, uma péssima escolha para um download de vários gigabytes, ondeofFile()(ouofInputStream()para streaming manual) evita manter tudo em memória de uma vez. - O motivo original de existência do
sendAsync()— não travar uma das threads de plataforma, que são limitadas, esperando I/O — praticamente desaparece com virtual threads (JDK 21+). Uma virtual thread bloqueada emsend()não prende uma thread do SO da forma que uma platform thread prenderia; a orientação atual para código baseado em virtual threads favorece osend()síncrono, mais simples, disparando muitas virtual threads para concorrência, em vez de compor cadeias deCompletableFuturepara evitar bloquear um tipo de thread que já não é mais escasso.