Mostrando entradas con la etiqueta REST. Mostrar todas las entradas
Mostrando entradas con la etiqueta REST. Mostrar todas las entradas

domingo, 7 de enero de 2024

Wiremock y Quarkus: Como configurarlo

Wiremock es una gran herramienta que puede ayudarte en la realización de pruebas de calidad de tu código. Un paso fundamental a la hora de desarrollar pruebas de aplicaciones o servicios que invocan a otros. Las pruebas son un paso fundamental en el desarrollo de software. Y Quarkus es un framework Java diseñado especialmente para el desarrollo de aplicaciones en contenedores, sin servidor y en la nube, especialmente para Kubernetes. De ambos hemos hablado varias veces ya.

Hoy veremos cómo podemos usarlos juntos, puesto que su uso no es tan sencillo, como pudiera ser con otra aplicación realizada con otro framework Java. Y para lograrlo haremos uso de la anotación @QuarkusTestResource. En una clase de testing debemos añadir dicha anotación asociada a una clase que implemente a su vez la interfaz QuarkusTestResourceLifecycleManager. Esto nos permitirá inicializar un servicio externo antes de la ejecución de las propias pruebas. En nuestro caso, nos permitirá arrancar el servidor Wiremock.

public class WiremockTestResource implements QuarkusTestResourceLifecycleManager {
  public static WireMockServer wireMockServer;
  @Override
  public Map<String, String> start() {
    wireMockServer = new WireMockServer(57001);
    wireMockServer.start();
    return new HashMap<String, String>();
  }
  @Override
  public void stop() {
    if (wireMockServer != null) {
      wireMockServer.stop();
    }
  }
}

Una vez creada la clase que permite arrancar Wiremock, haremos uso de ella a través de la anotación @QuarkusTestResource. De esta forma ya solo nos quedará indicar los stubs asociados a nuestros tests.

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class) //Resource that call external service
@QuarkusTestResource(WiremockTestResource.class)
public class WiremockResourceBasicTest {
  @Test
  public void getById() {
    Log.info("WiremockResourceBasicTest - getById");
    ObjectMapper mapper = new ObjectMapper();
    Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
    Log.info("WM port: " + WiremockTestResource.wireMockServer.getOptions().portNumber());
    WiremockTestResource.wireMockServer.stubFor(
      WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse()
      .withJsonBody(mapper.valueToTree(movie))
      .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));
    when().get("/1").andReturn().then().statusCode(200)
      .body("id", is(1)).body("name", is("Dune")).body("director",
      is("Denis Villeneuve"));
  }
}

Como veis es muy sencillo. Ahora veremos una opción un poco más complejas pero que permitirá crear el servidor de Wiremock con más opciones de configuración y modificarlas dinámicamente. Aunque el desarrollo es un poco lioso, debido a que tiene referencias cruzadas. 

Por un lado crearemos una anotación que tenga las propiedades que queramos modificar dinámicamente posteriormente a través de la propia anotación. Pero también le añadiremos la anotación @QuarkusTestResource que hará referencia a la clase que controla el servidor de Wiremock. 

@QuarkusTestResource(value = WiremockResourceConfigurable.class, restrictToAnnotatedClass = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WiremockTestAnnotation {
    String port() default "57005";
}

Ahora mostraremos la clase que configura el servidor de Wiremock. Será similar al ejemplo anterior, pero con tres añadidos. Para empezar, la interfaz que vamos a implementar será QuarkusTestResourceConfigurableLifecycleManager, y está estará asociada a la anotación creada anteriormente. Luego, a través de los atributos de la anotación, en el método init, podremos configurar dinámicamente el servidor. Y por último, con el retorno del método start, también podremos sobre escribir valores del fichero application.properties con los valores dinámicos. En este caso, a través de esta sobre escritura, podremos modificar el puerto del servicio externo pre configurado para el cliente.

public class WiremockResourceConfigurable 
  implements QuarkusTestResourceConfigurableLifecycleManager<WiremockTestAnnotation> {	
    public static WireMockServer server;
    private String port;
    @Override
    public void init(WiremockTestAnnotation params) {
        port = params.port();
    }
    @Override
    public Map<String, String> start() {
	server = new WireMockServer(Integer.valueOf(port));
        server.start();
	return  Map.of(
          "quarkus.rest-client.\"com.home.example.service.ExternalService\".url", 
          "http://localhost:"+port
          );
    }
    //...
}

Ya solo nos quedaría mostrar el test, haciendo uso de la anotación que nos permitiría modificar el puerto del servidor wiremock y del servicio externo dinámicamente. Muy similar al anterior. 

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
@WiremockTestAnnotation(port = "57005")
public class WiremockResourceTest {
    @Inject
    @ConfigProperty(name = "quarkus.rest-client.\"com.home.example.service.ExternalService\".url")
    private String serverUrl;
    @Test
    public void getById() {
        Log.info("serverUrl: " + serverUrl);
        ObjectMapper mapper = new ObjectMapper();
        Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
        WiremockResourceConfigurable.server.stubFor(
            WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse().withJsonBody(mapper.valueToTree(movie))
	    .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));	
        when().get("/1").andReturn().then().statusCode(200)
            .body("id", is(1)).body("name", is("Dune")).body("director",is("Denis Villeneuve"));
    }
}

Con esto hemos visto una particularidad más para mejorar los test en Quarkus. Y aunque Wiremock es una gran herramienta que desde hace tiempo es muy util para el desarrollo de pruebas. No quisiera omitir, que también es posible mockear el servicio externo de una forma sencilla con Quarkus.  Y esto es posible haciendo uso de las anotaciones @InjectMock y @RestClient. En el ejemplo que mostramos a continuación, ExternalService ha sido todo el tiempo el cliente web que hemos mockeado con Wiremock.  

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
public class WiremockResourceInjectTest {
    @InjectMock
    @RestClient
    ExternalService extService;
    @Test
    public void getById() {
        Mockito.when(extService.getMovieById(Mockito.anyLong())).thenReturn(new Movie(1L, "Denis Villeneuve", "Dune"));
        when().get("/1").andReturn().then().statusCode(200).body("id", is(1)).body("name", is("Dune")).body("director",
            is("Denis Villeneuve"));
    }
}

Espero que como siempre haya sido util, y si quieres puedes ver el código aquí. 

domingo, 14 de mayo de 2023

Quarkus & Microprofile: Como hacer un Microservicio con persistencia

 Hoy vamos a ver una nueva forma de hacer un microservicio. Esta vez utilizando Quarkus y Microprofile. Además le añadiremos una pequeña capa de persistencia. 

Quarkus ya lo hemos visto anteriormente integrado con Apache Camel, aquí, pero hoy lo veremos de forma individual. Quarkus es un framework Java pero Kubernetes-native, es decir optimizado para su uso en Graalvm y su despliegue a través de contenedores. Y como es el primero que hacemos de Quarkus en solitario, empezaremos indicando como podemos crear un proyecto, para ello usaremos el comando:

mvn com.redhat.quarkus.platform:quarkus-maven-plugin:2.13.5.Final-redhat-00002:create \
    -DprojectGroupId=com.home.example -DprojectArtifactId=ms-with-panache

Y las librerías principales a utilizar serán:

  • REST: quarkus-resteasy-reactive
  • Hibernate & Panache: quarkus-hibernate-orm-panache
  • JSON: quarkus-resteasy-reactive-jackson
Si tenéis experiencia en microservicios con Spring o JAX-RS, la parte de la generación del microservicio será sencilla. Pero aún así daremos una pequeña explicación de cómo poder generar cualquier método.
  • @Path("/book"): Nos permite indicar cual es la raiz del microservicio, en que contexto atiende a llamadas. También se usa en cada uno de los recursos, si tiene un contexto posterior diferente.
  • @GET, @DELETE... : Indica el verbo HTTP con el cual realizaremos la invocación.
  • @QueryParam : Nos permite indicar parámetros que viajan en la URL
  • @PathParam: Nos permite indicar parámetros que forman parte del contexto. Han debido indicarse en la anotación @Path. 
  •  @Produces y @Consumes: Nos permite indicar el formato de la salida y de lo que espera de entrada. 
Para la parte de la persistencia utilizaremos Panache, que pondrá a nuestra disposición dos patrones distintos para llevarla a cabo. Por un lado tendremos el patrón Repository, donde crearemos una clase intermedia que será la encargada de realizar las operaciones contra la base de datos similar a Spring Data.

@ApplicationScoped
@Transactional
public class BookRepository implements PanacheRepository<Book>{ }

Y por otro lado contaremos con el patrón Active Record, donde será la propia entidad la que pondrá a disposición del programador, los distintos métodos de persistencia. 

@Entity
@Table(name = "BOARDGAME")
public class BoardGame extends PanacheEntity {
    //warning with PanacheEntity we dont need getter/setter or Id 
    public String designer;
    public String name;    
}

A continuación un ejemplo de todo junto. En el repositorio Git podéis ver todo el código. 

@Path("/boardgame")
public class BoardGameResource {
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public BoardGame getBoardGameById(@PathParam("id") final Long id) {
        return BoardGame.findById(id);
    }
    //warning: if we don put @Transactional generates an error
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @Transactional
    public BoardGame persist(final BoardGame newBoardGame) {
        BoardGame.persist(newBoardGame);
        return newBoardGame;
    }
}

Para poder configurar las distintas características, lo podremos hacer a través del fichero application.properties podremos. Aquí indicamos algunas:
  • quarkus.http.port: Podemos indicar en que puerto admite las llamadas
  • quarkus.datasource.username/password/jdbc.url: Podemos indicar la conexión a la BBDD. 
  • quarkus.hibernate-orm: Tiene múltiples opciones para indicar si queremos o no que se genere el esquema de la BBDD al iniciar la aplicación y los ficheros para ello. 
Una vez que hemos desarrollado y configurado el proyecto, vamos a proceder a arrancarlo. Como estamos en desarrollo, utilizaremos el comando

mvn quarkus:dev

Otro punto a favor del uso de Quarkus, es que podemos recargar fácilmente la aplicación y comprobar los cambios, simplemente pulsando la tecla 's' en la consola donde lo tengamos ejecutando. 

Espero que os haya sido útil, como siempre. Y si queréis ver todo el código, está aquí. 

sábado, 10 de julio de 2021

RESTEasy: Como crear un Web Service y su cliente

Ya hemos visto varios frameworks que nos pueden ayudar a crear un servicio web y su cliente. Hoy veremos RESTEasy que es un framework de JBoss, el cual ofrece una implementación para las especificaciones JAX-RS 2.0 y 2.1. Y como es normal, aunque esta pensado para una integración perfecta con WildFly, también nos ofrecerá la posibilidad de desplegarlo en cualquier otro contenedor de servlets como Tomcat o Jetty. 

Empecemos con el Web Service, el cual será muy básico, lo necesario como para mostrar como poder montar uno más complejo. Lo primero será indicar cuales son las dependencias mínimas necesarias:

  • resteasy-servlet-initializer
Nos permite generar el Web Service sin necesidad de configuración manual por nuestra parte. Es decir, con las anotaciones que veamos a continuación será suficiente para configurar nuestro servicio. No necesitaremos de configurar nada en el fichero web.xml tal y como hicimos en este otro post. Incluso podriamos no crear un fichero web.xml, pero esto no lo haremos para evitar problemas con Maven. 

Esta a su vez tiene como dependencia a las librerías principales del framework: resteasy-core y las librerías con las implementaciones de la especificaciones.  

  • resteasy-jackson2-provider
Es la librería que nos permite la transformación de la respuesta en formato de objeto Java a JSON. En el caso de que quisiera responder XML debería incluir la librería resteasy-jaxb-provider. Si no incluimos la librería podemos tener un error del tipo: Could not find MessageBodyWriter for response object of type: com.home.example.resteasy.bean.Book of media type: application/json.

Una vez que hemos indicado las dependencias en el fichero pom.xml y que sabemos que nos vale con un fichero web.xml bien foramdo pero sin contenido. Vamos a configurar el servicio en dos sencillos pasos. 

Debemos crear una clase que extienda de javax.ws.rs.core.Application. Y que además tenga la anotación javax.ws.rs.ApplicationPath con la cual podremos indicar el path principal de nuestro Web Service. Por tanto, para poder acceder a los métodos REST deberemos por un lado indicar el nombre del servicio y por otro el application path indicado en la anotación. Ejemplo:

@ApplicationPath("/library")
public class LibraryApplication extends Application {
}
 
El siguiente paso será crear los métodos REST. Lo podremos hacer a través de distintas anotaciones:
  • @Path: Nos permite indicar un path común para todos los métodos. 
  • @POST, @GET, @DELETE o @PUT: Indica a que método HTTP esta asociado el método. 
  • @Produces, @Consume: Permite indicar que formato tendrá la entrada o salida. JSON o XML. 
  • @PathParam, @QueryParam: Permite indicar como recibiremos parámetros a través de la URL de invocación. 
Además de esta hay otras anotaciones que nos permitirán una mayor configuración de los métodos, indicar las cookies, cabeceras, valores por defecto o incluso si los parámetros vienen codificados. 

Si queremos devolver un objeto en un formato concreto no necesitaremos nada más que devolver dicho objeto. Pero si queremos devolver una respuesta más personalizada, donde indiquemos cookies, código de respuesta, etc, lo haremos a través del objeto javax.ws.rs.core.Response. Ejemplo:

@Path("/book")
public class BookServiceImple {
    private static Map<Integer, Book> library = new HashMap<Integer, Book>();
    static {
	library.put(1, new Book(1, "Ender's Game", "Orson S. Card"));
	library.put(2, new Book(2, "The stars my destination", "Alfred Bester"));
    }
    @POST
    @Path("/")
    @Produces({ MediaType.APPLICATION_JSON })
    @Consumes({ MediaType.APPLICATION_JSON })
    public Response create(final Book book) {
	if (null != library.get(book.getId())) {
	    return Response.status(Response.Status.NOT_MODIFIED).entity("Book is already in the library.").build();
	}
	library.put(book.getId(), book);
	return Response.status(Response.Status.CREATED).build();
    }
    @GET
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON })
    public Book read(@PathParam("id") final Integer id) {
	if (library.containsKey(id)) {
	    return library.get(id);
	} else {
	    return null;
	}
    }
}

Si queremos utilizarlo, deberemos invocar el comando Maven: clean package y desplegar el fichero .war generado en nuestro servidor de contenedores o aplicaciones favorito. Y ya podremos utilizarlo invocando el siguiente comando:

curl --location --request GET 'http://localhost:8080/RestEasyService/library/book/1'

Nuestro siguiente paso será crear el cliente. El cual podremos crearlo de dos formas diferentes, pero siempre haciendo uso de la clase de utilidad ResteasyClientBuilder. La cual cuenta con un método básico de creación del cliente pero que también aporta métodos para la configuración de dicho cliente, además de permitirnos crear un objeto de tipo WebTarget. 

Una vez en este punto, podemos realizar la invocación del Web Service de dos formas diferentes. La primera de ellas, será algo más rudimentaria y necesitará de una mayor configuración para cada una de las llamadas, así como indicar la URL exacta a la hora de crear el objeto WebTarget. Este objeto nos permitirá hacer las llamadas REST correspondiente a través de sus métodos get() o post() e indicando que tipo de objeto vamos a recibir. 

public class BookClient {
    final static String path = "http://localhost:8080/RestEasyService/library/";
    public Book read_target(final Integer id) {
	Book retorno = null;
	WebTarget target = ClientBuilder.newClient().target(path + "book/" + id);
	try (Response response = target.request().get()) {
	    retorno = response.readEntity(Book.class);
	}
	return retorno;
    }
}

Para la segunda manera de crear el cliente utilizaremos el framework de RESTEasy Proxy. El cual nos permitirá realizar invocaciones más sencillas pero como punto negativo tendremos que crear una Interface que cumpla con el contrato del Web Service. El mayor inconveniente de esta interface es que tendremos que crearla manualmente a través de anotaciones JAX-RS, pero lo más positivo es podremos crear clientes de Web Services que no tienen porque haber sido generados a su vez por JAX-RS. Un ejemplo:

@Path("/book")
public interface BookService {
    @POST
    @Path("/")
    @Produces({ MediaType.APPLICATION_JSON })
    @Consumes({ MediaType.APPLICATION_JSON })
    Response create(final Book book);

    @GET
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON })
    Book read(@PathParam("id") final Integer id);
}

Para crear un objeto de tipo proxy, lo podremos obtener a través del objeto WebTarget, indicando la interfaz que tiene el contrato del Web Service. Para este caso no necesitaremos indicar la URL exacta de cada uno de los métodos y las invocaciones y tratamientos de la respuesta, la realizaremos de una forma más natural y comprensible. 

public class BookClient {
    final static String path = "http://localhost:8080/RestEasyService/library/";
    private static BookService proxy;
    static {
	WebTarget target = ClientBuilder.newClient().target(path);
	proxy = ((ResteasyWebTarget) target).proxy(BookService.class);
    }
    public Book read_proxy(final Integer id) {
	return proxy.read(id);
    }
}

Con esto ya sabemos crear un Web Service y su cliente pero vamos a ver un último detalle importante como es la configuración a través de la clase MicroProfile Config.

Esta clase nos permitirá cumplir con la metodología de 12factor, permitiéndonos configurar el cliente o servicio a través de variables de entorno, propiedades del sistema o un fichero de configuración específico. A continuación veremos como podemos modificar la creación del cliente anterior para que obtenga la dirección del servicio a través de una variable de entorno. 

static {
    Config config = ConfigProvider.getConfig();
    String path = config.getValue("BOOKWS_URL", String.class);
    WebTarget target = ClientBuilder.newClient().target(path);
    proxy = ((ResteasyWebTarget) target).proxy(BookService.class);
}

Por prioridad primero intentará obtener el valor de una propiedad del sistema, después de una variable del entorno y por último lo buscará en el fichero del classpath META-INF/microprofile-config.properties. Aunque estas valores de prioridad y orígenes de información también son configurables. Esto nos permitirá mantener el código de la aplicación inmutable en cada entorno en el cual despleguemos nuestra aplicación.

Y hasta aquí las nociones básicas para crear Web Services y clientes con el framework de JBoss. Si quieres puedes ver el código fuente aquí. 

jueves, 4 de febrero de 2021

WSO2 y Redis

Ya estuvimos hablando en un post anterior de WSO2 y el cache mediator. Hoy veremos una solución alternativa pero similar. En este caso utilizaremos Redis, que nos permitirá cachear la respuesta de un backend. 

Redis es un almacén de estructura de datos de valores de clave en memoria rápido y de código abierto. Básicamente una base de datos nosql de tipo clave y valor, muy popular entre los desarrolladores. Redis, permite el uso de diferentes tipos de datos: List, Sets, Hashes, etc. 

Nosotros nos basaremos únicamente en los pares clave-valor, los hashes. Y veremos su uso a través de un ejemplo, como siempre. En este ejemplo llamaremos a un backend y almacenaremos su respuesta asociado al identificador del objeto. Lo cual nos permitirá que al volver a llamar con el mismo identificador, devolver la respuesta anteriormente obtenida. 

Para empezar, usaremos un WSO2 EI 6.5.0. Con la versión standalone no es posible conectarse a Redis, para conseguirlo debemos hacer dos cosas:

  • Añadir la librería Jedis (2.10.2) en la carpeta ${WSO2_HOME}/lib y arrancar el servidor. 
  • Añadir el conector de Redis a través de la interfaz gráfica.  

El conector se puede obtener desde el WSO2 Store, y hay distintas versiones en función del producto que utilicemos. Hay que recordar que una vez después de añadirlo es necesario habilitarlo.


La forma más sencilla de utilizar Redis es como siempre utilizar docker, y para ello usaremos el siguiente comando:

docker run -p 6379:6379 --name redis -d redis

El uso del conector de Redis es similar al de otros conectores. Básicamente son custom mediators, que admiten múltiples propiedades y los cuales tienen nombres identificativos de las operaciones que realizan. 

Vamos a crear una API con varios recursos. En el primer recurso realizaremos los siguientes pasos:
  • Buscamos el identificador en Redis a través de la operación hExists. En la cual tenemos que incluir el volumen donde esta almacenado y su clave. 
  • Si existe la clave, utilizaremos la operación hGet con los mismos valores para recuperar el valor almacenado. 
  • Si no existe la clave, procederemos a llamar al backend. Posteriormente almacenaremos el valor a través de la operación hSet, en la cual indicaremos el volumen, la clave y el valor asociado. 
Además debemos tener en cuenta distintos aspectos del conector a tener en cuenta:
  • La primera vez que utilicemos el conector debemos invocar la operación init, la cual nos posibilitará la conexión a Redis. 
  • El resultado de la operación hExists se almacena en una propiedad de contexto denominada redisResponse. 
  • La operación hGet devuelve un valor true o false en base a si encuentra o no el resultado.
  • Debemos almacenar y restaurar el payload al utilizar la operación hSet para evitar devolver el resultado de la misma al cliente. 
<resource methods="GET" uri-template="/{bookId}">
    <inSequence>
        <property name="bookId" expression="$ctx:uri.var.bookId"/>
        <redis.init>
            <redisHost>localhost</redisHost>
            <redisPort>6379</redisPort>
            <redisTimeout>500</redisTimeout>
        </redis.init>
        <redis.hExists>
            <redisKey>Library</redisKey>
            <redisField>{$ctx:bookId}</redisField>
        </redis.hExists>
        <filter source="$ctx:redisResponse" regex="true">
            <then>
                <redis.hGet>
                    <redisKey>Library</redisKey>
                    <redisField>{$ctx:bookId}</redisField>
                </redis.hGet>
            </then>
            <else>
                <call>
                    <endpoint>
                        <http method="GET" uri-template="http://localhost:57001/book/{uri.var.bookId}"/>
                    </endpoint>
                </call>
                <enrich>
                    <source type="body" clone="false"/>
                    <target type="property" property="ORIGINAL_PAYLOAD"/>
                </enrich>
                <redis.hSet>
                    <redisKey>Library</redisKey>
                    <redisField>{$ctx:bookId}</redisField>
                    <redisValue>{$ctx:ORIGINAL_PAYLOAD}</redisValue>
                </redis.hSet>
                <enrich>
                    <source type="property" clone="false" property="ORIGINAL_PAYLOAD"/>
                    <target type="body"/>
                </enrich>
            </else>
        </filter>
        <property name="ContentType" scope="axis2" type="STRING" value="application/json"/>
        <respond/>
    </inSequence>
</resource>

A diferencia del cache mediator, con el conector de Redis podremos borrar un valor almacenado. Y esto es gracias a que el conector contiene múltiples operaciones. 

<resource methods="DELETE" uri-template="/{bookId}">
    <inSequence>
        <property name="bookId" expression="$ctx:uri.var.bookId"/>
        <redis.init>
            <redisHost>localhost</redisHost>
            <redisPort>6379</redisPort>
            <redisTimeout>500</redisTimeout>
        </redis.init>
        <redis.hDel>
            <redisKey>Library</redisKey>
            <redisFields>{$ctx:bookId}</redisFields>
        </redis.hDel>
        <respond/>
    </inSequence>
    <outSequence/>
    <faultSequence/>
</resource>

Este es un ejemplo muy básico, en nuestra integración podemos realizar diferentes llamadas y lógicas más complicadas para luego almacenar ese valor en base a un identificador. Esto nos puede llevar a mejorar mucho el rendimiento de nuestras integraciones. 

viernes, 20 de noviembre de 2020

Apache Camel: Uso de Cache con Caffeine

Hoy vamos a ver como mejorar el rendimiento de nuestros web services en Apache Camel. Para ello usaremos Caffeine Cache, una sencilla pero potente cache en memoria realizada en Java y basada en la cache de Guava.

La idea detras del uso del cache, será poder almacenar el resultado de operaciones costosas en memoría. De esta forma, en sucesivas llamadas podremos acceder a dicho resultado de forma rápida y agil evitando la operación más costosa. 

Empecemos como siempre indicando cuales son las librerías a incluir. Por un lado la propia dependencía para usar Caffeine y por otro el starter de Spring que nos provee de una configuración por defecto. 

<dependency>
  <groupId>org.apache.camel.springboot</groupId>
  <artifactId>camel-caffeine-starter</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-caffeine</artifactId>
</dependency>

¿Como funciona? Debemos pensar en la cache como su fuera un ConcurrentMap. Es decir, iremos colocando objetos asociados a una clave determinada y a través de esa clave podremos recuperarlos. Por tanto para poder usarlo, usaremos acciones del tipo GET o PUT asociados a una clave. El cuerpo del mensaje será el objeto almacenado en la cache. 

Cuando realicemos alguna operación Caffeine nos indicará si dicha operación ha resultado exitosa y si hay algún resultado asociado a la misma. Esta información nos la dará a través de valores almacenados en la cabecera: CamelCaffeineActionSucceeded y CamelCaffeineActionHasResult. Caffeine también tiene una interface, denominada CaffeineConstants, con todas las constantes asociadas a sus operaciones o resultados que nos puede ayudar en el desarrollo. 

A continuación veremos el ejemplo para obtener un libro en base a su identificador. El identificador nos servirá como clave para almacenar el libro asociado en la cache. Primero comprobaremos si se encuentra en la cache (acción GET), en dicho caso devolveremos el valor asociado. En caso contrario realizaremos la operación de consulta a base de datos y almacenaremos el resultado en la cache (acción PUT) para futuras consultas. 

rest().get("cache/book/{id}")
.description("Details of an book by id").outType(Book.class)
.produces(MediaType.APPLICATION_JSON_VALUE).route()
.log("Cache : Select Book By Id: ${header.id}")
.setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_GET))
.setHeader(CaffeineConstants.KEY, header("id"))
.toF("caffeine-cache://%s", "BookCache")
.log("Has Result ${header.CamelCaffeineActionHasResult} ActionSucceeded ${header.CamelCaffeineActionSucceeded}")
.choice().when(header(CaffeineConstants.ACTION_HAS_RESULT).isEqualTo(Boolean.FALSE))
   .to("sql:{{sql.selectById}}")
   .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT))
   .setHeader(CaffeineConstants.KEY, header("id"))
   .toF("caffeine-cache://%s", "BookCache")
.otherwise()
   .log("Cache is working");

Si hacemos una prueba, comprobaremos que los tiempos y el rendimiento con Caffeine son mejores. Si lo comparamos usando hey, aún siendo una operación muy básica vemos como los tiempos se reducen mucho:


Pero tenemos que tener en cuenta que pasaría si el resultado de esa operación costosa cambiase. En dicho caso la cache seguría funcionando y devolviendo un valor erróneo. Para evitar eso tenemos, tenemos dos opciones. Primera opción a través de la configuración, con la cual podremos modificar cuántos resultados y cuanto tiempo se mantendrán en memoría. 

O una segunda opción, que será directamente invalidar manualmente la cache de un determinado objeto. Por ejemplo, en el caso de que borremos un objeto, no queremos que esa cache siga existiendo y devuelva resultados erróneos. Esto podremos hacerlo a través de la acción INVALIDATE. 

rest().delete("cache/book/{id}").produces(MediaType.APPLICATION_JSON_VALUE)
.route().to("sql:{{sql.delete}}")
.setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_INVALIDATE))
.setHeader(CaffeineConstants.KEY, header("id"))
.toF("caffeine-cache://%s", "BookCache")
.setHeader(Exchange.CONTENT_TYPE, constant(MediaType.APPLICATION_JSON_VALUE))
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200)).setBody(constant(null));

Por último vamos a ver la opción de como debería comportarse una aplicación cuando se actualiza el objeto cacheado. La solución fácil sería invalidar la cache asociada a la clave y volver a cachearlo cuando sea invocado de nuevo. Pero esta opción nos hará realizar una operación costosa más, de las necesarias. Si ya tenemos el objeto podríamos simplemente almacenarlo de nuevo en la cache y evitar esa futura operación costosa. Ejemplo con el método PUT. 

rest().put("cache/book/{id}").produces(MediaType.APPLICATION_JSON_VALUE)
.type(Book.class).route()
.choice().when().simple("${header.id} < 1")
   .bean(BookSQLRouter.class, "negativeId")
.otherwise()
   .log("PUT : Body: ${body}")//${body[id]
   .to("sql:UPDATE BOOK SET NAME = :#${body.name}, AUTHOR = :#${body.author} WHERE ID = :#id")
   .process(exchange -> {
       final Book book = exchange.getIn().getBody(Book.class);
       book.setId(Integer.valueOf(exchange.getIn().getHeader("id").toString()));
    })
   .setHeader(CaffeineConstants.ACTION, constant(CaffeineConstants.ACTION_PUT))
   .setHeader(CaffeineConstants.KEY, header("id"))
   .toF("caffeine-cache://%s", "BookCache")
   .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
   .setBody(constant(null));

Como siempre podeis ver el código completo aquí.