Zoom sur Quarkus

Quarkus est un nouveau framework Java, orienté développement de microservices, sorti début mars 2019. Il est développé par RedHat et est pensé pour le développement d’applications à déployer dans le cloud en supportant nativement Kubernetes (et OpenShift) et en permettant la création facilité d’un package natif de votre application léger en termes de taille de livrable et en utilisation mémoire. L’utilisation mémoire a été particulièrement optimisée pour la mémoire non-heap (metaspace et autre) en réduisant le nombre de classes chargées au runtime et donc en optimisant le Resident Set Size (RSS) de votre application (la taille totale de la mémoire utilisée par le process de votre application sur votre system).
À sa sortie, il a suscité pas mal d’étonnements, explications.

Quarkus : quoi, comment ?

Au développement, Quarkus est basé sur MicroProfile (via SmallRye : https://smallrye.io/), on développe alors des services standards Jax-RS qui seront servis par la couche HTTP de Undertow (le serveur HTTP de JBoss) et dont certaines fonctionnalités réactives et asynchrones seront apportées par Vert.X. Ce dernier permet aussi l’utilisation de client HTTP non bloquant et de messaging asynchrone.
Dans ces services, la donnée peut être retournée directement, ou via un CompletionStage<T> ou un Publisher<T> pour permettre un appel synchrone, asynchrone ou réactif au sein de la même API : trois paradigmes, une seule implémentation, et ça c’est top ! Plus d’info ici : Quarkus – Continuum
Quarkus vient avec un plugin Maven (ou Gradle) qui va se charger de toute la tuyauterie nécessaire pour le moteur d’injection de dépendances et le démarrage de l’application (le bootstraping), il va faire ça à la compilation (et pas au runtime comme Spring), via génération de bytecode. Ce qui permet un lancement plus rapide de votre application, et une empreinte mémoire plus faible. Par contre, cela rend obligatoire la compilation via Maven de votre projet (et donc pas via votre IDE) ; pour rendre ça plus attractif pour les développeurs, le plugin de compilation de Quarkus peut se lancer en mode dev avec du livereload !
Pour le déploiement, on peut déployer Quarkus via un jar autoporté à packager avec ses librairies, ou via une application native. C’est le plugin Maven de Quarkus qui va se charger de générer pour nous une image native de notre application via GraalVM si on le désire. Et il fait ça très bien, sans aucune configuration de notre part. On peut après cela construire une image Docker de quelques dizaines de Mo qui démarrera en quelques dizaines de ms avec une empreinte mémoire très faible !
Voici une comparaison, en termes d’empreinte mémoire et de taille d’image, d’applications classiques cloud native avec et sans Quarkus (source : https://quarkus.io/)

On peut voir que Quarkus démarre plus rapidement et avec une empreinte mémoire plus faible que les solutions standard du marché même sans compilation native, avec compilation native l’empreinte mémoire et le temps de démarrage sont époustouflants !
Tout ça semble prometteur non ? Pour visualiser comment ça marche, voici un tutoriel pour vos premiers pas en Quarkus !

Quarkus : Tutoriel

Étape 1 : générer un projet depuis l’artifact maven

mvn io.quarkus:quarkus-maven-plugin:0.13.1:create \
    -DprojectGroupId=fr.loicmathieu.demo.quarkus \
    -DprojectArtifactId=quarkus-demo \
    -DclassName="fr.loicmathieu.demo.quarkus.rest.PersonRest" \
    -Dpath="/persons" \
    -Dextensions="resteasy-jsonb"

Pour cela, on peut suivre le tutoriel suivant https://quarkus.io/guides/getting-started-guide, j’ai juste ajouté l’extension qui permet la sérialisation en Json.
On peut remarquer les points suivants en regardant ce qui a été généré :

  • Pas de classe Application
  • Dépendances gérées via le BOM de Quarkus
  • Seules dépendances (hors test) : quarkus-resteasy et quarkus-resteasy-jsonb
  • Dépendances de tests (junit5 pour les TU et rest-assured pour les tests d’intégration/tests fonctionnels)
  • Le plugin Maven de Quarkus
  • Un profil maven spécifique pour le natif

Étape 2 : démarrer le projet généré

mvn compile quarkus:dev

Il n’y a pas de support par les IDE, et comme Quarkus doit faire des étapes spécifiques à la compilation pour fonctionner (via un plugin Maven), il doit être lancé en ligne de commande via Maven. Mais heureusement pour nous, il y a du livereload très rapide (moins d’une seconde) !
Voici les logs de démarrage :

Listening for transport dt_socket at address: 5005
[io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
[io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 359ms
[io.quarkus] (main) Quarkus 0.13.1 started in 0.648s. Listening on: http://[::]:8080
[io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb]

On peut remarquer ici :

  • Par défaut, le port de debug est ouvert (5005)
  • Quarkus Augmentation : la magie !!! On en reparlera…
  • Démarrage en 650 ms
  • Écoute sur le port 8080
  • Les features chargées : cdi (pour l’injection de dépendance) et resteasy (pour les WS JAX-RS)
  • Un appel sur http://localhost:8080/persons nous retournera “hello”

Étape 3 : coder…

Un petit CRUD sur un objet Person avec un service et un repository…
Pour cela, il nous faut ajouter le jar de quarkus-orm-hibernate, et en fonction de la BDD utilisée, le jar correspondant (ici quarkus-jdbc-postgres).
Pendant le développement, le hot reload se fait à chaque appel (et pas à chaque sauvegarde d’un fichier, ce qui est bien) en moins de 500 ms la première fois chiffre qui tombe à 200ms au deuxième rechargement !
Pour développer ça, on peut suivre le tutoriel qui est ici : https://quarkus.io/guides/hibernate-orm-guide
Voici les principales étapes :

1. Créer un objet Person : source complète via lien en fin d’article

@Entity
public class Person implements Serializable {
   private Long id;
   private String firstName;
   private String lastName;
    @Id
   public Long getId() {
       return id;
   }
   public void setId(Long id) {
       this.id = id;
   }
    // autres getters et setters
}

2. Créer un repository JPA

@ApplicationScoped
public class PersonRepository {
   @Inject
   private EntityManager em;
    public void set(Person person){
       em.persist(person);
   }
    public Person get(Long id){
       return em.find(Person.class, id);
   }
    public List<Person> list(){
       return em.createQuery("from Person").getResultList();
   }
    public void delete(Long id){
       em.remove(em.find(Person.class, id));
   }
}

3. Créer un service (interface et implémentation)

public interface PersonService {
    public void set(Person person);
    public Person get(Long id);
    public List list();
    public void delete(Long id);
}
@ApplicationScoped
public class PersonServiceImpl implements PersonService {
    @Inject
    private PersonRepository personRepository;
    @Transactional
    public void set(Person person) {
        personRepository.set(person);
    }
    public List<Person> list() {
        return personRepository.list();
    }
    public Person get(Long id) {
        return personRepository.get(id);
    }
    @Transactional
    public void delete(Long id) {
        personRepository.delete(id);
    }
}

Ici on peut remarquer un support des transactions très facile à utiliser via une simple annotation.
J’ai choisi ici de faire un repository JPA “à la main”, mais Quarkus vient avec Panache qui permet de faire de l’hibernate avec panache 😉 . Panache va permettre de faire des repository très facilement (via une interface unique proche de ce que fait Spring Data) ou des Entity améliorés à la Active Record : https://quarkus.io/guides/hibernate-orm-panache-guide

4. Le service REST ensuite est assez simple

@Path("/persons")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PersonRest {
   @Inject
   private PersonService personService;
   @GET
   public List<Person> list() {
       return personService.list();
   }
   @GET
   public Person get(@PathParam("id") Long id) {
       return personService.get(id);
   }
   @POST
   public void create(Person person) {
       personService.set(person);
   }
   @PUT
   @Path("/{id}")
   public void update(@PathParam("id") Long id, Person person) {
       if(! id.equals(person.getId())){
           throw new BadRequestException("id in path and in body must be the same");
       }
       personService.set(person);
   }
   @DELETE
   @Path("/{id}")
   public void delete(@PathParam("id") Long id) {
       personService.delete(id);
   }
}

Et voici notre CRUD codé ! Il reste juste un petit peu de configuration à réaliser, il faut ajouter dans l’application.properties les informations de configuration de la BDD. Comme tout est dans le repository github je vous laisser aller voir si vous êtes curieux !
Maintenant que notre webservice est fini d’être développé, on veut le lancer !
Même si pendant le développement un simple  mvn compile quarkus:dev était lancé, nous voulons maintenant packager notre application.

Étape 4 : packaging

Via maven :

mvn package

Lancement de l’application via son runner :

java -jar target/quarkus-demo-1.0-SNAPSHOT-runner.jar

Résultat : un jar de 10 Mo qui se lance en près d’une seconde, mais attention sa taille est trompeuse, il utilise un répertoire local de bibliothèques qui fait lui-même 24 Mo, on a donc une application d’une taille totale de 34 Mo.

Via docker

Il y a plusieurs manières de packager via Docker, allons directement à la plus pratique, la plus simple, la plus puissante : packager un natif via un multi-stage build !
Quoi ? natif ? multi-stage build ???
Natif : Quarkus vient avec tout ce qu’il faut pour générer un package natif (c’est à dire compilé en natif dans une version optimisée pour un OS / une architecture) en se basant sur l’outils native-image du projet Graal, et la SubstrateVM qui est une JVM permettant de faire tourner uniquement des applications natives et qui comprend le JIT Graal. Plus d’informations sur ce projet hautement intéressant : https://www.graalvm.org/.
Le problème c’est que pour compiler en natif, il faut que GraalVM soit installé en local, et il faut donner à l’outil native-image de GraalVM un fichier de configuration assez complexe. Le plugin maven de Quarkus s’occupe pour nous d’appeler l’outils native-image avec les informations de configuration nécessaires, mais il nous faut quand même GraalVM installé en local sauf si… on réalise le build Maven dans Docker ce qui est possible avec un build multi stage.
Un build docker multi-stage permet d’avoir plusieurs FROM dans son Dockerfile, le premier permettant de builder notre application et le second permettant de générer l’image finale. On imagine aisément l’intérêt ici : le premier stage permettra de builder via Maven et GraalVM notre package natif et le deuxième stage de packager notre application avec un FROM d’une image distroless (image minimaliste non basée sur une distribution Linux et qui ne contient qu’un ensemble très restreint d’outils).
C’est pour ces raisons que, même si l’archetype Maven nous a généré deux Dockerfiles (un pour un build standard et un autre pour un build natif), nécessitant à chaque fois de packager via Maven notre application en amont, je préfère utiliser le Dockerfile suivant :

## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
# we will build a native image using the native maven profile
RUN mvn -f /usr/src/app/pom.xml -DskipTests -Pnative clean package
## Stage 2 : create the docker final image form a distroless image !
FROM cescoffier/native-base
# we copy from the previous build the artifacts
COPY --from=build /usr/src/app/target/*-runner /application
EXPOSE 8080
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]

Il suffit ensuite de builder l’image pour avoir une image minimaliste, contenant uniquement le binaire de notre application compilé et intégrant la SubstratVM.
Avant ça, il faut modifier le fichier .dockerignore, car celui généré par l’artefact Maven pose des soucis avec cette technique de build.
Il faut remplacer son contenu par : src/main/docker/*
Ensuite le build se fait tout simplement via cette commande Docker :

docker build -t loicmathieu/quarkus-demo \
    -f src/main/docker/Dockerfile .

Ici, l’inconvénient est qu’à chaque build on va retélécharger via Maven tous les artefacts nécessaires, j’ai déjà quelques idées pour contourner en partie ça… 😉
Après lancement de notre conteneur, il démarre en… 47 ms ! Comme notre BDD a été lancée de manière externe il faut surcharger la valeur de configuration de son URL, ceci est possible directement depuis la ligne de commande via -Dmy.config=value

Étape 5 : lancement

Voici un exemple de lancement :

#Lancement d’un conteneur Postgres
docker run --ulimit memlock=-1:-1 -it --rm=true \
  --memory-swappiness=0 --name postgres \
  -e POSTGRES_USER=sarah -e POSTGRES_PASSWORD=connor \
  -e POSTGRES_DB=skynet -p 5432:5432 postgres:10.5
#Lancement de notre conteneur avec surcharge de l’URL vers la BDD
docker run -ti -p 8080:8080 --link postgres \
  loicmathieu/quarkus-demo \
  -Dquarkus.datasource.url=jdbc:postgresql://postgres:5432/mydatabase

Et le résultat :

WARN  [org.hib.eng.jdb.spi.SqlExceptionHelper] (main) SQL Warning Code: 0, SQLState: 00000
WARN  [org.hib.eng.jdb.spi.SqlExceptionHelper] (main) table "person" does not exist, skipping
INFO  [io.quarkus] (main) Quarkus 0.13.1 started in 0.047s. Listening on: http://0.0.0.0:8080
INFO  [io.quarkus] (main) Installed features: [agroal, cdi, hibernate-orm, jdbc-postgresql, narayana-jta, resteasy, resteasy-jsonb]

Quarkus : et la magie dans tout ça ?

Vert.X est magique, mais ça on en a déjà un peu parlé ! 🙂
La principale magie de Quarkus est son système d’injection de dépendances et de bootstraping d’application (rappelez-vous, pas de classe Application) à la compilation. Il n’y a pas encore beaucoup de documentation sur le sujet, mais la page du guide de développement d’extension donne un petit aperçu de sa philosophie (en trois phases) :

  • Augmentation : c’est fait via Maven via des BuildStep qui vont, en fonction de chaque extension (CDI, resteasy, etc.) générer le bytecode (le code compilé Java) nécessaire pour le boostraping de notre application. La phase d’augmentation va enregistrer des Record (suites de bytecode) qui seront ensuite exécutés dans les phases successives. Ces Record sont le résultat de l’exécution du code de boostraping de l’extension et pas un rejeu de celui-ci.
  • Static Init : initialisation statique du framework et de ses extensions. Ce qui a été enregistré comme Record(STATIC_INIT) dans la phase d’augmentation.
    Par exemple, si un framework se configure via XML on va charger dans cette phase le résultat du parsing du XML et de la configuration du framework via celui-ci (le parsing ne sera pas réalisé au lancement de l’application, car réalisé en phase de compilation). Cette phase s’exécute via le plugin de compilation Maven.
  • Runtime Init : l’initialisation de l’application au démarrage de celle-ci. Ce qui a été enregistré en phase d’augmentation en tant que Record(RUNTIME_INIT) s’exécutera ici.

C’est la responsabilité des extensions de permettre un maximum d’initialisations dans la phase de Static Init pour permettre aux applications de s’exécuter le plus rapidement possible.
En mode dev, toutes ces phases se font au démarrage pour permettre le livereload.
Attention : ceci est ma compréhension du mécanisme, en attendant plus d’informations cette description peut être en partie inexacte !!!

Pour aller plus loin :

Pour finir :

Quarkus vient avec déjà un bel écosystème pour un framework sorti il y a même pas deux mois, en plus de ce qu’on a déjà vu, on peut citer :

Chaque guide est réalisé avec un artefact Maven qui génère tout ce qu’il faut pour implémenter la fonctionnalité en question. L’apprentissage est donc très aisé.
Parmi les grands manquants on trouve  le support des BDD NoSQL telle que MongoDB et Elasticsearch, même si leur intégration manuelle ne pose pas spécialement de problème on aurait voulu quelque chose de packagé (entre autres pour la gestion des connexions et le health check). Mais j’ai su d’une source proche du projet que c’est déjà dans les cartons pour MongoDB et avec panache ! 🙂
Le code source complet de cet exemple peut être trouvé ici : https://github.com/loicmathieu/quarkus-demo

Auteur/Autrice

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :