Quarkus est un framework Java open source permettant le développement d’applications Java cloud natives et fonctionnant avec les standards Java les plus couramment utilisées (JPA, Hibernate, RESTEasy, JAX-RS, Apache Camel, …). Il fonctionne nativement avec Kubernetes, OpenJDK, HotSpot et GraalVM, un JDK hautes performances concu pour accélerer l’execution d’applications Java (entre autres).
Lorsque Quarkus et utilisé avec Open JDK, il offre un temps de démarrage et une consommation mémoire plus faible qu’avec, par exemple, Spring Boot. Cependant, Quarkus dévoile tout son potentiel d’optimisation lorsqu’il est utilisé avec GraalVM et notamment GraalVM Native Image. Cette fonctionnalité offerte par GraalVM, permet de créer une application standalone optimisée.
Au travers de cet article, vous allez découvrir l’utilisation de Quarkus avec GraalVM, les points forts, les fonctionnalités disponibles mais aussi quelques limitations de la solution.
La norme Microprofile
Quarkus se base sur la norme Microprofile 4, issue du monde JavaEE (JakartaEE). Elle définit un ensemble de standards nécessaires à la mise en place d’applications Java dans une architecture Microservice. On retrouve par exemple :
- Config : gestion de la configuration
- Fault tolérance : mise en place de coupe-circuit pour une architecture microservice résiliente
- Health : supervision d’indicateur sur l’état de santé de l’application
- JWT RBAC : prise en charge de l’authentification OAuth 2
- Metrics : indicateurs systèmes et fonctionnels de l’application
- Open API : définition et documentation d’interface REST
- Open tracing : monitoring et traçage des appels entre microservices
- Rest client : définir le webservice REST
En voyant cette liste, vous pensez très probablement à Spring Boot.
Quarkus VS Sprint Boot : Pourquoi Quarkus ?
Spring Boot est très établi dans le paysage des microservices Java, il est donc légitime de se poser la question : pourquoi choisir Quarkus ? Quarkus est conçu pour répondre à la problématique de la conception d’application cloud native optimisée. L’équipe de développement de Quarkus a fait le choix de compiler les applications en code natif via GraalVM Native Image. En effet, cette technologie permet de compiler à l’avance le code Java vers un exécutable autonome, appelé image native. Le programme qui en résulte a un temps de démarrage plus rapide et une surcharge de mémoire d’exécution plus faible par rapport à une JVM Cette approche permet d’obtenir des résultats impressionnants :
- un temps de démarrage extrêmement rapide, 17ms pour une application REST basique
- une empreinte mémoire réduite, 24Mo pour une application de base
Les performances à l’exécution restent cependant identiques à celles d’une application Java classique. Les performances de consommation de mémoire et de démarrage sont dues à :
- L’absence de classloading
- L’absence de code interprété
- L’absence de JIT et CPU Profiling
- La génération d’une image de la Heap durant le build
- L’absence de métadonnée sur les classes chargées
Voici un aperçu des résultats obtenus en local :
Cependant, comme nous le verrons en fin d’article, GraalVM Native Image vient avec quelques limitations.
Un framework extensible
Quarkus a une autre grande force, il est extensible et de nombreuses extensions existent. Certains éditeurs se sont spécialisés dans la production de celles-ci, à l’instar de SmallRye.
Ces extensions permettent d’ajouter des fonctionnalités à Quarkus ou de supporter certains protocoles et outils fréquemment utilisés dans les applications Java:
- Spring Data JPA
- Spring Cloud Config
- Vert-X
- Kafka
- Flyway
- SmallRye OpenAPI
- Des dizaines d’autres…
Si vous en avez le besoin, il est également possible de créer vos propres extensions.
Doit-on migrer vers Quarkus ?
L’architecture microservice a une grande force, le langage/framework utilisé n’a que peu d’importance tant que les interactions sont normées et que les microservices sont compatibles avec cette norme. Il n’y aura donc aucun souci à faire cohabiter des microservices Spring Boot et Quarkus
Vous pouvez développer tout ou partie de vos nouveaux microservices avec Quarkus afin de bénéficier de ses qualités. Par exemple, un service qui doit être particulièrement scalable profitera du temps de démarrage très faible. Même s’il peut paraitre intéressant de migrer vos microservices existants, cette tâche peut s’avérer longue. Prenons l’exemple d’un microservice Spring Boot, il faudra :
- Trouver les composants équivalents à ceux utilisés avec Spring Boot
- Vérifier que ces mêmes composants fonctionnent correctement avec GraalVM Native Image
- Adapter le code afin d’utiliser les nouveaux plugins Quarkus
Utilisation de Quarkus
Il est temps de passer à l’expérimentation. Comme vous le verrez, l’utilisation de Quarkus ne vous dépaysera pas si vous êtes, par exemple, un habitué de Spring Boot.
Installer GraalVM pour Quarkus
Quarkus aura besoin de GraalVM pour compiler l’application en code natif. Il est donc nécessaire de l’installer avant de commencer sur Quarkus. Télécharger GraalVM depuis le site https://www.graalvm.org/ Pour MacOS:
- Il est nécessaire de déplacer graalVM dans les machines virtuelles de la JVM :
sudo mv graalvm-ce-java<version>-<version> /Library/Java/JavaVirtualMachines
- GraalVM doit également être ajouté dans le Path de la machine :
export JAVA_HOME=/Library/Java/JavaVirtualMachines/<graalvm>/Contents/Home)
- GraalVM a également besoin d’une image native pour le système :
gu install native-image
- Pour compiler Quarkus en application native il suffit de lancer maven avec le profile native :
mvn clean package -Pnative
Pour Windows:
- Dezipper GraalVM dans un répertoire par exemple :
C:graalvm-ce-java<version>-<version>
- Ajouter la variable d’environnement
GRAALVM_HOME
=C:graalvm-ce-java<version>-<version>
- Installer la native-image avec la commande
C:graalvm-ce-java<version>-<version>bingu install native-image
- Installer le support pour la compilation native en C avec Visual Studio 2017 Visual C++ Build Tools téléchargeable via ce lien
- Suivant le type d’architecture que vous avez ( x86, x64, ARM, and ARM64), il faut lancer un .bat pour parametrer le type de compilateur. Exemple pour x64:
C:Program Files (x86)Microsoft Visual Studio2017BuildToolsVCAuxiliaryBuildvcvars64.bat
. Vous trouverez plus d’informations via ce lien.
Pour compiler Quarkus en application native il suffit de lancer maven avec le profile native : mvn clean package -Pnative
Définir une API Rest avec Quarkus
Si vous utilisez d’ores et déjà Spring Boot pour développer des microservices, vous ne serez pas dépaysé. Prenons l’exemple avec la définition d’une API REST.
Le fonctionnement est similaire que ce soit pour :
- L’injection de dépendance avec CDI (Context Dependency Injection), équivalent de Spring Core
- La définition des requêtes HTTPs par annotation
Bien que CDI et Spring soient différents, le passage d’une technologie à l’autre est relativement indolore puisque les frameworks utilisent des concepts proches. Vous définissez votre ressource avec l’annotation @Path
et le verbe HTTP par annotation (@GET, @PUT …) sur chaque méthode. Voici un exemple :
Quarkus propose aussi une solution pour mettre en place des tests d’intégrations avec le module maven quarkus-junit5. Ce dernier propose l’annotation @QuarkusTest nous permettant d’exécuter un test d’intégration. Si vous êtes habitué à SpringBoot, cela ressemble à l’utilisation de @SpringBootTest.
Persistance de données
Vous ne serez pas perdu avec Quarkus, la persistance se fait au travers de JPA. Cette norme est très répandue dans bon nombre d’applications. Nous sommes très habitués à l’utiliser dans le développement d’applications et d’outils Java.
Logging
Quarkus est compatible avec les principaux frameworks de logging du marché :
- java.util.logging
- Jboss Logging
- SLF4J
- Log4J
- Appache common logging
En interne, Quarkus utilise le Jboss Logging, par conséquent, vous pouvez l’utiliser sans ajouter de dépendance. Dans le cas contraire, il sera nécessaire d’ajouter une dépendance. Pour de plus amples informations à ce sujet, je vous invite à lire la documentation.
Pour le tracing, il vous sera probablement nécessaire d’encoder les logs en JSON, il existe l’extension quarkus-logging-json
pour répondre à cette problématique :
Pour l’activer, vous devez ajouter une propriété dans le fichier src/main/resources/application.properties
:
Par défaut, Quarkus n’intègre pas de traceId pour le tracking des requêtes. Il existe un module dédié à cette tâche. Comme pour l’encodage en JSON, il suffira d’ajouter la dépendance Maven :
La librairie quarkus-smallrye-opentracing est bien fournie et permet d’envoyer les informations de tracking sur plusieurs canaux différents (base de données, mongoDb, kafka, zipkin…). Il est cependant à noter que la seule l’utilisation d’un traceId n’est pas suffisante pour monitorer correctement une application. Il est nécessaire d’avoir des identifiants supplémentaires pour mieux suivre le parcours utilisateur. https://quarkus.io/guides/opentracing
Filtre HTTP
Les filtres HTTP via annotation sont des éléments indispensables afin d’implémenter certaines fonctions transversales d’une application web telles que la sécurité ou le monitoring.
Bien que Quarkus se base sur une stack JavaEE, les annotations @WebFilter ne sont pas incluses dans le Framework. On retrouve cette fonctionnalité dans l’implémentation Quarkus de RestEasy Reactive. Cette implémentation propose deux annotations pour effectuer cette action :
- ServerRequestFilter : pour le filtrage de requêtes entrantes
- ServerResponseFilter : afin de filtrer les réponses du serveur
Pour cela, il faut importer :
Dans l’exemple suivant, nous allons voir l’utilisation des deux annotations.
Dans notre exemple, la première annotation nous permet de filtrer les requêtes entrantes et d’extraire ou créer les informations de tracking. Le device identifier permet d’avoir un identifiant unique par client et le correlation id permet d’avoir un identifiant par session fonctionnelle. Toujours dans cet exemple, la seconde annotation permet de modifier le header de la réponse pour y ajouter le correlation id.
Concepts avancés et limitations de GraalVM Native Image
GraalVM Native Image permet une optimisation de l’application sur deux points principaux :
- Le temps de lancement
- Une empreinte mémoire faible
Cependant, ces optimisations ont un coût et certaines fonctionnalités d’un JDK classique ne sont pas utilisables.
Contributions SPI
Les interfaces SPI sont au cœur de beaucoup de frameworks (CDI, JPA, CXF, …). Cette fonctionnalité permet de définir une interface et de déclarer les implémentations dans un fichier portant le nom de l’interface stocké dans META-INF/services. Dès lors, il est possible de charger les implémentations via le service loader. Par exemple si on définit une interface Obfuscator du type :
Il est possible de la déclarer dans le fichier /META-INF/services/com.smartwave.quarkus.spi.obfuscators.Obfuscator
Pour charger l’extension il suffit d’utiliser le service loader :
Ce code fonctionne sans problème sur une JVM classique. Pourtant ce type d’usage ne sera pas fonctionnel sur Quarkus avec GraalVM Native Image. En effet, une des limitations de GraalVM Native Image est l’absence de classloading.
Alors que le même code fonctionnera en mode Developpement (sans GraalVM Native Image):
Ce détail révèle une information importante : il ne faut pas avoir une confiance aveugle dans le mode développement lorsque vous avez pour objectif de générer votre livrable avec GraalVM Native Image. Seuls les tests avec la version native peuvent nous garantir le bon fonctionnement de l’application.
Proxification
Le proxy est un des design patterns important. On le retrouve dans tous les ORM, frameworks d’IOC et d’injection de dépendances tels que Spring ou CDI. En Java il y a deux façons de créer des proxies :
- en utilisant les proxies Java. Ces proxies sont cependant réalisables uniquement sur des interfaces. Nous ne rentrerons pas dans le détail de cette méthode.
- en utilisant une librairie tel que CGlib ou Byte buddy. La plupart des frameworks sont basés sur CGlib (Spring, Hibbernate ou Weld CDI pour ne citer qu’eux).
Proxification CGlib
CGlib permet la création de proxy sur des objets réels. L’utilisation est très simple, il suffit de définir un Enhancer en lui donnant la classe à proxifier et un handler pour traiter l’interception.
Ici GraalVM refusera de compiler l’image native. Une erreur est affichée dès la compilation:
L’utilisation de CGLib n’est pas possible avec GraalVM Native Image.
Comment contourner cela ?
L’extension Hibernate pour Quarkus utilise ByteBuddy pour effectuer sa proxification. Cette approche est donc à privilégier, bien que son utilisation soit beaucoup moins intuitive et répandue que CGLIB (référence : ProxyBuildingHelper.java)
Si vous souhaitez en savoir plus sur les limitations de Quarkus, je vous invite à consulter cet article plus complet sur le sujet.
Conclusion
Quarkus avec GraalVM Native Image permet d’optimiser grandement le temps de démarrage et la consommation mémoire. Aujourd’hui, beaucoup de solutions ont été trouvées pour faire fonctionner des frameworks très variés (CDI, JPA, Jax-RS, etc. … ) et la grande majorité des fonctionnalités natives Java sont fonctionnelles. Il existe toutefois encore certaines limitations.
La conception d’application avec Quarkus demande de porter une attention particulière sur les librairies incluses. L’ajout de certaines librairies externes peut en effet aboutir à des erreurs lors de la génération ou l’exécution de l’image.
Globalement, il est préférable d’utiliser Quarkus et GraalVM Native Image pour de nouveaux microservices qui nécessitent une optimisation des ressources. La migration d’applications existantes sera couteuse et le coût sera probablement supérieur aux bénéfices retirés dans une grande majorité des cas.