Implementar un cliente REST con JAX-RS (Jersey)

Hace tiempo escribí un post explicando como implementar un cliente REST con el cliente de Spring (RestTemplate), así que ahora haremos lo mismo pero con un cliente JAX-RS (Jersey).

Concepto:

La implementación Jersey de JAX-RS nos provee también de un cliente REST para poder acceder a servicios REST facilmente, de una manera muy similar a la del RestTemplate de Spring.

El cliente de Jersey es también configurable, y le podemos «registrar» módulos, por ejemplo para logging, para parsear los jsons (jackson), etc.

En el ejemplo, al igual que en el post de RestTemplate, veremos como llamar al servicio con los métodos básicos (GET, POST, PUT, DELETE), directamente desde una aplicación java desde su método ‘main’. Lo haremos, tanto atacando contra un servicio definido en apiary, como contra un servicio arrancado en local. Vamos a ello.

Entorno usado:
Java JDK 1.8
Maven 3.2.5
Git 2.5.1
IDE Intellij 14.1.4 Ultimate version

Ejemplo con apiary:

1. Usaré otra vez el servicio definido en mi cuenta de apiary, con un recurso ‘book’.
La url del servicio: http://docs.booksapi.apiary.io/

2. Creamos un proyecto maven básico, y añadimos las siguientes dependencias:

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-client</artifactId>
            <version>2.21</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.jaxrs</groupId>
            <artifactId>jackson-jaxrs-json-provider</artifactId>
            <version>2.6.1</version>
        </dependency>
    </dependencies>

La librería jersey-client es la única que necesitamos de Jersey. También necesitamos un ‘provider’ de Jackson, para el parseo de los jsons. Lo veremos al instanciar el cliente.

3. Creamos una típica clase con método main, con las rutas a nuestro servicio:

package com.edwise.pocs.jerseyrestclient;

public class AppBookRestApiary {

    private static final String URL_BASE_API =
            "http://private-114e-booksapi.apiary-mock.com/";
    private static final String BOOKS_RESOURCE = "books/";

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

4. Creamos una entity, exactamente igual que la usada en el post con RestTemplate:

package com.edwise.pocs.jerseyrestclient.model;

public class Book {
    private Long id;
    private String title;

    // getter and setters...

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}

Entity muy simple, que cumple la ‘interfaz’ del servicio.

5. Instanciamos nuestro cliente con Jersey:

    public static void main(String[] args) {
        Client client = ClientBuilder.newClient(
                new ClientConfig().register(JacksonJsonProvider.class));
        WebTarget webTarget = client.target(URL_BASE_API).path(BOOKS_RESOURCE);
      
    }

Para instanciar un cliente, usamos un método estático de la clase ClientBuilder. Como parámetro hay que pasarle un ClientConfig, que en este caso le registramos el provider de Jackson, para el parseo de los jsons.
Por último, le pasamos la url base de nuestro servicio, así como el path concreto. Esto nos devuelve un objeto WebTarget que será el que usaremos para ‘hablar’ con el servicio.

6. Implementamos ahora la llamada a cada uno de los métodos que acepta el servicio:

– GET all: para obtener todos los ‘books’:

        Response response = webTarget
                .request(MediaType.APPLICATION_JSON_TYPE)
                .get();

        System.out.println();
        System.out.println("GET All StatusCode = " + response.getStatus());
        System.out.println("GET All Headers = " + response.getHeaders());
        System.out.println("GET Body (object list): ");
        Arrays.asList(response.readEntity(Book[].class))
                .forEach(book -> System.out.println("--> " + book));

Con el método request de la interfaz WebTarget haremos todas las peticiones. Le pasamos como parámetro el tipo a recibir, y llamamos al metodo get.
Esto nos devuelve un objeto Response en el que podemos acceder como siempre al código html devuelto, los ‘headers’, etc.
Para el parseo del json a objeto, usamos el método readEntity, pasandole el tipo como parámetro (un array en este caso).

– GET: para obtener un ‘book’ en concreto, por su id:

        Response response = webTarget.path("12")
                .request(MediaType.APPLICATION_JSON_TYPE)
                .get();

        System.out.println();
        System.out.println("GET StatusCode = " + response.getStatus());
        System.out.println("GET Headers = " + response.getHeaders());
        System.out.println("GET Body (object): "
                + response.readEntity(Book.class));

Es similar al anterior, pero le añadimos al path el id (quedaría como /books/12).
Esta vez en el readEntity el tipo a recibir es un único objeto, no un array.

– POST: para insertar un ‘book’ nuevo:

        Book bookToInsert = createBook(null, "New book title");
        Response response = webTarget
                .request()
                .post(Entity.entity(bookToInsert, MediaType.APPLICATION_JSON_TYPE));

        System.out.println();
        System.out.println("POST executed");
        System.out.println("POST StatusCode = " + response.getStatus());
        System.out.println("POST Header location = "
                + response.getHeaders().get("location"));

En este caso el método request no necesita tipo a recibir, dado que no devuelve ‘body’ el servicio para el POST. Con el método post le pasamos el objeto a insertar, gracias a la clase Entity y su método estático entity en el que además le pasamos el MediaType al que parsear el objeto.
Para obtener un header en concreto, usamos su método get, en este caso obtenemos el ‘location’.

– PUT: para actualizar un ‘book’ en concreto, por su id:

        Book bookToUpdate = createBook(123L, "Book title updated");
        Response response = webTarget.path("123")
                .request()
                .put(Entity.entity(bookToUpdate, MediaType.APPLICATION_JSON_TYPE));

        System.out.println();
        System.out.println("PUT StatusCode = " + response.getStatus());
        System.out.println("PUT Headers = " + response.getHeaders());

Muy similar al POST, excepto que le añadimos al ‘path’ el id del recurso a actualizar. Usamos el método put.

– DELETE: para borrar un ‘book’ en concreto, por su id:

        Response response = webTarget.path("12")
                .request()
                .delete();

        System.out.println();
        System.out.println("DELETE StatusCode = " + response.getStatus());
        System.out.println("DELETE Headers = " + response.getHeaders());

No necesita mucha explicación: añadimos al path el id del recurso a borrar, y llamamos al método delete.

Ejemplo contra un servicio en local:

1. Si no lo tenemos todavía, descargamos mi proyecto integrationtests-rest-example de mi github, y lo arrancamos con ‘mvn spring-boot:run’.

2. Creamos una clase para la entity que nos devuelve el servicio:

package com.edwise.pocs.jerseyrestclient.model;

import java.time.LocalDateTime;

public class Info {

    private Long id;
    private String infoText;
    private LocalDateTime creationDateTime;

    // getters and setters...

    @Override
    public String toString() {
        return "Info{" +
                "id=" + id +
                ", infoText='" + infoText + '\'' +
                ", creationDateTime=" + creationDateTime +
                '}';
    }
}

3. Como en la entity tenemos un campo de tipo LocalDateTime (de la nueva API Date de Java 8, especificación JSR310), necesitamos una de los submódulos de jackson para parseo. Añadimos la siguiente dependencia en nuestro pom.xml:

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.6.1</version>
        </dependency>

4. Creamos otra clase con método main en la que implementaremos las llamadas a los métodos del servicio (igual que antes). En el método, instanciamos el cliente REST de la siguiente manera:

package com.edwise.pocs.jerseyrestclient;

// imports...

public class AppInfoRestLocal {

    private static final String URL_BASE_API = "http://localhost:8080/api/";
    private static final String INFO_RESOURCE = "info/";

    public static void main(String[] args) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        Client client = ClientBuilder.newClient(
                new ClientConfig().register(new JacksonJsonProvider(mapper)));
        WebTarget webTarget = client.target(URL_BASE_API).path(INFO_RESOURCE);

    }

Para crear el cliente en este caso, necesitamos ‘personalizar’ el provider de Jackson para que parsee correctamente los campos de tipo ‘Java 8 API Date’. Para ello instanciamos un mapper de Jackson, le registramos el módulo de para la jsr310 (JavaTimeModule ahora, JSR310Module ha sido deprecado hace poco), y usamos ese mapper para crear nuestro provider de Jackson para jsons.

5. El resto es casi identico al caso anterior, podéis ver el código en mi github: AppInfoRestLocal

Todo el código de los dos ejemplos está subido en mi github, proyecto jersey-rest-client-example. Se puede ejecutar cada ejemplo ejecutando su clase ‘main’, la cual ejecuta todos los métodos.