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.
- 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]
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]
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"
- 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
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
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.
- 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
Podemos generar un binario directamente desde nuestro SO (necesitamos GraalVM) o generarlo desde un contenedor Docker, que tiene GraalVM instalado.
- 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!"
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.
Implementa un subconjunto de CDI 2.0.
En todo lo que son servicios usar @ApplicationScoped, ya que no tienen estado.
Usando Json-B (extensión "resteasy-jsonb") (además de JAXRS (con "resteasy"))
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
- 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 { }; }
- Crear la clase
NotExpiredValidator
que implementeConstraintValidator<NotExpired, LocalDate>
y su métodoboolean isValid(LocalDate value, ConstraintValidatorContext context)
- Anotar el campo deseado con
@NotExpired
- Usando LogManager de org.jboss.logging
- Configuramos el nivel de log de Quarkus con la property
quarkus.log.console.level
, por ejemplo:
quarkus.log.console.level=DEBUG
- Para configurar el nivel de log de nuestra aplicación:
quarkus.log.category."org.acme".level=DEBUG"
- Añadir la extensión 'quarkus-rest-client'
- Crear una interfaz con las anotaciones JAXRS necesarias y @RegisterRestClient que se mapee contra el servicio REST a consultar.
- En application.properties registrar la URL base. Por ejemplo:
org.acme.quickstart.restclient.WorldClockService/mp-rest/url=http://worldclockapi.com
- Allí donde se quiera utilizar, inyectar con @Inject y @RestClient.
- Recibir y propagar el Header 'Authorization'.
Añadir en application.properties:
org.eclipse.microprofile.rest.client.propagateHeaders=Authorization
- 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")
- Usando un POJO.
3.1. Creamos un POJO
WorldClockHeaders.java
:
3.2. Declaramos su uso en el método de la interfaz Rest Client:public class WorldClockHeaders { @HeaderParam("X-Logger") String logger; }
3.3. Lo usamos en nuestro código:@GET @Path("/json/cet/now") @Produces(MediaType.APPLICATION_JSON) @ClientHeaderParam(name="X-Logger", value="DEBUG") WorldClock getNow(@BeanParam WorldClockHeaders headers);
@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; }
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.
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"));
}
}
Para hacer cosas antes y después de los test, como sobreescribir las propiedades programáticamente o limpiar la BBDD tenemos que:
- 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. - Anotar la clase de test con
@QuarkusTestResource(MyQuarkusTestResourceLifecycleManager.class)
Quarkus usa JPA con la implementación de Hibernate, aunque también podemos usar Panache.
- Debemos añadir la extensión
quarkus-hibernate-orm
y la extensión del driver JDBC de la BBDD que usemos:quarkus-jdbc-mariadb
- Añadir la configuración del Datasource en application.properties
Util especialmente el poner la propiedadquarkus.hibernate-orm.database.generation=update
para que vaya actualizando el esquema solo.
Panache es un framework de persistencia que implementa Active Record pattern y DAO pattern. \
- Debemos añadir la extensión
quarkus-panache
- Nuestra @Entity debe extender de PanacheEntity
- (Opcional) Los campos podemos dejarlos public ya que luego en runtime utiliza los getters/setters
- 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()
, ... - Para consultas y otros métodos no asociados a una entidad podemos usar los métodos estáticos
Entidad.findById(id)
,Entidad.findAll()
, ...
- 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
oPerson.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).
- Deberemos crear nuestro
DevelopersRepository
que extienda dePanacheRepository<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(); }
Para producción no usaremos el auto generado del esquema. Una buena herramienta es Flyway, que permite versionar el esquema con scripts. \
- Añadir extensión
quarkus-flyway
- Crear una carpeta
db/migration
ensrc/main/resources
donde iran los scripts con la nomenclaturaV1.1.0__My_description.sql
. Importante: debe haber 2 barras bajas entre versión y descripción. - En application.properties indicar que se usará Flyway con
quarkus.flyway.migrate-at-start=true
- 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)
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. \
Quarkus se integra con RxJava
- Añadimos la extensión "quarkus-smallrye-reactive-streams-operators"
- El endpoint que queremos hacer reactivo debe devolver un
CompletionStage<Response>
(oCompletionStage<JavaBean>
, ...) - El return se obtiene con
ReactiveStreams.***
- El endpoint debe devolver un
Publisher<JavaBean>
y estar anotado con@Produces(MediaType.SERVER_SENT_EVENTS)
- El return se obtiene con
Flowable.***
Nota: Esto se puede probar con un curl (o httpie) normal :)
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 unFlowable<Dato>
. - El consumidor es un metodo anotado con
@Incoming("key")
que tiene por parámetroDato
y no devuelve nada (void).
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 aemitter.send(dato)
. - El consumidor sirve el mismo que antes
https://quarkus.io/guides/kafka-guide En este caso mandaremos mensajes/eventos con un Stream a Kafka y los consumiremos con otro Stream. \
- Tenemos que añadir la extensión de reactive-messaging-kafka
- Arrancamos Kafka con docker
- El productor sirve el mismo que antes (podemos cambiar la key del Stream a
generated-temperature
) - 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
https://quarkus.io/guides/amqp-guide
- Añadir extensión
- Mismo productor y mismo consumidor
- Configuración en application.properties para AMQP (ver guía)
Utilizaremos la extensión de rest-client y reutlizaremos la interface "WorldClockService" del ejemplo sobre RestClient. \
- 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);
- 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));
}
https://quarkus.io/guides/jwt-guide
- Añadir extensión
- Tener nuestra clave pública
publicKey.pem
en META-INF/resources - Configurar
application.properties
- 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
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.
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
- En cualquier clase anotamos uno o varios métodos con
@Produces
y@Liveness
o@Readiness
y hacemos que devuelva un objetoHealthCheck
- El mismo método puede tener ambas anotaciones
- 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.
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.
[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/