Crear de forma sencilla un cliente de un servicio REST o HTTP con Retrofit

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

La implementación de un servicio REST o HTTP es solo una parte, el otro lado es crear un cliente de ese u otro servicio que permita invocarlo, proporcionar parámetros y obtener las respuestas. Con la librería Retrofit implementar un cliente de un servicio en Java es una tarea bastante sencilla sencilla que utiliza una simple interfaz a la que se le añaden varias anotaciones que le indican a Retrofit cómo construir una implementación a partir de la interfaz. El código que hace uso de la clase que implementa la interfaz del servicio con Retrofit no es diferente de usar una clase que implementa una interfaz.

Java

En el lenguaje de programación Java hay varias librerías que permiten hacer peticiones a un servicio que utilice el protocolo HTTP. Varias de las librerías más conocidas son OkHttp, Apache HttpComponents e incluso en el JDK en la versión 11 se ha incorporado un cliente HTTP que soporta HTTP/2 y Spring ofrece 3 clientes REST.

Estas librerías cumplen su función y ofrecen total flexibilidad en su uso, sin embargo, requieren hacer las peticiones HTTP de forma explícita lo que supone un código repetitivo y tedioso incluyendo hacer las conversiones de objetos a JSON y de JSON a objetos Java en las peticiones y respuestas.

La librería Retrofit para crear un cliente de un servicio REST o HTTP

Retrofit es una librería que simplifica en gran medida el construir clientes HTTP de una API REST o realizar un cliente de un servicio REST. Con Retrofit basta con crear una interfaz Java que represente el servicio y decorarla con las anotaciones que proporciona Retrofit. También es posible utilizar Retrofit para servicios implementados con GraphQL que aunque no están basados en REST si utilizan el protocolo HTTP.

Retrofit utiliza como librería para realizar las peticiones OkHttp y es compatible con varias librerías para realizar las conversiones de datos de JSON a objetos y de objetos a JSON, entre ellas Jackson, Gson y JSON-B. También soporta realizar las peticiones de forma síncrona o asíncrona.

Esta es una interfaz de Java que representa un sencillo servicio REST de una petición GET. Las anotaciones instruyen a Retrofit como a partir de esta interfaz crear el cliente del servicio REST, al proporcionar esta interfaz Retrofit devuelve una instancia de la interfaz que al invocar a los métodos internamente realiza las peticiones HTTP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package io.github.picodotdev.blogbitix.javaretrofit;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Path;
import retrofit2.http.Query;

public interface Service {

    @GET("/message/{name}")
    Call<String> message(@Header("Accept-Language") String acceptLanguage, @Path("name") String name, @Query("random") String random);
}
Service.java

Al proporcionar a Retrofit la interfaz este crea una instancia que implementa la interfaz pero que internamente implementa el cliente HTTP del servicio. Aparte de la interfaz para obtener la instancia del servicio se ha de proporcionar la URL base donde se ubica el servicio asi como otros objetos relacionados como interceptores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main implements CommandLineRunner {

    @Autowired
    private MeterRegistry registry;

    ...

    private Service buildService() {
        OkHttpClient client = new OkHttpClient.Builder()
                .addNetworkInterceptor(buildLoggingInterceptor())
                .eventListener(OkHttpMetricsEventListener.builder(registry, "okhttp.requests").build())
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .addConverterFactory(ScalarsConverterFactory.create())
                .baseUrl("http://localhost:8080/").build();

        return retrofit.create(Service.class);
    }

    ...
}
Main-buildService.java

Anotaciones de Retrofit

Las anotaciones de Retrofit en la interfaz de Java describen el servicio como variables en el path de la URL, parámetros, para realizar conversiones a JSON o el método HTTP a invocar o cabeceras HTTP.

  • HTTP, GET, POST, PUT, PATCH, DELETE, OPTIONS y HEAD: estas anotaciones indican el método HTTP que se realiza.
  • Path, Query: la anotación Path sustituye una variable en el path de la URL por el valor del argumento anotado. La anotación Query añade un argumento en la query de la URL.
  • Headers, Header: la anotación Headers permite especificar una colección de cabeceras HTTP a incluir en la petición. La anotación Header añade una cabecera a partir del valor de un argumento en la firma del método.
  • Body: la anotación Body transforma el argumento como los datos a incluir como JSON en cuerpo de la petición utilizando la librería que implementa la conversión de objetos a JSON.

Aplicar funcionalidades transversales con interceptores

Algunas funcionalidades comunes al crear un cliente de un servicio REST son obtener trazas de las peticiones que se están realizando, realizar autenticación, generar métricas o trazabilidad con Sleuth. Estas son funcionalidades transversales a todos los métodos de la interfaz del servicio REST que se implementan usando interceptores.

Un interceptor es una clase que implementa una interfaz de la librería OkHttp que es invocada al realizarse una petición HTTP. OkHttp soporta métricas añadiendo un EventListener al construir la instancia del cliente de OkHttp.

OkHttp proporciona una implementación de interceptor que emite trazas cuando se realiza una petición útil para observar en el log qué peticiones se están realizando y que códigos de estado se están devolviendo. Esta es una implementación propia de un interceptor para emitir las trazas que se realizan con el cliente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main implements CommandLineRunner {

    ...

    private Interceptor buildLoggingInterceptor() {
        return chain -> {
            Request request = chain.request();

            long t1 = System.nanoTime();
            System.out.println(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers()));

            Response response = chain.proceed(request);

            long t2 = System.nanoTime();
            System.out.println(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers()));

            return response;
        };
    }

    ...
}
Main-interceptor.java

Servicio de ejemplo con Retrofit

Un controlador como el siguiente de un servicio REST definido con Spring Framework sencillo que únicamente devuelve un mensaje en función de los parámetros recibidos a través de una en la petición, una variable en el path de la petición y un parámetro en la query.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package io.github.picodotdev.blogbitix.javaretrofit;

...

@RestController
public class RestService {

    private static final Map<String, String> MESSAGES;

    static {
        MESSAGES = new HashMap<>();
        MESSAGES.put("es-ES;default", "¡Hola mundo!");
        MESSAGES.put("es-ES;hello", "¡Hola %s!");
        MESSAGES.put("en-GB;default", "Hello World!");
        MESSAGES.put("en-GB;hello", "Hello %s");
    }

    @GetMapping(path = { "/message/", "/message/{name}" })
    public String message(@RequestHeader(value = "Accept-Language", defaultValue = "en-GB") String locale, @PathVariable(required = false) String name, @RequestParam(name = "random", required = false) String random) {
        System.out.printf("Random: %s%n", random);
        String message = "";
        if (name == null || name.isBlank()) {
            String key = String.format("%s;default", locale);
            message = MESSAGES.getOrDefault(key, MESSAGES.get("en-GB;default"));
        } else {
            String key = String.format("%s;hello", locale);
            String value = MESSAGES.getOrDefault(key, MESSAGES.get("en-GB;default"));
            message = String.format(value, name);
        }
        return message;
    }
}
RestService.java

El cliente del servicio construido por Retrofit se realiza a partir de la definición de la interfaz, el cliente es un objeto que implementa esa interfaz y en código Java no de su uso es simplemente invocar sus métodos y proporcionar los parámetros. La implementación del cliente contiene el código necesario para transformar las invocaciones de los métodos de la interfaz en peticiones al servicio REST.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package io.github.picodotdev.blogbitix.javaretrofit;

...

@SpringBootApplication
public class Main implements CommandLineRunner {

    ...

    @Override
    public void run(String... args) throws Exception {
        Service service = buildService();

        String r1 = service.message("es-ES", "", UUID.randomUUID().toString()).execute().body();
        String r2 = service.message("es-ES", "Java", UUID.randomUUID().toString()).execute().body();
        String r3 = service.message("en-GB", "", UUID.randomUUID().toString()).execute().body();
        String r4 = service.message("en-GB", "Java", UUID.randomUUID().toString()).execute().body();

        System.out.printf("Result: %s%n", r1);
        System.out.printf("Result: %s%n", r2);
        System.out.printf("Result: %s%n", r3);
        System.out.printf("Result: %s%n", r4);
    }

    ...
}
Main.java

Esta es la salida del programa en la consola donde se ven las trazas del interceptor de OkHttp con los datos de la petición y las respuesta del servicio junto con el mensaje de respuesta del servicio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Sending request http://localhost:8080/message/?random=520983c7-79d9-4956-aceb-b0dad5c8902c on Connection{localhost:8080, proxy=DIRECT hostAddress=localhost/127.0.0.1:8080 cipherSuite=none protocol=http/1.1}
Accept-Language: es-ES
Host: localhost:8080
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9

Random: 520983c7-79d9-4956-aceb-b0dad5c8902c
Received response for http://localhost:8080/message/?random=520983c7-79d9-4956-aceb-b0dad5c8902c in 94,6ms
Content-Type: text/plain;charset=UTF-8
Content-Length: 13
Date: Fri, 23 Apr 2021 17:46:41 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Sending request http://localhost:8080/message/Java?random=d4cca45d-4a45-40ca-9872-fc2c344f1572 on Connection{localhost:8080, proxy=DIRECT hostAddress=localhost/127.0.0.1:8080 cipherSuite=none protocol=http/1.1}
Accept-Language: es-ES
Host: localhost:8080
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9

Random: d4cca45d-4a45-40ca-9872-fc2c344f1572
Received response for http://localhost:8080/message/Java?random=d4cca45d-4a45-40ca-9872-fc2c344f1572 in 3,7ms
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Date: Fri, 23 Apr 2021 17:46:41 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Sending request http://localhost:8080/message/?random=e802c2e7-9f5c-4908-8fe6-22b3c8069192 on Connection{localhost:8080, proxy=DIRECT hostAddress=localhost/127.0.0.1:8080 cipherSuite=none protocol=http/1.1}
Accept-Language: en-GB
Host: localhost:8080
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9

Random: e802c2e7-9f5c-4908-8fe6-22b3c8069192
Received response for http://localhost:8080/message/?random=e802c2e7-9f5c-4908-8fe6-22b3c8069192 in 2,2ms
Content-Type: text/plain;charset=UTF-8
Content-Length: 12
Date: Fri, 23 Apr 2021 17:46:41 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Sending request http://localhost:8080/message/Java?random=16fc8a5f-b9ab-4b26-8049-81a4e7901820 on Connection{localhost:8080, proxy=DIRECT hostAddress=localhost/127.0.0.1:8080 cipherSuite=none protocol=http/1.1}
Accept-Language: en-GB
Host: localhost:8080
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.14.9

Random: 16fc8a5f-b9ab-4b26-8049-81a4e7901820
Received response for http://localhost:8080/message/Java?random=16fc8a5f-b9ab-4b26-8049-81a4e7901820 in 2,7ms
Content-Type: text/plain;charset=UTF-8
Content-Length: 10
Date: Fri, 23 Apr 2021 17:46:41 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Result: ¡Hola mundo!
Result: ¡Hola Java!
Result: Hello World!
Result: Hello Java!
System.out

En el archivo de construcción hay que incluir la dependencia de Retrofit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
plugins {
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform('org.springframework.boot:spring-boot-dependencies:2.4.5'))

    implementation 'org.springframework.boot:spring-boot'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'

    implementation 'io.micrometer:micrometer-core:1.6.6'
}


application {
    mainClass = 'io.github.picodotdev.blogbitix.javaretrofit.Main'
}
build.gradle

El código que utiliza el cliente realiza peticiones HTTP de modo que al hacer pruebas unitarias es necesario utilizar un servidor mock para devolver las respuestas simuladas del servicio real a las peticiones HTTP sin necesidad de que este esté disponible, esto elimina dependencias del entorno de pruebas haciéndolo más sencillo, también el servidor mock permite el desarrollo sin necesidad del servicio real.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: