Skip to content

Instantly share code, notes, and snippets.

@jmlclosa
Last active February 4, 2020 20:31
Show Gist options
  • Save jmlclosa/4913709f0d694b8b054f9fcf00ef3dec to your computer and use it in GitHub Desktop.
Save jmlclosa/4913709f0d694b8b054f9fcf00ef3dec to your computer and use it in GitHub Desktop.
[Notas sobre Quarkus] #quarkus #formación

Introducción

Qué es?

Es un framework Open Source para el desarrollo de aplicaciones Java orientadas a microservicios y "cloud-native", es decir, pensadas para funcionar en contenedores de Docker. Proporciona un subconjunto de implementaciones de Java EE 8 (JPA, CDI a medias, JAX-RS, JSON-P, JSON-B, Bean Validation, ...) y implementación completa de MicroProfile (Health, Meter, Rest client, ...)

Es un proyecto similar a Spring Boot, Thorntail o Payara Micro, pero con algunas ventajas adicionales que veremos.

No es un servidor de aplicaciones. En un Servidor de aplicaciones tradicional, cuando despliegas un WAR, lo descomprime, genera proxies, analiza anotaciones, etc. Quarkus hace lo mismo pero en tiempo de compilación. Por lo que tenemos los errores de despliegue al compilar.

Utiliza Graal VM (Máquina virtual políglota que soporta aplicaciones JavaScript, Python, Ruby, R, JVM-based, C y C++ y permite la interoperabilidad entre éstas). Gracias a Graal VM es capaz de generar aplicaciones nativas (binarios) que permiten ejecutar una aplicación sin una máquina virtual.

Principales características

  • Al estar basado en Java EE y MP, no tenemos que aprender prácticamente nada, y podríamos "volver atrás" fácilmente (usar un servidor de aplicaciones)
  • Arranque rapidísimo y más en binario
  • Consumo de memoria bajo, más bajo en binario
  • Buenos tiempos de respuesta gracias a que hace muchas de las operaciones pesadas en tiempo de compilación
  • Uber-JAR con la aplicación en un JAR y las librerías externas en otra carpeta, en vez de un "Fat JAR". Esto hace que el desplegar una nueva versión de la aplicación sea más rápido, ya que solo se necesita desplegar el JAR de la aplicación (y librerías si se ha añadido alguna dependencia) o la última capa (la del JAR) si estamos generando una imagen Docker.
  • Desarrollo rápido con su modo quarkus:dev. Ya no necesitamos JRebel!
  • Integraciones con librerías y herramientas como Docker, Camel, Kafka, K8s, Keycloak, ...
  • Rápida evolución
  • Pensado para aplicaciones cloud-native, ya que al reducir el consumo de CPU y RAM hasta 10 veces en modo binario, permite optimizar el uso de recursos en la nube y, por tanto, reducir la factura. En modo JVM el consumo de memoria se reduce por 2. [3]

Porqué quiero un binario?

Uno de los motivos principales es que cuando ejecutamos una JVM en un contenedor puede explotar debido a que JVM no sabe que está en un contenedor e intenta coger toda la memoria y CPU que puede/necesita, pudiendo superar el límite del contenedor. Cuando esto pasa, Linux mata el proceso. Esto se soluciona a partir de Java 11, pero Quarkus ya lo soluciona al generar un binario. [3]

Cómo funciona?

Ejecutar "Hello World"

mvn io.quarkus:quarkus-maven-plugin:0.20.0:create \
    -DprojectGroupId=com.ctm.examples \     
    -DprojectArtifactId=quarkus-hello \        
    -DclassName="com.ctm.examples.HelloQuarkusResource" \     
    -Dpath="/hello"

./mvnw compile quarkus:dev
  • Produce un JAR y una carpeta donde estan todas las dependencias.
  • También podemos producir un binario, ¡que genera dentro de un contenedor de Docker!
  • Listar las extensiones:
./mvnw quarkus:list-extensions
./mvnw quarkus:add-extension -Dextensions="jdbc,hibernate"
./mvnw quarkus:add-extension -Dextensions="quarkus-jdbc-mariadb,quarkus-hibernate-orm"

Guión del ejemplo

  • Hello World (/)
    • Generar JAR
    • Generar binario
    • Generar imagen de docker
  • CDI
  • JPA + database (H2)
  • JTA Transactions
  • JAXRS with Resteasy and JSON-B
  • Health
  • OpenAPI and Swagger UI
  • Security with JWT
  • Rest client
  • Use of configuration profiles on application.properties
  • Security with Keycloak 3.4.3
  • Fault tolerance

Binario

Para generar el binario ejecutamos: ./mvnw package -Pnative Para esto necesitamos GraalVM. Si no lo tenemos o no queremos instalar, lo podemos generar dentro de una imagen docker: ./mvnw package -Pnative -Dnative-image.docker-build=true

Docker

Al crear un proyecto Quarkus genera dos Dockerfile, uno para generar una imagen con JVM y otro para generar una imagen con la aplicación en nativo.
En ambos Dockerfile da las instrucciones de como generar la imagen.

Apuntes

  • No implementa todo CDI
    • Si una clase no está anotada con una anotación de scope (@Dependent, @ApplicationScoped, @Singleton, @RequestScoped and @SessionScoped) o como @Dependent no podrá ser inyectada
    • Interceptores (@AroundInvoke, @PostConstruct, @PreDestroy, @AroundConstruct)
    • No soporta @ConversationScoped ni @Decorator
  • @ApplicationScoped doesnt provide better performance than RequestScoped, there is no reflection.
  • EJB are not included because same reason.
  • No tenemos JSF, por lo que la UI tiene que estar aparte o usar HTML/JS

Notas curso Quarkus de OpenWebinars [2]

Compilación nativa

Podemos generar un binario directamente desde nuestro SO (necesitamos GraalVM) o generarlo desde un contenedor Docker, que tiene GraalVM instalado.

Configuración

  • application.properties
  • carpeta en el directorio donde arrancamos la aplicación: config/application.properties
  • Variable de entorno: export MI_SALUDO="Hola gente!" unset MI_SALUDO
  • Argumento de arranque: ./mi_aplicacion -Dmi.saludo="Hello world!"

Cargar configuración personalizada

Usando un converter podemos cargar desde BBDD, en memoria, caché, etc.
Debemos implementar ConfigSource (SPI de microprofile-config) y crear en META-INF/services el fichero que indique la implementación de ConfigSource.

Inyección de dependencias

Implementa un subconjunto de CDI 2.0.
En todo lo que son servicios usar @ApplicationScoped, ya que no tienen estado.

JSON

Usando Json-B (extensión "resteasy-jsonb") (además de JAXRS (con "resteasy"))

Validaciones

Se usa las anotaciones de javax.validation en los Beans y @Valid en el método REST o en métodos CDI.
Debemos instalar la extension "hibernate-validator".
Si falla una validación devuelve un 400-Bad request

Validaciones propias

  1. Crear una anotación con su @Target y su @Retention y que tenga 3 campos:
    @Target(...)
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {NotExpiredValidator.class})
    public @interface NotExpired {
      String message() default "Beer must not be expired"
      Class<?>[] groups() default { };
      Class<? extends Payload>[] payload() default { };
    }
    
  2. Crear la clase NotExpiredValidator que implemente ConstraintValidator<NotExpired, LocalDate> y su método boolean isValid(LocalDate value, ConstraintValidatorContext context)
  3. Anotar el campo deseado con @NotExpired

Logging

  1. Usando LogManager de org.jboss.logging
  2. Configuramos el nivel de log de Quarkus con la property quarkus.log.console.level, por ejemplo:
    quarkus.log.console.level=DEBUG
  3. Para configurar el nivel de log de nuestra aplicación:
    quarkus.log.category."org.acme".level=DEBUG"

Rest client

  1. Añadir la extensión 'quarkus-rest-client'
  2. Crear una interfaz con las anotaciones JAXRS necesarias y @RegisterRestClient que se mapee contra el servicio REST a consultar.
  3. En application.properties registrar la URL base. Por ejemplo:
    org.acme.quickstart.restclient.WorldClockService/mp-rest/url=http://worldclockapi.com
  4. Allí donde se quiera utilizar, inyectar con @Inject y @RestClient.

Añadir headers en Rest Client:

  1. Recibir y propagar el Header 'Authorization'.
    Añadir en application.properties:
    org.eclipse.microprofile.rest.client.propagateHeaders=Authorization
  2. Añadir un Header estático.
    En la interfaz RestClient declarada, en el método donde queremos añadir el HEader, añadir:
    @ClientHeaderParam(name="X-Logger", value="DEBUG")
  3. Usando un POJO. 3.1. Creamos un POJO WorldClockHeaders.java:
    public class WorldClockHeaders {
        @HeaderParam("X-Logger")
        String logger;
    }
    
    3.2. Declaramos su uso en el método de la interfaz Rest Client:
    @GET
    @Path("/json/cet/now")
    @Produces(MediaType.APPLICATION_JSON)
    @ClientHeaderParam(name="X-Logger", value="DEBUG")
    WorldClock getNow(@BeanParam WorldClockHeaders headers);
    
    3.3. Lo usamos en nuestro código:
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public WorldClock now() {
        WorldClockHeaders headers = new WorldClockHeaders();
        headers.localization = "Spain";
        WorldClock now = worldClockService.getNow(headers);
        System.out.println(now);
        return now;
    }
    

Testing en Quarkus

Usamos por defecto JUnit 5 y RestAssured.
Creamos un test unitario normal y lo anotamos con @QuarkusTest. Esto hará que compile, empaquete y ejecute nuestra aplicación Quarkus antes de pasar los test. Así podemos lanzar los test de integración REST.

Mocking y Stubbing en Quarkus

Teniendo una interfaz 'ExpensiveService' de un servicio que inyectamos en la clase a testear y una implementación real 'RealExpensiveService' que la implementa, en producción se ejecutará esta.
En test, para no ejecutar la funcionalidad de RealExpensiveTest y seguir haciendo test de integración con @QuarkusTest podemos crear una implementación mock 'MockExpensiveService' anotada con @ApplicationScoped y @Mock que se inyectará automáticamente en vez de RealExpensiveService:\

public interface ExpensiveService {
    int calculate();
}
---
@ApplicationScoped
public class RealExpensiveService implements ExpensiveService {
    @Override
    public int calculate() {
        sleep(5000); // Omitido codigo real
        return 100;
    }
}
---
@Path("/mocking")
public class MockingResource {
    @Inject
    ExpensiveService expensiveService;
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public int expensive() {
        return expensiveService.calculate();
    }
}
---
@ApplicationScoped
@Mock
public class MockExpensiveService implements ExpensiveService {
    @Override
    public int calculate() {
        return 20;
    }
}
---
@QuarkusTest
class MockingResourceTest {
    @Test
    void expensive() {
        given()
        .when().get("/mocking")
        .then().statusCode(200)
                .body(CoreMatchers.is("20"));
    }
}

Quarkus test Resources and actions

Para hacer cosas antes y después de los test, como sobreescribir las propiedades programáticamente o limpiar la BBDD tenemos que:

  1. Implementar QuarkusTestResourceLifecycleManager y sus métodos "start" y "stop".
    En el método start se devuelve un Map<String, String> que nos sirve para sobreescribir propiedades de application.properties.
  2. Anotar la clase de test con @QuarkusTestResource(MyQuarkusTestResourceLifecycleManager.class)

Quarkus Data con Hibernate

Quarkus usa JPA con la implementación de Hibernate, aunque también podemos usar Panache.

  1. Debemos añadir la extensión quarkus-hibernate-orm y la extensión del driver JDBC de la BBDD que usemos: quarkus-jdbc-mariadb
  2. Añadir la configuración del Datasource en application.properties
    Util especialmente el poner la propiedad quarkus.hibernate-orm.database.generation=update para que vaya actualizando el esquema solo.

Quarkus Data con Panache

Panache es un framework de persistencia que implementa Active Record pattern y DAO pattern. \

  1. Debemos añadir la extensión quarkus-panache

Quarkus Data con Panache: Active Record pattern

  1. Nuestra @Entity debe extender de PanacheEntity
  2. (Opcional) Los campos podemos dejarlos public ya que luego en runtime utiliza los getters/setters
  3. En el servicio ya no utilizamos un EntityManager si no que directamente nuestra entidad tiene una serie de métodos para trabajar con la persistencia como persist(), persist(List<Entidad>), delete(), ...
  4. Para consultas y otros métodos no asociados a una entidad podemos usar los métodos estáticos Entidad.findById(id), Entidad.findAll(), ...

Queries con Panache usando Active Record

  • Para buscar por un campo usaremos el método estático Person.find("name", name).firstResult()
  • Para buscar por dos campos: Person.find("name = ?1 and age = ?2", name, age).firstResult()
  • También proporciona una funcionalidad para paginación de resultados: Person.find("...").page(): Page o Person.find("...").page(Page page), ...
  • También proporciona una funcionalidad para ordenar resultados
  • count()
  • ...
  • Si nuestra Entity extiende de PanacheEntity, se crea un ID auto-generado y no lo tenemos que declarar. Además, es un atributo público y podemos acceder a él sin getter. Si queremos personalizar el ID deberemos extender de PanacheEntityBase y declarar nuestro ID (JPA).

Quarkus Data con Panache: DAO pattern

  • Deberemos crear nuestro DevelopersRepository que extienda de PanacheRepository<Developer>
  • En este pattern si es necesario el ID
  • En DevelopersRepository tendremos acceso a métodos como persist(Developer), find("attr", value), ... Ejemplo de repository:\
    @Transactional
    public Developer create(Developer developer) {
        persist(developer);
        return developer;
    }
    
    public Developer findByName(String name) {
        return find("name", name).firstResult();
    }
    

Migrando BBDD con Flyway

Para producción no usaremos el auto generado del esquema. Una buena herramienta es Flyway, que permite versionar el esquema con scripts. \

  1. Añadir extensión quarkus-flyway
  2. Crear una carpeta db/migration en src/main/resources donde iran los scripts con la nomenclatura V1.1.0__My_description.sql. Importante: debe haber 2 barras bajas entre versión y descripción.
  3. En application.properties indicar que se usará Flyway con quarkus.flyway.migrate-at-start=true
  4. En application.properties asegurar que la propiedad de auto-generado de hibernate sea 'none': quarkus.hibernate-orm.database.generation=none

Al arrancar iremos viendo en los logs la versión del esquema usado:

2019-08-25 17:37:30,809 INFO  [org.fly.cor.int.lic.VersionPrinter] (main) Flyway Community Edition 5.2.4 by Boxfuse
2019-08-25 17:37:30,935 INFO  [org.fly.cor.int.dat.DatabaseFactory] (main) Database: jdbc:h2:file:./openwebinars (H2 1.4)
2019-08-25 17:37:31,085 INFO  [org.fly.cor.int.com.DbValidate] (main) Successfully validated 2 migrations (execution time 00:00.029s)
2019-08-25 17:37:31,110 INFO  [org.fly.cor.int.sch.JdbcTableSchemaHistory] (main) Creating Schema History table: "PUBLIC"."flyway_schema_history"
2019-08-25 17:37:31,152 INFO  [org.fly.cor.int.com.DbMigrate] (main) Current version of schema "PUBLIC": << Empty Schema >>
2019-08-25 17:37:31,155 INFO  [org.fly.cor.int.com.DbMigrate] (main) Migrating schema "PUBLIC" to version 1.0.0 - Initial
2019-08-25 17:37:31,190 INFO  [org.fly.cor.int.com.DbMigrate] (main) Migrating schema "PUBLIC" to version 1.1.0 - senior field
2019-08-25 17:37:31,234 INFO  [org.fly.cor.int.com.DbMigrate] (main) Successfully applied 2 migrations to schema "PUBLIC" (execution time 00:00.130s)

Reactive programming

¿Por qué?

Para liberar el pool de threads HTTP mientras se hace tareas "pesadas".
Normalmente cuando se hace una petición al servidor de aplicaciones se coge un thread del pool HTTP y se trata todo allí. Si tenemos muchas peticiones, se agotan el pool y empiezan a fallar o quedar colgadas nuevas peticiones.
Para solucionarlo, al recibir una petición se coge un thread del HTTP e inmediatamente se crea un nuevo thread (no del pool HTTP) donde se ejecuta la lógica de negocio y se libera el thread HTTP. Cuando acaba la lógia, se recupera el thread HTTP y se le se devuelve el resultado para que responda la petición inicial. \

¿Cómo?

Quarkus se integra con RxJava

  1. Añadimos la extensión "quarkus-smallrye-reactive-streams-operators"
  2. El endpoint que queremos hacer reactivo debe devolver un CompletionStage<Response> (o CompletionStage<JavaBean>, ...)
  3. El return se obtiene con ReactiveStreams.***

Usando Server Sent Events

  1. El endpoint debe devolver un Publisher<JavaBean> y estar anotado con @Produces(MediaType.SERVER_SENT_EVENTS)
  2. El return se obtiene con Flowable.*** Nota: Esto se puede probar con un curl (o httpie) normal :)

Usando Reactive messaging

En este caso "jugamos" con Producers y Consumers donde el productor es "standalone" y genera valores por si solo: \

  • El productor es un metodo anotado con @Outgoing("key") que devuelve un Flowable<Dato>.
  • El consumidor es un metodo anotado con @Incoming("key") que tiene por parámetro Dato y no devuelve nada (void).

Mandando a una cola de mensajes

En este caso el productor produce datos al llamar a un método: \

  • El productor es un campo Emitter<Dato> emitter de una clase que anotaremos con @Inject @Stream("key") y luego en el método que queremos que produzca los datos (por ejemplo un endpoint) llamamos a emitter.send(dato).
  • El consumidor sirve el mismo que antes

Reactive messaging con Kafka

https://quarkus.io/guides/kafka-guide En este caso mandaremos mensajes/eventos con un Stream a Kafka y los consumiremos con otro Stream. \

  1. Tenemos que añadir la extensión de reactive-messaging-kafka
  2. Arrancamos Kafka con docker
  3. El productor sirve el mismo que antes (podemos cambiar la key del Stream a generated-temperature)
  4. El consumidorsirve el mismo que antes (podemos cambiar la key del Stream a generated-temperature) .5 Configuración de kafka en application.properties con los outgoing (productor) y incoming (consumidor):
mp.messaging.outgoing.generated-temperature.connector=smallrye-kafka
mp.messaging.outgoing.generated-temperature.topic=temperature
mp.messaging.outgoing.generated-temperature.value.serializer=org.apache.kafka.common.serialization.IntegerSerializer
mp.messaging.incoming.generated-temperature.connector=smallrye-kafka
mp.messaging.incoming.generated-temperature.value.deserializer=org.apache.kafka.common.serialization.IntegerDeserializer

Reactive messaging con AMQP

https://quarkus.io/guides/amqp-guide

  1. Añadir extensión
  2. Mismo productor y mismo consumidor
  3. Configuración en application.properties para AMQP (ver guía)

Cliente REST asíncrono

Utilizaremos la extensión de rest-client y reutlizaremos la interface "WorldClockService" del ejemplo sobre RestClient. \

  1. Hacemos que la interfaz WorldClockService tenga el método now que devuelva un CompletionStage<WorldClock>:
@GET
@Path("/json/{timezone}/now")
@Produces(MediaType.APPLICATION_JSON)
CompletionStage<WorldClock> asyncNow(@PathParam("timezone") String timezone);
  1. En nuestro Resource que utiliza el método anterior, crearemos un nuevo endpoint y utilizaremos el CompletionStage para combinar resultados:
@GET
@Path("/async")
@Produces(MediaType.APPLICATION_JSON)
public CompletionStage<List<WorldClock>> asyncNows() {
    CompletionStage<WorldClock> cet = worldClockService.asyncNow("cet");
    return cet.thenCombineAsync(
            worldClockService.asyncNow("gmt"),
            (cetResult, gmtResult) -> Arrays.asList(cetResult, gmtResult));
}

Quarkus Cloud

MP - Seguridad con JWT

https://quarkus.io/guides/jwt-guide

  1. Añadir extensión
  2. Tener nuestra clave pública publicKey.pem en META-INF/resources
  3. Configurar application.properties
  4. Securizar nuestros endpoints con las anotaciones de javax.annotation.security

Podemos obtener datos del JsonWebToken inyectándolo o inyectando sus Claim.

También se puede securizar con Keycloak

MP - Tolerancia a fallos

https://quarkus.io/guides/fault-tolerance-guide Implementa patrones de tolerancia a fallos orientados a microservicios. Por ejemplo: @Retry, @Fallback, @CircuitBreaker, @Bulkhead, @Timeout

  • El Fallback se puede implementar en un método o en una clase que implemente FallbackHandler. Para usarlo sería: @Fallback(fallbackMethod = "miFallbackMethod") o @Fallback(MiFallbackHandler.class) respectivamente.

MP - Health Checks

https://quarkus.io/guides/health-guide

  • @Liveness en una clase que implemente HealthCheck para ir comprobando si nuestra aplicación sigue viva a través del endpoint /health/live
  • @Readiness en una clase que implemente HealthCheck para comprobar si nuestra aplicación ha arrancado a través del endpoint /health/ready En /health se muestran ambas

Health Checks con CDI

  • En cualquier clase anotamos uno o varios métodos con @Produces y @Liveness o @Readiness y hacemos que devuelva un objeto HealthCheck
  • El mismo método puede tener ambas anotaciones

MP - Métricas

MP - OpenTracing

MP - OpenAPI

Mandar emails

  • Añadir extensión
  • Configurar en application.properties
  • Viene con 2 clientes, uno reactivo y otro no
  • Inyectamos un "Mailer" o "ReactiveMailer", el primero devuelve "void" y el segundo un "CompletionStage". Estas clases nos dan un metodo para mandar mails
  • Hay una property "mailer.mock" que nos permite no mandar realmente mails, para el entorno de dev. Esto imprime por consola el mail mandado.

Tareas periódicas

Despliegue y escalado en Kubernetes

Tenemos una extensión para kubernetes que nos permite generar los ficheros de configuración para desplegar la aplicación en K8s. \

  • Genera los ficheros en target/writing-classes/kubernetes, con kubernetes.yaml como principal.
  • Configura los puertos, la imagen Docker a usar (que se genera como siempre)
  • Si usamos el API de Health Check nos añade la livenessProbe y la readinessProbe en el yaml.
  • Si ejecutamos kubectl apply -f kubernetes.yaml, desplegará el pod
  • Podemos escalar usando kubectl scale deployment getting-started --replicas=20
    Arrancar estos 20 servicios en nativo es muy rápido y consume "poco" al ser nativo.

Resources

[1] 61st Airhacks.tv (Adam Bien): https://youtu.be/0LcdDini73o?t=335
[2] Curso en OpenWebinars de Alex Soto: https://openwebinars.net/cursos/quarkus/
[3] Entrevista a Alex Soto: https://jaxenter.com/quarkus-whats-next-for-the-lightweight-java-framework-160793.html
[4] Cheat sheet de Quarkus de Alex Soto: https://lordofthejars.github.io/quarkus-cheat-sheet/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment