liksi logo

Pourquoi passer à Spring Boot 3

Jean-François Chalony et Thomas Piscitelli - Publié le 16/01/2023
Pourquoi passer à Spring Boot 3

À chaque sortie d’une nouvelle version majeure d’un framework, nous sommes tiraillés entre l’envie de mettre à jour notre stack au plus vite afin de bénéficier des nouvelles features et la peur du coût engendré par cette mise à jour à cause des changements non rétrocompatibles et/ou la survenue d’éventuelles régressions. Dans cet article, nous allons étudier le bénéfice risque/gain de ce passage à Spring Boot 3 afin de pouvoir répondre à la question “Pourquoi passer à Spring Boot 3 ?”

Les nouveautés

Une bonne partie des nouveautés introduites par Spring Boot 3 ne sont pas directement liées à Spring Boot 3 mais au fait que cette version impose Java 17 en version minimale requise et Spring Framework 6. Passer à Spring Boot 3 permet donc de bénéficier des nouvelles features de ces nouvelles versions.

Nouveautés Java 17

  • Ajout des Sealed Classes
  • Nouvelle implémentation du générateur de nombre-pseudo aléatoire RandomGenerator
  • En expérimental, le Pattern Matching pour les switchs
  • L’impossibilité d’accès aux classes internes de la JVM
  • Suppression de l’AOT et JIT compiler
  • Pour les autres nouveautés ou détails c’est par ici

Nouveautés Spring Framework 6

  • Java 17
  • Migration Java EE à Jakarta EE ce qui implique un changement de package javax.* en jakarta.*
  • Compatibilité avec les dernières versions des serveurs d’application : Tomcat 10.1, Jetty 11, …
  • AOT (Ahead Of Time) qui permet dès le build d’optimiser l’application mais qui allonge le temps de build
  • Implémentation du ProblemDetails pour les APIs HTTP
  • Pour plus de détails, vous pouvez aller voir la release note

Nouveautés Springboot 3

L’objectif de cet article n’est pas de faire la liste exhaustive des nouveautés de Spring Boot 3 mais de détailler les plus importantes. Nous n’aborderons donc pas les nouveautés suivantes en détail mais elles sont à noter :

  • Java 17 obligatoire avec le support de java 19
  • Ajout d’une extension pour le logger Log4j2
  • Ajout/amélioration de l’autoconfiguration pour Micrometer
  • Pour aller plus loin dans les détails, c’est par ici

Les features qui donnent envie de passer à Spring Boot 3

Un des intérêts du passage à Spring Boot 3 est de nous permettre d’être à jour sur toutes les librairies tirées par Spring Boot. Cela semble évident mais ça reste important de le signifier car c’est toujours bon d’un point de vue faille de sécurité, correctifs de bugs et performance.

AOT et GraalVM

La “big feature” de Spring Boot 3 est de nous permettre la construction d’image native pour GraalVM afin de réduire au maximum l’empreinte mémoire de notre application et le temps de démarrage de celle-ci. En effet, on peut ainsi construire une image de l’application la plus légère possible en ne mettant que les classes nécessaires.

Cela semble alléchant sur le papier et nous allons donc tester cela et comparer à une version “classique” et une version AOT. Pour rappel, la compilation AOT (ou compilation anticipée) est une compilation qui traduit un langage évolué en langage machine avant l’exécution d’un programme. Elle s’oppose à la compilation à la volée (JIT) qui se fait lors de l’exécution du programme. La compilation AOT permet donc de compiler les classes Java en code natif avant de lancer la machine virtuelle.

Initialisation

Nous allons créer un petit projet pour comparer les performances apportées par AOT ou par GraalVM. Et identifier aussi les contraintes. Ce projet de test est composé d’une api REST avec BDD postgres et implémente un CRUD classique sur une seule ressource. Pour l’initialiser, Nous utilisons le starter spring ( https://start.spring.io/ ) en sélectionnant les options suivantes :

  • GraalVM Native Support
  • Spring Web
  • Spring Data JPA

Construction des différents packages

  • Construction du jar exécutable “classique”

    mvn clean package
    
  • Construction du jar exécutable avec AOT d’activé

    mvn clean spring-boot:process-aot package
    
  • Construction d’une image native

    mvn clean -Pnative package 
    

Comparaison

  • Le temps de démarrage :

    • classique : 3,2 secondes
    • AOT : 3,0 secondes
    • native : 140 millisecondes
  • L’empreinte mémoire

    • classique : 310 Mo
    • AOT : 300 Mo
    • native : 150 Mo
  • Le temps de build

    • classique : 5,7 secondes
    • AOT : 8,6 secondes
    • native : 3 min et 7 secondes

La compilation native répond bien à ses promesses : le temps de démarrage et l’empreinte mémoire sont très significativement réduit. Le coût se répercute sur le temps de build. Cette option est donc à éviter lors des builds sur les environnements de dev.

Les images natives ne sont pas forcément applicables à tous les besoins mais restent intéressantes dans certains cas comme pour une application avec une courte durée de vie où le temps de démarrage de l’application est important (lambdas, genre cli, dans un cluster où il y a une gestion dynamique des instances selon la charge).

Implémentation de ProblemDetails pour les APIs HTTP et clients HTTP déclaratifs

Ces fonctionnalités sont déjà présentées par Mathieu et Yoann dans l’article de retour sur le Devoxx Paris 2023. Josh Long les évoque dans sa conf sur Spring Boot 3

Avoir recours à ProblemDetails permet de standardiser son code et la gestion des erreurs ce qui est toujours une bonne pratique. Les clients HTTP déclaratifs simplifient la gestion des clients HTTP. Le fait que la notation ressemble beaucoup à celle du @RestController permet d’uniformiser encore plus son code et de simplifier sa lecture

Bonus Sprint Boot 3.1 #1 : Testcontainers

Testcontainers peut être utilisé pour les tests d’intégration. Si ce sujet vous intéresse, il existe un article de blog qui lui est dédié ici

Mais Testcontainers peut également être utilisé en mode développement.

Ce mode permet de lancer l’application en démarrant rapidement les services dont elle dépend (comme une base de donnée par exemple). Cela permet d’éviter de devoir les provisionner manuellement avant de lancer l’application.

C’est une alternative à un Docker Compose local. La configuration est alors faite directement dans le code, et non pas via un fichier de description docker compose.

Simplification de la configuration des informations de configuration dans Testcontainers

Une nouvelle annotation @ServiceConnection a été créé dans le module spring-boot-testcontainers.

Elle permet d’extraire les détails de connexion du container. Cette nouvelle annotation est la pratique recommandée mais si le container utilisé ne la supporte pas encore l’API permet toujours de les définir manuellement.

Avant Spring Boot 3.1, on devait avoir recours au DynamicPropertyRegistry afin de spécifier l’uri d’un container MongoDB

   @Container
   static MongoDBContainer mongodb =  new MongoDBContainer("mongo:5.0");

   @DynamicPropertySource
   static void registerMongoProperties(DynamicPropertyRegistry registry) {
      registry.add("spring.data.mongodb.uri", mongodb::getReplicaSetUrl);
   }

On voit bien que le recours à l’annotation @ServiceConnection simplifie ce code

    @Container
    @ServiceConnection
    static MongoDBContainer mongodb = new MongoDBContainer("mongo:5.0");

Déclaration des containers

Pour utiliser Testcontainers en mode développement, vous devez lancer votre application en utilisant le classpath “test” plutôt que “main”.

Cela vous permettra d’accéder à toutes les dépendances de test déclarées et vous offrira un emplacement naturel pour écrire votre configuration de test.

Les services dont dépendent l’application peuvent être déclarés comme des Container dans une interface

public interface MyContainers {

    @Container
    @ServiceConnection
    MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");

    @Container
    @ServiceConnection
    ElasticsearchContainer ES_CONTAINER = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.5.1");

}

Nous pouvons alors utiliser ces containers dans la classe déclarant la TestConfiguration à l’aide de l’annotation @ImportTestcontainers

@TestConfiguration
@ImportTestcontainers(MyContainers.class)
public class MyContainersConfiguration {

}

Il est également possible de déclarer ces containers directement dans la classe avec l’annotation @Bean

@TestConfiguration
public class MyContainersConfiguration {

    @Bean
    @ServiceConnection
    MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");

    @Bean
    @ServiceConnection
    ElasticsearchContainer ES_CONTAINER = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch-oss:7.5.1");
}

Déclaration de l’application exécutable en mode développement

Pour créer une version exécutable de votre application en mode développement, vous devez créer une classe “Application” dans le répertoire src/test.

Par exemple, si votre application principale se trouve dans src/main/java/com/example/MyApplication.java, vous devriez créer src/test/java/com/example/TestMyApplication.java.

La classe TestMyApplication peut utiliser la méthode SpringApplication.from(…​) pour lancer la véritable application

public class TestMyApplication {

    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main).with(MyContainersConfiguration.class).run(args);
    }

}

Exécution de l’application exécutable en mode développement

Un nouveau goal maven (spring-boot:test-run) et tâche Gradle (bootTestRun) peuvent être utilisés pour lancer l’application dans ce mode.

La gestion du cycle de vie de Testcontainers a été améliorée et les containers sont désormais correctement initialisés en premier et supprimés en dernier. Le support des containers réutilisable a également été amélioré.

À noter : le recours au module spring-boot-devtools permet de redémarrer automatiquement l’application quand le code source change.

Bonus Sprint Boot 3.1 #2 : Docker Compose

Si vous avez déjà configuré votre application pour qu’elle tourne en local via Docker Compose et que vous ne voulez donc pas utiliser Testcontainer en mode développement, un nouveau module spring-boot-docker-compose permet l’intégration avec Docker Compose.

Au démarrage de l’application, l’intégration Docker Compose cherchera automatiquement un fichier de configuration Docker Compose dans le répertoire courant.

Par défaut, la commande docker compose up est exécutée au lancement de l’application et les informations de connexions ajoutées au contexte applicatif. De la même façon la commande docker compose down est exécutée lorsque l’application s’arrête.

Le cycle de vie de docker compose et les commandes lancées au démarrage/stop de l’application sont bien entendu configurable.

Migration sur Spring Boot 3

Toutes ces nouvelles features donnent bien envie de passer à Spring Boot 3 mais pour quel coût ? Comme pour toutes migrations techniques, plus vous partez d’anciennes versions, plus le coût sera élevé.

Si vous avez été un bon élève, et que vous êtes déjà en version 2.7.x, il n’y a pas de grand obstacle notable.

  • Java 17 ou + est obligatoire, et Java 8 n’est plus supporté : c’est l’occasion de réaliser cette migration et de quitter enfin Java 8. En profiter pour passer directement à Java 21 apparait comme judicieux avec l’arrivée des virtuals threads supportés par Spring Boot 3.2 Passer sous une version de Java récente est une opération maligne avec l’arrivée prochaine de Loom en Java 20
  • Jakarta EE : Spring Boot 3.0 embarque la version Jakarta EE 10. Cela impactera certains de vos imports car des noms de package ont évolués et jakarta EE utilise désormais les packages jakarta au lieu de javax. Des outils existent pour faciliter cette migration mais Replace All dans votre code source fait globalement l’affaire.

Les possibles obstacles de migration étant dépendant de votre application, il est préférable de consulter le guide de migration avant de vous lancer.

Retour d’expérience

Avant de conclure cet article, nous avons demandé leur retour d’expérience à des collègues qui ont participé à faire cette migration sur l’application d’un client :

Nous avons eu à faire la montée de version Spring Boot 3 sur l’application d’un de nos clients il y a quelques mois. Pour mettre du contexte, cette application est composée de plusieurs microservices, gérés par des équipes différentes. La plupart de ces microservices partagent une librairie commune avec des composants de base, elle aussi à mettre à jour. Tous les microservices étaient en version Spring Boot 2.6.X au moment de la migration : pas la dernière version, mais une version encore supportée. Dernière chose à savoir : la migration a été décidée suite à un scan de sécurité avant release. Elle a donc été intégrée au planning sans planification préalable.

Nous avons rencontré plusieurs difficultés lors de cette migration. La première est que bien que les microservices étaient dans une version de Spring Boot assez récente, le minimum avaient été fait lors des montées de versions précédentes. Ils implémentaient aussi beaucoup de comportements custom déviant des starters de base. Nous faisions encore appel à beaucoup de classes ou de méthodes dépréciées, en se disant toujours “on verra ça plus tard”. Avec la montée de version vers Spring Boot 3, plus de marge de manœuvre… Beaucoup de classes et méthodes dépréciées ont été supprimées et une bonne partie de la base de code ne compilait plus. Malheureusement ce problème affectait aussi le code de tests. Nous n’avions donc aucune certitude sur la validité des correctifs apportés puisque les tests devaient être modifiés eux aussi.

Notre deuxième problème, bien plus lié à l’organisation du projet qu’à la montée de version en elle-même, a été la mise à jour de la librairie commune. À cause du manque de planification de cette mise à jour, les équipes se sont plusieurs fois retrouvées à faire des modifications concurrentes sur cette librairie pour permettre à leur microservice de compiler. Encore une fois cette problématique n’est pas directement liée à la montée de version de Spring Boot 3, mais cette mauvaise planification l’a énormément complexifié.

Pour conclure ce REX sur une note plus positive, notons que cette migration, malgré ses difficultés, nous a permis d’améliorer grandement la qualité du code de l’application, y compris de ses tests. Notons aussi que ces désagréments auraient pû être facilement évités avec plus de planification et plus d’assiduité dans les montées de versions.

Conclusion

La feature de génération des images natives pour GraalVM répond aux attentes et est un levier intéressant d’amélioration de l’empreinte mémoire et du temps de démarrage de votre application. Même si vous ne souhaitez pas le mettre en place sur votre application, les autres améliorations apportées par Spring Boot 3 restent intéressantes. Le travail réalisé sur l’intégration/fiabilisation de Testcontainers en mode développement ou de docker compose va dans le sens des efforts menés pour permettre de fluidifier et simplifier le développement.

Le passage à Spring Boot 3 permet de bénéficier des updates de tout l’éco-système de Spring/Java et d’éviter de prendre du retard dans la gestion de votre dette technique. Avec l’arrivée des threads virtuels dans Java 21 et son intégration à venir dans la future version de Spring Boot, cela semble une très bonne idée de se mettre en position de pouvoir monter facilement sur cette version qui pourra améliorer grandement le partage des ressources et la scalabilité de votre application.

Au regard de ces arguments et du coût qui apparait comme raisonnable (si bien planifié) de la mise à jour, le passage à Spring Boot 3 est définitivement une action à ajouter à votre roadmap technique.

Derniers articles