15
minutes
Mis à jour le
2/5/2025


Share this post

Découvrez le compte-rendu du Spring Meetup Paris de mars 2025 : optimisez vos tests Java avec JUnit 5 et boostez vos batchs Spring grâce à GraalVM Native Image. Des conseils pratiques, des exemples concrets et des retours d'expérience pour améliorer la qualité et les performances de vos applications

#
Spring Boot
#
Meetup & Conferences
#
Java
#
graalvm
#
junit
#
assertj
Samy Nalbandian
Software Engineer

Préambule

En tant que co-organisateurs du Spring Meetup j’ai surtout le privilège d’y participer et je vous partage aujourd’hui mes apprentissages de la session de Mars.

Pour rappel les Spring Meetups sont des événements communautaires organisés régulièrement pour les développeurs et passionnés de l'écosystème Spring. Ces rencontres permettent aux participants d'échanger, d'apprendre et de partager leurs connaissances autour des technologies Spring, Java et des bonnes pratiques de développement.

La première présentation était consacrée à l'optimisation des tests avec JUnit 5 par Juliette de Rancourt, tandis que la seconde explorait l'accélération des Spring Batch avec GraalVM et les images natives par Vincent Vauban.

Voici un compte-rendu détaillé de ces deux talks, avec des exemples concrets et des cas d'utilisation pratiques.

Partie 1 : Optimisez vos tests avec JUnit 5 : Concision et Évolutivité

Dans le monde du développement Java, les tests sont une partie essentielle de notre travail quotidien. Lors du Spring Meetup Paris, nous avons eu le plaisir d'assister à une présentation captivante sur l'optimisation des tests avec JUnit 5. Cette partie résume les points clés de cette présentation et vous propose des exemples concrets pour améliorer vos tests.

Introduction : Pourquoi améliorer nos tests ?

Le titre de la présentation était "Comment rendre ses tests concis et évolutifs grâce à JUnit 5". Mais que signifie réellement ce titre ?

  • Concis : Améliorer la lisibilité des tests pour qu'ils soient plus directs et focalisés sur le comportement à tester, sans être noyés dans un setup complexe.
  • Évolutifs : Faciliter l'écriture et la maintenance des tests, notamment en permettant la réutilisation de certains éléments.

L'objectif n'était pas d'apprendre à écrire des tests ou à faire du TDD, mais plutôt de refactoriser des tests existants pour les rendre plus efficaces.

Améliorer les assertions avec AssertJ

JUnit fournit des assertions basiques, mais elles ne sont pas toujours très lisibles ou pratiques à utiliser. Comparons :

AssertJ offre une syntaxe plus fluide et plus lisible, qui ressemble davantage à des phrases. De plus, les messages d'erreur sont beaucoup plus informatifs.

On peut même aller plus loin en créant nos propres assertions métier :

Cette approche permet d'avoir des assertions qui parlent le langage du métier, rendant les tests beaucoup plus expressifs.

Tests paramétrés

Les tests paramétrés permettent d'exécuter le même test avec différentes valeurs d'entrée. C'est particulièrement utile pour tester plusieurs cas sans dupliquer le code.

Exemple simple avec @ValueSource

Au lieu d'écrire deux tests séparés pour vérifier qu'un événement ne peut pas avoir une capacité de 0 ou négative, nous avons un seul test qui vérifie les deux cas.

Utilisation de @MethodSource

Pour des cas plus complexes, on peut utiliser @MethodSource :

Améliorer la lisibilité avec @DisplayName

Pour rendre les rapports de test plus lisibles, on peut personnaliser le nom affiché pour chaque cas de test :

Utilisation de @CsvSource

Pour tester des paires de valeurs :

Conversion automatique des paramètres

JUnit 5 peut convertir automatiquement les chaînes de caractères en différents types :

Pour les types personnalisés, on peut créer des convertisseurs :

Injection de paramètres avec ParameterResolver

Pour simplifier le setup des tests, on peut utiliser des ParameterResolver pour injecter des objets directement dans nos tests :

L'implémentation du resolver pourrait ressembler à ceci :

Cette approche permet de se concentrer sur ce qui est réellement testé, en éliminant le bruit du setup.

Tests d'interface pour éviter la duplication

Lorsqu'on a plusieurs implémentations d'une même interface (par exemple, des repositories pour différentes bases de données), on peut créer une interface de test :

Optimisation des tests Spring

Utiliser les annotations spécifiques

Au lieu d'utiliser @SpringBootTest pour tous les tests, on peut utiliser des annotations plus spécifiques qui chargent uniquement les composants nécessaires :

Améliorer les assertions pour les tests de contrôleurs

Pour les tests de contrôleurs, on peut utiliser les méthodes d'assertion de MockMvc :

Utilisation d'ApprovalTests pour les réponses JSON

Pour les réponses JSON complexes, on peut utiliser ApprovalTests :

mockMvc.perform(get("/events/1"))

Cette approche crée un fichier "approved" contenant la réponse attendue, et les tests futurs vérifieront que la réponse correspond toujours à ce fichier.

Tests de workflow avec ordre d'exécution

Pour tester un flux complet, on peut ordonner l'exécution des tests :

L'annotation @TestInstance(TestInstance.Lifecycle.PER_CLASS) est importante ici, car elle garantit que la même instance de classe de test est utilisée pour tous les tests, permettant ainsi de partager l'état entre eux.

Conclusion

JUnit 5 offre un large éventail d'outils pour rendre nos tests plus concis et évolutifs. En utilisant ces fonctionnalités, nous pouvons :

  1. Améliorer la lisibilité de nos tests avec des assertions expressives
  2. Réduire la duplication avec les tests paramétrés
  3. Simplifier le setup avec l'injection de paramètres
  4. Partager la logique de test entre différentes implémentations
  5. Optimiser les performances des tests Spring

Ces techniques nous permettent non seulement d'avoir une suite de tests plus maintenable, mais aussi d'améliorer l'expérience d'écriture des tests, les rendant plus agréables à créer et à maintenir.

N'hésitez pas à explorer la documentation de JUnit 5, qui est très complète et offre encore plus de possibilités que celles présentées ici. Comme l'a mentionné le présentateur, "la limitation est plus au niveau de notre imagination que du framework lui-même".

Partie 2 : Accélérez vos Spring Batch avec GraalVM et les images natives

Compte-rendu du deuxième talk du Spring Meetup Paris - Mars 2025

Le deuxième talk du Spring Meetup Paris nous a présenté comment optimiser les performances des applications Spring Batch grâce à GraalVM et aux images natives. Cette présentation a mis en lumière des gains de performance impressionnants, particulièrement adaptés aux tâches courtes et répétitives.

Le contexte : des batchs à optimiser

Le présentateur a commencé par expliquer le contexte de sa mission : optimiser des batchs qui chargeaient environ 8000 classes à chaque démarrage, entraînant une consommation excessive de mémoire et des pics de CPU problématiques. Avec une vingtaine de batchs utilisant le même template et s'exécutant plusieurs fois par jour, l'impact sur les coûts FinOps était significatif.

C'est dans ce contexte que l'utilisation de Spring Native, sorti en 2022 avec la possibilité d'utiliser GraalVM, est apparue comme une solution prometteuse.

Qu'est-ce qu'une image native ?

Une image native est un exécutable autonome qui contient tout ce dont l'application a besoin pour fonctionner, sans nécessiter de JVM séparée. Contrairement à l'approche traditionnelle où l'application Java s'exécute sur une JVM (comme HotSpot VM), une image native intègre directement le code machine compilé.

Voici les principales différences entre une application Java traditionnelle et une image native :

Application Java traditionnelle :

  • Application code
  • Spring Framework
  • Spring Boot
  • Bytecode
  • JVM (HotSpot VM)

Image native :

  • Application code
  • Spring Framework
  • Spring Boot
  • Native Machine Code
  • Heap (où toutes les classes sont chargées au moment du build)
  • Substrate VM (remplace la JVM traditionnelle)

Les avantages des images natives

Les images natives offrent plusieurs avantages significatifs :

  1. Démarrage ultra-rapide : jusqu'à 40 fois plus rapide qu'une application Java traditionnelle
  2. Consommation réduite de ressources : moins de CPU et de mémoire utilisés
  3. Images plus légères : pas besoin d'inclure la JVM
  4. Performance maximale dès le démarrage : pas besoin d'attendre l'optimisation du JIT compiler
  5. Temps d'exécution homogènes : les performances sont stables à chaque exécution

Ces avantages sont particulièrement intéressants pour les applications qui démarrent fréquemment et s'exécutent pendant une courte durée, comme les batchs.

GraalVM : une machine virtuelle universelle

GraalVM est une machine virtuelle universelle qui permet d'exécuter des applications écrites dans différents langages :

  • Java et autres langages JVM
  • JavaScript
  • Python
  • Et d'autres langages via le framework Truffle Language Implementation

Cette polyvalence en fait un outil puissant, mais c'est surtout sa capacité à compiler des applications Java en images natives qui nous intéresse ici.

Le processus de compilation avec GraalVM

La compilation d'une application Java en image native avec GraalVM suit un processus différent de la compilation Java traditionnelle :

  1. Compilation traditionnelle :
    • Code source Java → javac → bytecode → JVM (compilation JIT pendant l'exécution)
  2. Compilation avec GraalVM :
    • Code source Java → javac → bytecode
    • Analyse AOT (Ahead-of-Time) → bytecode AOT + configuration native
    • GraalVM native-image → exécutable natif (.exe sur Windows, ELF sur Linux)

La différence principale réside dans l'étape de compilation AOT (Ahead-of-Time) qui permet de générer du code machine avant l'exécution, plutôt que de laisser la JVM le faire à la volée.

Mise en pratique : création d'une application Spring Batch

Pour démontrer les avantages des images natives, le présentateur a créé une application Spring Batch simple de facturation avec les éléments suivants :

  1. Un objet métier Billing avec des propriétés comme ID, numéro de téléphone, année, mois, montant, etc.
  2. Un job Spring Batch composé d'un step avec trois tâches :
    • Un reader qui lit les données depuis un fichier CSV
    • Un processor qui traite les données (dans ce cas, affiche simplement les informations)
    • Un writer qui écrit les données dans une base de données PostgreSQL

Le code ressemblait à ceci :

Compilation en image native

Pour compiler l'application en image native, il a fallu ajouter la dépendance spring-native au projet :

Puis exécuter la commande de compilation :

Le processus de compilation peut prendre jusqu'à 10 minutes, ce qui est l'un des inconvénients de cette approche.

Résultats impressionnants

Les résultats de la démonstration ont été frappants :

  • Application Java traditionnelle : temps de démarrage d'environ 8,74 secondes
  • Image native : temps de démarrage d'environ 0,989 secondes

Soit une amélioration d'environ 9 fois (et jusqu'à 40 fois dans certains cas, selon le présentateur).

Des benchmarks réalisés par Oracle confirment ces résultats, montrant que les images natives peuvent être jusqu'à 4 fois plus rapides que les applications Java traditionnelles pour certains workloads.

En termes de ressources, les images natives consomment également moins de mémoire et de CPU, ce qui se traduit par des économies significatives en environnement cloud.

Les limites des images natives

Malgré leurs avantages, les images natives présentent certaines limitations :

  1. Complexité de build : le processus de compilation est plus complexe et nécessite des outils supplémentaires (comme Visual Studio C++ sur Windows)
  2. Temps de compilation long : jusqu'à 10-15 minutes pour compiler une application
  3. Maturité limitée : moins d'outils de diagnostic et de monitoring que pour les JVM traditionnelles
  4. Problèmes de parité entre environnements : risque de différences entre les environnements de développement et de production
  5. Limitations avec la réflexion et les proxies : certaines fonctionnalités Java qui reposent sur le chargement dynamique de classes peuvent poser problème

Le principal défi technique vient du fait que toutes les classes doivent être connues au moment de la compilation. Les frameworks qui utilisent beaucoup la réflexion ou le chargement dynamique de classes (comme certains aspects de Spring) peuvent nécessiter une configuration spécifique.

Cas d'utilisation idéaux

Les images natives sont particulièrement adaptées pour :

  • Les tâches courtes qui démarrent fréquemment (comme les batchs)
  • Les microservices avec une durée de vie courte
  • Les applications serverless ou à la demande
  • Les applications où le temps de démarrage est critique

En revanche, pour les applications à longue durée d'exécution, les avantages sont moins évidents, car la JVM traditionnelle avec son compilateur JIT peut atteindre des performances similaires après la phase d'optimisation.

Conclusion

L'utilisation de GraalVM et des images natives avec Spring Batch offre des avantages significatifs en termes de performance et d'utilisation des ressources, particulièrement pour les tâches courtes et répétitives.

Dans le cas présenté, cette approche a permis de résoudre efficacement le problème des 8000 classes chargées à chaque démarrage, réduisant ainsi la consommation de mémoire et les pics de CPU.

Bien que cette technologie présente encore certaines limitations et complexités, elle constitue une option très intéressante pour optimiser les applications Spring Batch et réduire les coûts FinOps dans des environnements cloud.

Comme l'a souligné le présentateur, cette approche est particulièrement pertinente pour les workloads qui démarrent fréquemment et s'exécutent pendant une courte durée, tandis que la JVM traditionnelle reste préférable pour les applications à longue durée d'exécution.

Pour en savoir plus n’hésitez pas à vous rendre sur le super site de FooJay pour avoir un exemple plus complet ou encore sur la vidéo youtube du talk

Conclusion générale

Ces deux présentations du Spring Meetup Paris de mars 2025 nous ont offert des perspectives précieuses sur l'optimisation de nos applications Java, que ce soit au niveau des tests ou des performances d'exécution.

Les Spring Meetups constituent une excellente opportunité pour rester à jour avec les dernières évolutions de l'écosystème Spring et Java, tout en échangeant avec d'autres professionnels partageant les mêmes intérêts. Ces événements sont organisés régulièrement dans différentes villes à travers le monde.

Si vous souhaitez participer aux prochains Spring Meetups ou en savoir plus sur les événements passés et à venir :

N'hésitez pas à rejoindre la communauté Spring et à participer aux prochains meetups pour continuer à enrichir vos connaissances et partager votre expertise !