Quarkus. Java supersónico y subatómico

Hace unos meses entré a trabajar a HCL, una empresa de consultoría de clase mundial que tiene oficinas en Guadalajara. Anteriormente había estado trabajando en TATA Consultancy Services, TCS, pero me salí porque el trabajo se me comenzó a hacer fastidioso. Después de aplicar a varias compañías y de haber hecho algunos procesos de selección, al final fui contratado por HCL en donde me asignaron a una cuenta aunque no entré a trabajar inmediatamente. Ciertas empresas mantienen una reserva de desarrolladores que poco a poco van instalando conforme otros desarrolladores se salen o nuevos proyectos se abren. Tiende a haber mucha rotación en las empresas de tecnología, atrás quedaron los días cuando se trabajaba de por vida para una sola compañía. Hoy en día quedarte mucho tiempo en una sola compañía invita al estancamiento profesional.

Pues bien, después de un mes en reserva, me asignaron un proyecto de reciente creación. El proyecto requería desarrollar en la plataforma Quarkus de la cuál nunca había oído hablar hasta ese momento. La fortaleza de Quarkus está en el desarrollo de microservicios en la nube. Esto de los microservicios ya tiene varios años como concepto pero creo que no fue hasta que Netflix tuvo éxito implementándolos que empezaron a agarrar tracción en la práctica. Ahora al parecer, las grandes empresas que conforman el famoso, o infame, si así se quiere ver, “corporate America”, están poniendo a prueba esta tecnología.

Pero, ¿Qué rayos es Quarkus?

Quarkus es un “framework” de Java que promete que las aplicaciones que se construyan con él se inicialicen con rapidez, de ahí lo supersónico, ocupen poco espacio en memoria y sus archivos jar ocupen poco espacio en disco, de ahí lo subatómico. En la imagen de abajo se muestran algunas métricas de su sitio oficial.

Para lograr lo anterior Quarkus hace uso de una JVM capaz de compilar en código nativo. Así se evitan los costos en tiempo cuando se ejecuta bytecode. Quarkus cuenta con un sistema que analiza metadatos en tiempo de compilación y remueve todas aquellas clases que no se usen en la aplicación. Esto reduce el tamaño de los archivos jar y mejora los tiempos de carga. También se reduce el uso de las librerías de reflexión y la imagen nativa lleva un proceso de prearranque. No entiendo muy bien estos últimos dos puntos todavía.

El sitio oficial de Quarkus contiene una serie de guías bastante digeribles para empezar a usarlo. Yo pude empezar a desarrollar en alrededor de dos semanas. En cambio, cuando quise aprender a usar Spring Boot, me tomó casi dos años entender como estaba el asunto. Qaurkus también tiene su generador de proyectos muy al estilo de Spring-Boot para empezar a programar a la de ya en la siguiente url.

Un ejemplo muy básico

Vamos a ejemplificar el uso de Quarkus. En las siguientes secciones construiremos una aplicacioncita de demostración. El proyecto se puede bajar del siguiente enlace de GitLab. Esta app está orientada a negocios. Consultará a una API externa para obtener una lista de los días de asueto en los Estados Unidos que es donde están los clientes a los que vale la pena atender. Aunque con un sencillo cambio en la configuración podremos obtener los días feriados de México o de muchos otros países del mundo. La lista de los días feriados eventualmente se utilizará para hacer cálculos con fechas. En muchas aplicaciones de negocios es necesario saber si un determinado día es día laboral, o si es fin de semana o día feriado. Por ejemplo una aplicación de reservas vacacionales necesita tener esta información para ajustar sus tarifas.

En este caso yo configuré la app usando el grupo por defecto, org.acme, que es el nombre del paquete en donde se van a guardar nuestras clases. El artefacto es el nombre del proyecto o folder bajo el cual van a estar todos los archivos que pertenecen a la aplicación. El nombre del artefacto es el mismo que el nombre de la carpeta comprimida que se va a generar cuando la aplicación se termine de configurar. Voy a usar Gradle para construir la aplicación. Las dependencias que voy a bajar para mi aplicación son las siguientes.

  • RESTEasy JAX-RS
  • RESTEasy Jackson
  • REST Client
  • REST Client Jackson

Con las primeras dos dependencias podemos hacer nuestros propios servicios REST. Con las dos últimas dependencias nos podemos conectar a servicios REST de terceros. Las dependencias que terminan en Jackson son para serializar y de-serializar mensajes JSON. Es decir para convertir las clases de datos o modelos a mensajes JSON y viceversa. Todas estas dependencias contienen una serie de anotaciones que nos ahorrarán un montón de trabajo. El árbol del proyecto que acabamos de bajar queda como en la figura de abajo.

Este proyecto viene con una sola clase muy sencilla, GreetingResource, que contiene los elementos básicos del desarrollo de una api REST en Quarkus. Es una aplicacioncita “hello world” adaptada al mundo Quarkus.

package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello RESTEasy";
    }
}

Cuando se llama al método hello() con el comando CuRL o con Postman o SoapUI o desde el navegador, como ustedes prefieran, se imprime en la ventana del navegador el mensaje “Hello RESTEasy

Usando el cliente REST

Vamos a modificar nuestro proyecto para crear algo un poco más interesante. Vamos a jalar información del servicio REST externo Holiday API sobre los días feriados de observancia nacional en Estados Unidos. Para ver que tipo de respuesta obtenemos de la API, utilizamos el siguiente CuRL.

$ curl -G -d country="MX" -d year="2020" -d pretty
-d key="__YOUR_API_KEY__" "https://holidayapi.com/v1/holidays"

El CuRL anterior arroja el JSON que se muestra abajo.

{
    "status": 200,
    "requests": {
        "used": 21,
        "available": 9979,
        "resets": "2021-10-01 00:00:00"
    },
    "holidays": [
        {
            "name": "New Year's Day",
            "date": "2020-01-01",
            "observed": "2020-01-01",
            "country": "US",
            "uuid": "82f78b8a-019e-479e-a19f-99040275f9bf",
            "weekday": {
                "date": {
                    "name": "Wednesday",
                    "numeric": 3
                },
                "observed": {
                    "name": "Wednesday",
                    "numeric": 3
                }
            },
            "public": "true"
        },
        {
            "name": "Martin Luther King, Jr. Day",
            "date": "2020-01-20",
            "observed": "2020-01-20",
            "country": "US",
            "uuid": "638d6949-d68e-438a-9335-5ef07e221db5",
            "weekday": {
                "date": {
                    "name": "Monday",
                    "numeric": 1
                },
                "observed": {
                    "name": "Monday",
                    "numeric": 1
                }
            },
            "public": "true"
        },
        {
            "name": "George Washington's Birthday",
            "date": "2020-02-17",
            "observed": "2020-02-17",
            "country": "US",
            "uuid": "0f1f387c-fad3-4f69-9275-2dcf2201b4a8",
            "weekday": {
                "date": {
                    "name": "Monday",
                    "numeric": 1
                },
                "observed": {
                    "name": "Monday",
                    "numeric": 1
                }
            },
            "public": "true"
        },
        {
            "name": "Memorial Day",
            "date": "2020-05-25",
            "observed": "2020-05-25",
            "country": "US",
            "uuid": "f7e566e9-ac02-4f8c-895d-9d673e8fe956",
            "weekday": {
                "date": {
                    "name": "Monday",
                    "numeric": 1
                },
                "observed": {
                    "name": "Monday",
                    "numeric": 1
                }
            },
            "public": "true"
        },
        {
            "name": "Independence Day",
            "date": "2020-07-04",
            "observed": "2020-07-03",
            "country": "US",
            "uuid": "88268759-9b90-468c-804f-b729b8418e7c",
            "weekday": {
                "date": {
                    "name": "Saturday",
                    "numeric": 6
                },
                "observed": {
                    "name": "Friday",
                    "numeric": 5
                }
            },
            "public": "true"
        },
        {
            "name": "Labor Day",
            "date": "2020-09-07",
            "observed": "2020-09-07",
            "country": "US",
            "uuid": "8f052b64-922c-420f-b793-4bec237580fa",
            "weekday": {
                "date": {
                    "name": "Monday",
                    "numeric": 1
                },
                "observed": {
                    "name": "Monday",
                    "numeric": 1
                }
            },
            "public": "true"
        },
        {
            "name": "Columbus Day",
            "date": "2020-10-12",
            "observed": "2020-10-12",
            "country": "US",
            "uuid": "dbb9cc22-e9d4-4a2b-9fec-4202e22d2a8d",
            "weekday": {
                "date": {
                    "name": "Monday",
                    "numeric": 1
                },
                "observed": {
                    "name": "Monday",
                    "numeric": 1
                }
            },
            "public": "true"
        },
        {
            "name": "Veterans Day",
            "date": "2020-11-11",
            "observed": "2020-11-11",
            "country": "US",
            "uuid": "3e239a53-e436-458e-9452-f93d9470c17e",
            "weekday": {
                "date": {
                    "name": "Wednesday",
                    "numeric": 3
                },
                "observed": {
                    "name": "Wednesday",
                    "numeric": 3
                }
            },
            "public": "true"
        },
        {
            "name": "Thanksgiving Day",
            "date": "2020-11-26",
            "observed": "2020-11-26",
            "country": "US",
            "uuid": "1bc492ba-366f-4318-96af-7c2372779cd5",
            "weekday": {
                "date": {
                    "name": "Thursday",
                    "numeric": 4
                },
                "observed": {
                    "name": "Thursday",
                    "numeric": 4
                }
            },
            "public": "true"
        },
        {
            "name": "Christmas Day",
            "date": "2020-12-25",
            "observed": "2020-12-25",
            "country": "US",
            "uuid": "57787cbd-300c-48e4-a456-165c8bdc97b0",
            "weekday": {
                "date": {
                    "name": "Friday",
                    "numeric": 5
                },
                "observed": {
                    "name": "Friday",
                    "numeric": 5
                }
            },
            "public": "true"
        }
    ]
}

Construyendo los modelos

Con esta información en mano vamos a construir una serie de modelos en Java. Los modelos son clases que tienen una estructura muy similar a la del JSON de arriba. Se dice que hay un “mapeo” entre el modelo y el mensaje JSON. Estos modelos sirven para guardar la información que viene en los mensajes JSON. También se puede crear mensajes JSON a partir de la información contenida en los modelo. En la jerga, a este proceso de conversión de información de un formato a otro para su transmisión y uso se llama serialización y deserialización respectivamente. Yo lo hice a mano, sin embargo, hay muchas paginas web que nos pueden ayudar y ahorrar algo de trabajo, por ejemplo, https://www.javainuse.com/pojo.

La relación entre los modelos es la siguiente.

  • BankHolidayModel
    • RequestModel
    • HolidyaDetailsModel
      • WeekDayModel
        • DateModel
        • ObservedModel
//BankHolidayModel

package org.acme.model.response;

import java.util.List;

public class BankHolidayModel {
    int status;
    RequestsModel requests;
    List<HolidayDetailsModel> holidays;

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public RequestsModel getRequests() {
        return requests;
    }

    public void setRequests(RequestsModel requests) {
        this.requests = requests;
    }

    public List<HolidayDetailsModel> getHolidays() {
        return holidays;
    }

    public void setHolidays(List<HolidayDetailsModel> holidays) {
        this.holidays = holidays;
    }
}
//RequestModel

package org.acme.model.response;

public class RequestsModel {
    int used;
    int available;
    String resets;

    public int getUsed() {
        return used;
    }

    public void setUsed(int used) {
        this.used = used;
    }

    public int getAvailable() {
        return available;
    }

    public void setAvailable(int available) {
        this.available = available;
    }

    public String getResets() {
        return resets;
    }

    public void setResets(String resets) {
        this.resets = resets;
    }
}
//HolidayDetailsModel

package org.acme.model.response;

import com.fasterxml.jackson.annotation.JsonProperty;

public class HolidayDetailsModel {
    String name;
    String date;
    String observed;
    @JsonProperty("public")
    String _public;
    String country;
    String uuid;
    WeekDayModel weekday;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getObserved() {
        return observed;
    }

    public void setObserved(String observed) {
        this.observed = observed;
    }

    public String get_public() {
        return _public;
    }

    public void set_public(String _public) {
        this._public = _public;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }

    public WeekDayModel getWeekday() {
        return weekday;
    }

    public void setWeekday(WeekDayModel weekday) {
        this.weekday = weekday;
    }
}
//WeekDayModel

package org.acme.model.response;

public class WeekDayModel {
    DateModel date;
    ObservedModel observed;

    public DateModel getDate() {
        return date;
    }

    public void setDate(DateModel date) {
        this.date = date;
    }

    public ObservedModel getObserved() {
        return observed;
    }

    public void setObserved(ObservedModel observed) {
        this.observed = observed;
    }
}
//DateModel

package org.acme.model.response;

public class DateModel {
    String name;
    int numeric;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNumeric() {
        return numeric;
    }

    public void setNumeric(int numeric) {
        this.numeric = numeric;
    }
}
//ObservedModel

package org.acme.model.response;

public class ObservedModel {
    String name;
    int numeric;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNumeric() {
        return numeric;
    }

    public void setNumeric(int numeric) {
        this.numeric = numeric;
    }
}

Creando la interface

Para conectar con el servicio externo y obtener la información sobre los días feriados, se creará la interface IBankHolidaysService. Nosotros como desarrolladores, no necesitamos implementar esta interface, el framework lo hará por nosotros.

package org.acme.external;

import org.acme.model.response.BankHolidayModel;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;

@Path("/v1")
@RegisterRestClient
@RegisterClientHeaders
public interface IBankHolidaysService {
    @GET
    @Path("/holidays")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    BankHolidayModel getBankHolidaysData(
            @QueryParam("key") String key,
            @QueryParam("country") String country,
            @QueryParam("year") String year,
            @QueryParam("public") String publicHolidays
    );
}

El servicio externo requiere de algunos parámetros para funcionar. La anotación @QueryParam nos permite pasar esos parámetros a la API externa. Por ejemplo, actualmente toda API, aunque sea gratuita, requiere que nos registramos y que obtengamos una clave de acceso al servicio o API key que debemos incluir en algún lado. En nuestro caso y como estamos utilizando la versión gratuita, la clave se agrega en la URL.

Otros parámetros a pasar en la URL son el país del cual queremos obtener los días feriados, el año que se quiere consultar y varios otros parámetros optativos. Hay que tener en consideración que la versión gratuita del servicio externo no nos permite obtener la información de los feriados del año en curso. Se debe pagar una licencia para eso. Sin embargo, la versión gratuita es más que suficiente para desarrollar nuestra aplicación.

Configuración

Debemos de alguna forma enlazar o conectar la interface con la API externa que se va a llamar. Esto se hace a través del archivo de configuración application.properties que ya viene definido cuando dentro del árbol de proyecto desde su creación.

Dentro del archivo application.properties se agregan las siguientes dos lineas. De esta manera nuestra interface sabe a que API externa conectarse.

org.acme.external.IBankHolidaysService/mp-rest/url=https://holidayapi.com
org.acme.external.IBankHolidaysService/mp-rest/scope=javax.inject.Singleton

Creando el recurso JAX-RS

La clase HolidaysResource es el punto de entrada a nuestra API.

package org.acme.resource;

import org.acme.external.IBankHolidaysService;
import org.acme.model.response.BankHolidayModel;
import org.acme.model.response.HolidayDetailsModel;
import org.eclipse.microprofile.rest.client.inject.RestClient;

import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/v1")
public class HolidaysResource {
    @Inject
    @RestClient //adding this is very important to run the program
    IBankHolidaysService bankHolidaysService;

    @GET
    @Path("/holidays")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response getBankHolidaysList() {
        BankHolidayModel bankHolidays = bankHolidaysService.getBankHolidaysData("ca578b91-0eaf-4cc6-8e28-daa40f640264", "US", "2020", "true");

        return Response.ok(bankHolidays).build();
    }
}

Trabajo futuro

Hace falta definir algunos “endpoints” y reglas de negocio propios para usar la información obtenida de la API externa.

La API para obtener la información sobre los días feriados no requiere ser consultada con frecuencia. La información sobre los días feriados no es algo que cambie continuamente. Consultar esta información una vez al día o una vez cada pocos días sería suficiente para muchas aplicaciones. En una futura actualización se estará llamando a la API externa a través de una método cronometrado (scheduler). Este método consultará la API externa cada cierto tiempo y guardará la información en un caché. El caché será consultado por nuestras reglas de negocio tantas veces como sea necesario.

Hay muchas otras cosas que se deben hacer para tener al menos una API completamente funcional. Ya veremos.

Enlaces

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *