Skip to content

9 HttpClient

HttpClient

Java 11 introdujo una nueva API para la creación de clientes HTTP compatibles con HTTP/2. Además, y a diferencia de la clase HttpURLConnection, el nuevo cliente HTTP nos proporciona mecanismos para realizar y gestionar peticiones asíncronas.

La nueva API contiene tres clases principales:

  • HttpClient: Se comporta como un contenedor para la configuración común de múltiples peticiones HTTP.
  • HttpRequest: Representa la petición que se envía a través de un objeto HttpClient.
  • HttpResponse: Representa la respuesta de una petición HttpRequest realizada.

Para poder enviar una petición antes debemos construir un objeto HttpClient. Dicha construcción sigue el patrón de diseño builder. Así, deberemos obtener un objeto de la interface HttpClient.Builder, a través del método estático HttpClient.newBuilder(), configurarlo usando los métodos de HttpClient.Builder y finalmente llamar a su método build() para obtener el objeto HttpClient.

HttpClient httpClient = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(30))
        .executor(Executors.newFixedThreadPool(2))
        .version(HttpClient.Version.HTTP_2)
        .authenticator(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication("user", "password".toCharArray());
            }
        })
        .build();

Si queremos usar todos los valores por defecto, entonces bastará con hacer:

HttpClient httpClient = HttpClient.newBuilder().build();

En ese caso, es más cómodo usar el método estático HttpClient.newHttpClient(), que es equivalente.

HttpClient httpClient = HttpClient.newHttpClient();

Una vez creado, el objeto HttpClient es inmutable, por lo que es thread-safe y podemos enviarle varias peticiones HTTP.

Por defecto, el cliente HTTP al realizar una petición tratará de abrir una conexión HTTP/2. Si el servidor responde con HTTP/1.1, el cliente automáticamente pasa a usar dicha versión. Si ya sabemos que el servidor sólo permite HTTP/1.1, podemos indicar que queremos usar dicha versión durante la construcción del cliente, a través del método version(Version.HTTP_1_1) del objeto HttpClient.Builder.

Con el método connectTimeout(duration) del objeto HttpClient.Builder podemos establecer cuánto tiempo debe esperar el cliente la respuesta del servidor al establecer una conexión. Si no es posible establecerla se lanzará la excepción HttpConnectTimeoutException.

El método executor(executor) del objeto HttpClient.Builder permite establecer qué ejecutor debe usarse para las peticiones asíncronas y tareas dependientes. Si no lo especificamos se creará un nuevo ThreadPoolExecutor.

Mediante el método authenticator(authenticator) podemos establecer el autentificador que se desea usar con las peticiones.

Para crear una petición también seguiremos el patrón de diseño builder. Así, llamaremos al método estático HttpRequest.newBuilder(), que retorna un objeto de la interfaz HttpRequest.Builder. Después llamaremos a sus métodos para configurar la petición. Finalmente, llamaremos al método build() para construir la petición, retornando un objeto de la clase HttpRequest.

HttpRequest httpRequest = HttpRequest.newBuilder()
        .GET()
        .uri(URI.create("https://httpbin.org/get"))
        .setHeader("User-Agent", "Java 11 HttpClient Bot")
        .build();

Para especificar la URI a la que queremos realizar la petición tenemos dos opciones: pasársela al método HttpRequest.newBuilder(uri), o, como hemos hecho en el ejemplo anterior, establecerla mediante el método uri(uri) del objeto HttpRequest.Builder.

El método setHeader(keyString, valueString) nos permite establecer una cabecera para la petición, sobrescribiéndola si ya existiera.

Para indicar el método (verbo) HTTP que queremos usar en nuestra petición, usaremos alguno de los métodos disponibles en HttpRequest.Builder, como GET(), POST(bodyPublisher), PUT(bodyPublisher)y DELETE(), o usar el método genérico method(methodString, bodyPublisher).

Como vemos, en los métodos POST(bodyPublisher) y PUT(bodyPublisher) debemos indicar cuál va a ser el cuerpo de la petición, representado por la interfaz HttpRequest.BodyPublisher. La clase de utilidad BodyPublishers proporciona una serie de métodos estáticos factoría para crear objetos HttpRequest.BodyPublisher a partir de valores de las fuentes de contenido más usuales:

  • HttpRequest.BodyPublishers.ofString(bodyString): Retorna un cuerpo de petición creado a partir de una cadena de caracteres.
  • HttpRequest.BodyPublishers.ofInputStream(streamSupplier): Retorna un cuerpo de petición creado a partir del contenido del un InputStream.
  • HttpRequest.BodyPublishers.ofByteArray(byteArray): Retorna un cuerpo de petición creado a partir del contenido del un byte[].
  • HttpRequest.BodyPublishers.ofFile(path): Retorna un cuerpo de petición creado a partir del contenido del un fichero.
  • HttpRequest.BodyPublishers.noBody(): Retorna un cuerpo de petición vacío.

Por ejemplo:

String json = new StringBuilder()
                .append("{")
                .append("\"name\":\"mkyong\",")
                .append("\"notes\":\"hello\"")
                .append("}").toString();
HttpRequest httpRequest = HttpRequest.newBuilder()
        .uri(new URI("https://postman-echo.com/post"))
        .setHeader("User-Agent", "Java 11 HttpClient Bot")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .header("Content-Type", "application/json")
        .build();

La API no nos proporciona un método estático factoría para cuando el cuerpo debe corresponder a los datos de un formulario, pero podemos crearlo nosotros:

public static HttpRequest.BodyPublisher ofFormData(Map<Object, Object> data) {
    var stringBuilder = new StringBuilder();
    for (Map.Entry<Object, Object> entry : data.entrySet()) {
        if (builder.length() > 0) {
            builder.append("&");
        }
        builder.append(URLEncoder.encode(entry.getKey().toString(),
                                        StandardCharsets.UTF_8));
        builder.append("=");
        builder.append(URLEncoder.encode(entry.getValue().toString(), 
                                        StandardCharsets.UTF_8));
    }
    return HttpRequest.BodyPublishers.ofString(builder.toString());
}

Y podemos usar dicho método de la siguiente manera:

Map<Object, Object> data = Map.of(
    "username", "abc",
    "password", "123",
    "custom", "secret",
    "ts", System.currentTimeMilis());
HttpRequest httpRequest = HttpRequest.newBuilder()
    .POST(ofFormData(data))
    .uri(URI.create("https://httpbin.org/post"))
    .setHeader("User-Agent", "Java 11 HttpClient Bot")
    .header("Content-Type", "application/x-www-form-urlencoded")
    .build();

HttpResponse es una interfaz cuyas implementaciones son retornadas por un objeto HttpClient cuando se envía una petición HttpRequest.

Los métodos más importantes de esta interfaz son:

  • statusCode(): Retorna un entero con el código de estado de la respuesta.
  • body(): Retorna el cuerpo de la respuesta pareado a un determinado tipo.
  • headers(): Retorna un objeto HttpHeaders con las cabeceras de la respuesta. Dicho objeto posee un método map() que retorna un Map<String, List<String>> con las cabeceras.
  • request(): Retorna el objeto HttpRequest correspondiente a la petición por la que se ha obtenido esta repuesta.
  • uri(): Retorna la URI desde la que se ha obtenido la respuesta.

Podemos enviar una petición HttpRequest a través de un cliente HttpClient de dos maneras distintas: asíncronamente y asíncronamente.

Para realizar una petición asíncrona deberemos usar el método send(httpRequest, bodyHandler) del objeto HttpClient, al que suministraremos además del objeto HttpRequest, un objeto que implemente la interfaz HttpResponse.BodyHandler<T> cuyo cometido será manejar el cuerpo de la respuesta hacia un destino de tipo T.

La clase de utilidad HttpResponse.BodyHandlers proporciona métodos estáticos factoría para construir manejadores del cuerpo de la respuesta hacia los destinos más habituales:

  • HttpResponse.BodyHandlers.ofString(bodyString): Retorna un manejador del cuerpo de la respuesta hacia una cadena de caracteres.
  • HttpResponse.BodyHandlers.ofInputStream(streamSupplier): Retorna un manejador del cuerpo de la respuesta hacia un InputStream.
  • HttpResponse.BodyHandlers.ofByteArray(byteArray): Retorna un manejador del cuerpo de la respuesta hacia un byte[].
  • HttpResponse.BodyHandlers.ofFile(path): Retorna un manejador del cuerpo de la respuesta hacia un fichero.
  • HttpResponse.BodyHandlers.ofLines(): Retorna un manejador del cuerpo de la respuesta hacia un Stream<String>.
  • HttpResponse.BodyHandlers.discarding(): Retorna un manejador del cuerpo de la respuesta que descarta su contenido.

Por ejemplo:

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
    .GET()
    .uri(URI.create("https://httpbin.org/get"))
    .build();
HttpResponse<String> httpResponse = 
    httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
if (httpResponse.statusCode() == 200) {
    String responseBody = httpResponse.body();
    System.out.print(responseBody);
}

Si queremos enviar la petición de manera asíncrona, de forma que el hilo que envía la petición no quede bloqueado, podemos usar el método sendAsync(httpRequest, bodyHandler), que retorna inmediatamente un objeto CompletableFuture, que es marcado como completado cuando la respuesta HttpResponse esté disponible.

La petición se ejecutará en el ejecutor que hayamos configurado en el objeto HttpClient, o en el ejecutor por defecto si es que no lo hemos configurado.

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
    .GET()
    .uri(URI.create("https://httpbin.org/get"))
    .build();
CompletableFuture<HttpResponse<String>> cfHttpResponse = 
    httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString());
cfHttpResponse
        .thenApply(response -> {
            System.out.println(response.statusCode());
            return response;
        })
        .thenApply(HttpResponse::body)
        .thenAccept(System.out::println)

Proyecto HttpClient

En este proyecto vamos a realizar una aplicación que realiza distintos tipos de peticiones HTTP a un servicio REST de ejemplo, usando el cliente Http disponible desde la versión 11 de Java. Cada petición y su correspondiente respuesta son mostradas por consola.

Para encapsular las distintas peticiones a la Web API REST, creamos una clase ApiService con los métodos para realizar las distintas peticiones. el constructor de esta clase recibe un objeto HttpClient a través del que realizar las peticiones.

Nuestro main construye el objeto HttpClient y el objeto ApiService, con el que interactúa para realizar las peticiones.

import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Scanner;

@SuppressWarnings("SameParameterValue")
public class Main {

    private final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5))
            .build();
    private final ApiService apiService = new ApiService(httpClient);
    private final Scanner scanner = new Scanner(System.in);

    Main() {
        int selectedOption = showMenu();
        processOption(selectedOption);
    }

    public static void main(String[] args) {
        new Main();
    }

    private int showMenu() {
        System.out.println("\nMENU");
        System.out.println("1. Show all posts synchronously");
        System.out.println("2. Show all posts");
        System.out.println("3. Show post");
        System.out.println("4. Show user's posts");
        System.out.println("5. Create post");
        System.out.println("6. Update post");
        System.out.println("7. Update post partially");
        System.out.println("8. Delete post");
        System.out.println("9. Show only headers");
        System.out.println("10. Show access options");
        System.out.print("Select an option: ");
        try {
            return scanner.nextInt();
        } catch (Exception e) {
            scanner.nextLine();
            return 0;
        }
    }

    private void processOption(int selectedOption) {
        switch (selectedOption) {
            case 1:
                showPostsSync();
                break;
            case 2:
                showPosts();
                break;
            case 3:
                showPost(1);
                break;
            case 4:
                showUserPosts(1);
                break;
            case 5:
                createPost("{\"title\": \"Baldomero\", \"body\": \"Llegate Ligero\", \"userId\": 1}");
                break;
            case 6:
                updatePost(1, "{\"id\": 1, \"title\": \"Baldomero\", \"body\": \"Llegate Ligero\", \"userId\": 1}");
                break;
            case 7:
                patchPost(1, "{\"title\": \"Baldomero\"}");
                break;
            case 8:
                deletePost(1);
                break;
            case 9:
                showPostsHeaders();
                break;
            case 10:
                showPostsAccessOptions();
                break;
        }
    }

    private void showPostsSync() {
        try {
            HttpResponse<String> httpResponse = apiService.getPostsSync();
            showResponse(httpResponse);
        } catch (InterruptedException | IOException e) {
            showError(e);
        }
    }

    private void showPosts() {
        apiService.getPosts().whenComplete(this::showResponseOrError).join();
    }

    private void showPostsHeaders() {
        apiService.getPostsHeaders().whenComplete(this::showResponseOrError).join();
    }

    private void showPostsAccessOptions() {
        apiService.getPostsAccessOptions().whenComplete(this::showResponseOrError).join();
    }

    private void showPost(int postId) {
        apiService.getPost(postId).whenComplete(this::showResponseOrError).join();
    }

    private void createPost(String jsonPost) {
        apiService.createPost(jsonPost).whenComplete(this::showResponseOrError).join();
    }

    private void updatePost(int postId, String jsonPost) {
        apiService.updatePost(postId, jsonPost).whenComplete(this::showResponseOrError).join();
    }

    private void patchPost(int postId, String jsonPost) {
        apiService.patchPost(postId, jsonPost).whenComplete(this::showResponseOrError).join();
    }

    private void deletePost(int postId) {
        apiService.deletePost(postId).whenComplete(this::showResponseOrError).join();
    }

    private void showUserPosts(int userId) {
        apiService.getUserPosts(userId).whenComplete(this::showResponseOrError).join();
    }

    private void showResponseOrError(HttpResponse<String> httpResponse, Throwable throwable) {
        if (throwable != null) {
            showError(throwable);
        } else {
            showResponse(httpResponse);
        }
    }

    private void showError(Throwable e) {
        System.out.println(e.toString());
    }

    private void showResponse(HttpResponse<String> response) {
        System.out.println(response.statusCode());
        response.headers().map().forEach((key, value) -> System.out.println(key + ": " + String.join(";", value)));
        if (response.statusCode() >= 200 && response.statusCode() < 300) {
            String responseBody = response.body();
            if (responseBody != null) {
                System.out.println("\n" + responseBody);
            }
        }
    }

}
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class ApiService {

    private final String BASE_URL = "https://jsonplaceholder.typicode.com/";
    private final HttpClient httpClient;

    public ApiService(HttpClient httpClient) {
        this.httpClient = httpClient;
    }

    public HttpResponse<String> getPostsSync() throws IOException, InterruptedException {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(BASE_URL + "posts"))
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.send(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> getPosts() {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(BASE_URL + "posts"))
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> getPostsHeaders() {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .method("HEAD", HttpRequest.BodyPublishers.noBody())
                .uri(URI.create(BASE_URL + "posts"))
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> getPostsAccessOptions() {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .method("OPTIONS", HttpRequest.BodyPublishers.noBody())
                .uri(URI.create(BASE_URL + "posts"))
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> getPost(int postId) {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(BASE_URL + "posts/" + postId))
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> createPost(String jsonPost) {
        HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.ofString(jsonPost);
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .POST(bodyPublisher)
                .uri(URI.create(BASE_URL + "posts"))
                .header("Content-Type", "application/json; charset=utf-8")
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> updatePost(int postId, String jsonPost) {
        HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.ofString(jsonPost);
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .PUT(bodyPublisher)
                .uri(URI.create(BASE_URL + "posts/" + postId))
                .header("Content-Type", "application/json; charset=utf-8")
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> patchPost(int postId, String jsonPost) {
        HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.ofString(jsonPost);
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .method("PATCH", bodyPublisher)
                .uri(URI.create(BASE_URL + "posts/" + postId))
                .header("Content-Type", "application/json; charset=utf-8")
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> deletePost(int postId) {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .DELETE()
                .uri(URI.create(BASE_URL + "posts/" + postId))
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

    public CompletableFuture<HttpResponse<String>> getUserPosts(int userId) {
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(BASE_URL + "users/" + userId + "/posts"))
                .build();
        HttpResponse.BodyHandler<String> bodyHandler = HttpResponse.BodyHandlers.ofString();
        return httpClient.sendAsync(httpRequest, bodyHandler);
    }

}