Skip to content

8 OkHttp

OkHttp

OkHttp es una librería desarrollada por la empresa Square para crear clientes HTTP y HTTP/2 eficientes, lo que permite realizar las peticiones se realizan más rápidamente y ahorrar ancho de banda. Proporciona a los desarrolladores una API que incorpora clases para modelar las peticiones (request) y las respuestas (response).

Para poder usar esta librería será necesario añadir la dependencia correspondiente en el archivo build.gradle del módulo en cuestión si usas gradle o en el pom.xml si usas maven:

dependencies {
    // ...
    implementation 'com.squareup.okhttp3:okhttp:X.X.X'
}
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>X.X.X</version>
</dependency>

La clase OkHttpClient representa el cliente HTTP que será usado para realizar las peticiones y obtener las respuestas, por lo que crearemos un objeto de dicha clase sobre el que trabajaremos:

final OkHttpClient okHttpClient = new OkHttpClient();

Si queremos personalizar el cliente HTTP a la hora de construirlo usaremos el patrón de diseño builder a través de la clase OkHttpClient.Builder:

final OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor())
    .cache(new Cache(cacheDir, cacheSize))
    .connectTimeout(500, TimeUnit.MILLISECONDS)
    .readTimeout(500, TimeUnit.MILLISECONDS)
    .build();

Lo normal es que se cree un único objeto cliente OkHttpClient (patrón singleton), que será usado para realizar todas las peticiones.

Para representar una petición, la librería nos ofrece la clase Request, que construiremos a través de un objeto constructor de la clase Request.Builder, que proporciona, entre otros, los siguientes métodos de configuración de la petición:

  • url(url): URL destinataria de la petición.
  • header(sNombre, sValor): Añade un elemento de cabecera, sustituyendo su valor si ya existía en la petición un elemento de cabecera con el mismo nombre.
  • addHeader(sNombre, sValor): Añade un elemento de cabecera, incluso si ya existía otro elemento con el mismo nombre.
  • Métodos para indicar el verbo HTTP que se desea usar: get(), head(), post(requestBody), put(requestBody), patch(requestBody), delete(), method(verb, requestBody). Alguno de estos métodos establecen el cuerpo de la petición, representado por un objeto RequestBody.
  • build(): Construye, configura y retorna el objeto Request.

Veamos un ejemplo sencillo:

Request request = new Request.Builder()
      .url("http://publicobject.com/helloworld.txt")
        .build();

Si nuestra petición va a contener un cuerpo de petición, será necesario que creemos un objeto RequestBody a través de su método estático create(). Este método recibe el tipo MIME del cuerpo y el elemento a enviar, ya sea en forma de cadena de caracteres, fichero, etc. (el método está sobrecargado para tal fin). Por ejemplo:

RequestBody requestBody = 
    RequestBody.create(jsonPost, MediaType.get("application/json; charset=utf-8"));
Request request = new Request.Builder()
        .url(new URL("https://jsonplaceholder.typicode.com/posts"))
        .post(requestBody)
        .build();

Si el cuerpo de la petición debe ser enviado en forma de pares variable=valor codificados en la URL, para construir el RequestBody haremos uso de la clase FormBody.Builder, y usaremos sus métodos add(variable, valor) o addEncoded(variable, valor) para añadir cada uno de los pares, para finalmente llamar al método build() para obtener el objeto RequestBody. Por ejemplo:

SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss",
                Locale.getDefault());
URL url = new URL("http://www.informaticasaladillo.es/echo.php");
RequestBody formBody = new FormBody.Builder()
        .addEncoded("nombre", "Baldomero")
        .addEncoded("fecha", formatter.format(new Date()))
        .build();
Request request = new Request.Builder()
        .url(url)
        .post(formBody)
        .build();

Por su parte, la clase Call modela la realización de una petición. Para obtener un objeto Call usaremos el método newCall(request) del objeto OkHttpClient, que recibe el objeto Request correspondiente a la petición que se quiere llevar a cabo.

Call call = okHttpClient.newCall(request)

La clase Call nos proporciona dos métodos distintos para ejecutar la petición, dependiendo de si lo queremos hacer de forma síncrona o asíncrona.

El método execute() de un objeto Call realiza la petición de manera síncrona en el hilo desde el que se llame, quedando el llamador bloqueado hasta que se reciba la respuesta en forma de objeto de la clase Response. Debemos tener en cuenta que si se produce un error en la conexión, se lanzará una excepción, por lo que deberemos incluir la llamada a este método dentro de un bloque try catch.

Es importante recalcar que en entornos como Android, en los que no se permite por defecto operaciones de red desde el hilo principal, la llamada al método execute() deberá realizarse desde un hilo secundario.

Llamada síncrona con execute()

Request request = new Request.Builder()
    .url(new URL("https://jsonplaceholder.typicode.com/posts"))
    .get()
    .build();
Call call = okHttpClient.newCall(request);
// Blocking call
try (Response response = call.execute()) {
    // Process error
    // ...
} catch (IOException e) {
    // Process exception
    // ...
}

Si usamos el método enqueue(listenerCallback) del objeto Call, la petición se realiza de manera asíncrona en un hilo secundario gestionado por la librería. El método recibe un objeto listener que implemente la interfaz Callback, cuyo método onResponse(call, response) será llamado cuando se obtenga la respuesta (objeto de la clase Response), que le será pasada como parámetro. Si se produce un error en la petición, se llamada al método onFailure(call) del objeto listener.

Los métodos onResponse() y onFailure() son llamados en el hilo secundario, lo que es especialmente importante si estamos en un entorno Android, en el que no es posible actualizar la interfaz de usuario desde un hilo distinto al hilo principal.

Request request = new Request.Builder()
    .url(new URL("https://jsonplaceholder.typicode.com/posts"))
    .get()
    .build();
Call call = client.newCall(request);
// Returns immediately (no value returned).
call.enqueue(new Callback() {
    @Override public void onFailure(Call call, IOException e) {
        // Process error
        // ...
    }

    @Override public void onResponse(Call call, Response response) throws IOException {
        // Process response
        // ...
    }
});

Para modelar la respuesta, la librería OkHttp proporciona la clase Response, que posee los métodos necesarios para extraer la información obtenida:

  • code(): Retorna el status code HTTP contenido en la respuesta.
  • message(): Retorna el mensaje de estado HTTP contenido en la respuesta.
  • protocol(): Retorna la versión del protocolo HTTP usada.
  • request(): Retorna el objeto Request que dio lugar a esta respuesta.
  • isSuccessful(): Retorna si la respuesta ha sido satisfactoria, es decir, si el status code está en el rango [200-300).
  • headers(): Retorna un objeto Headers correspondiente a las cabeceras de la respuesta. Implementa la interfaz Iterable<Pair<String, String>>, por lo que podemos recorrer los pares clave-valor correspondientes a las cabeceras.
  • body(): Retorna el cuerpo de la respuesta en forma de objeto ResponseBody, que posee el método string() para obtener el cuerpo de la respuesta en forma de cadena de caracteres, o el método bytes() para obtener los bytes.

Veamos un ejemplo completo de petición síncrona:

Ejemplo completo de petición síncrona

Request request = new Request.Builder()
    .url(new URL("https://jsonplaceholder.typicode.com/posts"))
    .get()
    .build();
Call call = okHttpClient.newCall(request);
try (Response response = call.execute()) {
    System.out.print(response.code());
    System.out.println(" " + response.message());
    response.headers().forEach(
        header -> System.out.println(header.component1() + ": " + header.component2()));
    if (response.isSuccessful()) {
        try (ResponseBody responseBody = response.body()) {
            System.out.println("\n" + responseBody.string());
        }
    }
} catch (IOException e) {
    System.out.println(e.toString());
}

Veamos ahora un ejemplo completo de petición asíncrona:

Ejemplo completo de petición asíncrona

Request request = new Request.Builder()
    .url(url)
    .get()
    .build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(@NotNull Call call, @NotNull IOException e) {
        System.out.println(e.toString());
    }

    @Override
    public void onResponse(@NotNull Call call, @NotNull Response response) {
        if (response.isSuccessful()) {
            try (ResponseBody responseBody = response.body()) {
                System.out.println("\n" + responseBody.string());
            }
        }    
    }
});

OkHttp no trabaja directamente con CompletableFuture, pero podemos convertir el Callback en un CompletableFuture de una manera muy sencilla:

private CompletableFuture<Response> executeAsync(Call call) {
    CompletableFuture<Response> cf = new CompletableFuture<>();
    call.enqueue(new Callback() {
        @Override
        public void onFailure(@NotNull Call call, @NotNull IOException e) {
            cf.completeExceptionally(e);
        }

        @Override
        public void onResponse(@NotNull Call call, @NotNull Response response) {
            cf.complete(response);
        }
    });
    return cf;
}

Internamente, nuestro cliente OkHttp usa para ejecutar las peticiones asíncronas un ThreadPoolExecutor cacheado, sin número mínimo de hilos, ni máximo, y donde los hilos se mantienen inactivos durante 60 segundos antes de ser eliminados. Aunque no es necesario, si queremos explícitamente finalizar dicho ejecutor deberemos hacer:

Finalizar el ejecutor usado por OkHttp

// Cierra y elimina todas las conexiones inactivas en el pool.
okHttpClient.connectionPool().evictAll();
// Termina el ejecutor.
okHttpClient.dispatcher().executorService().shutdownNow();

Proyecto OkHttp

En este proyecto vamos a realizar una aplicación que realiza distintos tipos de peticiones HTTP a un servicio REST de ejemplo, usando la librería OkHttp. 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 OkHttpClient a través del que realizar las peticiones.

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

import okhttp3.*;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

@SuppressWarnings("SameParameterValue")
public class Main {

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

    // 1. Create Http client.
    private final OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
            // 1.1. Request timeouts.
            .connectTimeout(5000, TimeUnit.MILLISECONDS)
            .readTimeout(5000, TimeUnit.MILLISECONDS)
            .build();
    private final ApiService apiService = new ApiService(okHttpClient);
    private final Scanner scanner = new Scanner(System.in);

    Main() {
        int selectedOption = showMenu();
        processOption(selectedOption);
        okHttpClient.connectionPool().evictAll();
        okHttpClient.dispatcher().executorService().shutdownNow();
    }

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

    private static void showResponse(Response response) {
        // 4.1. Response code.
        System.out.print(response.code());
        // 4.2. Response message.
        System.out.println(" " + response.message());
        // 4.2. Response headers.
        response.headers().forEach(header -> System.out.println(header.component1() + ": " + header.component2()));
        if (response.isSuccessful()) {
            // 4.3 Response body
            try {
                ResponseBody responseBody = response.body();
                if (responseBody != null) {
                    System.out.println("\n" + responseBody.string());
                }
            } catch (IOException ignored) {
            }
        }
    }

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

    private void showPostsSync() {
        try {
            Response response = apiService.getPostsSync();
            showResponse(response);
        } catch (IOException e) {
            showError(e);
        }
    }

    private void showPostsWithCallback() {
        apiService.getPostsWithCallback(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                showError(e);
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) {
                showResponse(response);
            }
        });
    }

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

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

    private void showPostsHeaders() {
        apiService.getPostsHeaders().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(Response response, Throwable throwable) {
        if (throwable != null) {
            showError(throwable);
        } else {
            showResponse(response);
        }
    }

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

}
import okhttp3.*;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;

public class ApiService {

    private final String BASE_URL = "https://jsonplaceholder.typicode.com/";
    private final OkHttpClient okHttpClient;

    public ApiService(OkHttpClient okHttpClient) {
        this.okHttpClient = okHttpClient;
    }

    public Response getPostsSync() throws IOException {
        // 2. Create http request.
        Request request = new Request.Builder()
                // 2.1. URL
                .url(BASE_URL + "posts")
                // 2.2. Request method.
                .get()
                .build();
        // 3. Create a new Call with that request.
        Call call = okHttpClient.newCall(request);
        // 4. Execute call and obtain a response (blocking)
        return call.execute();
    }

    public void getPostsWithCallback(Callback callback) {
        Request request = new Request.Builder()
                .url(BASE_URL + "posts")
                .get()
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(callback);
    }

    public CompletableFuture<Response> getPosts() {
        Request request = new Request.Builder()
                .url(BASE_URL + "posts")
                .get()
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> getPostsAccessOptions() {
        Request request = new Request.Builder()
                .url(BASE_URL + "posts")
                .method("OPTIONS", null)
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> getPostsHeaders() {
        Request request = new Request.Builder()
                .url(BASE_URL + "posts")
                .head()
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> getPost(int postId) {
        Request request = new Request.Builder()
                .url(BASE_URL + "posts/" + postId)
                .get()
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> createPost(String jsonPost) {
        RequestBody requestBody = RequestBody.create(jsonPost, MediaType.get("application/json; charset=utf-8"));
        Request request = new Request.Builder()
                .url(BASE_URL + "posts")
                .post(requestBody)
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> updatePost(int postId, String jsonPost) {
        RequestBody requestBody = RequestBody.create(jsonPost, MediaType.get("application/json; charset=utf-8"));
        Request request = new Request.Builder()
                .url(BASE_URL + "posts/" + postId)
                .put(requestBody)
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> patchPost(int postId, String jsonPost) {
        RequestBody requestBody = RequestBody.create(jsonPost, MediaType.get("application/json; charset=utf-8"));
        Request request = new Request.Builder()
                .url(BASE_URL + "posts/" + postId)
                .post(requestBody)
                .addHeader("X-HTTP-Method-Override", "PATCH")
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> deletePost(int postId) {
        Request request = new Request.Builder()
                .url(BASE_URL + "posts/" + postId)
                .delete()
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    public CompletableFuture<Response> getUserPosts(int userId) {
        Request request = new Request.Builder()
                .url(BASE_URL + "users/" + userId + "/posts")
                .get()
                .build();
        Call call = okHttpClient.newCall(request);
        return executeAsync(call);
    }

    // ---------------------------------------------------------

    private CompletableFuture<Response> executeAsync(Call call) {
        CompletableFuture<Response> cf = new CompletableFuture<>();
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                cf.completeExceptionally(e);
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) {
                cf.complete(response);
            }
        });
        return cf;
    }

}