Développement Web

Spring Boot 4 + GraalVM Native Image : compilation AOT en production

9 min de lecture

Spring Boot 4.0 GA est sorti le 20 novembre 2025, suivi par 4.0.6 en avril 2026 (la version stable courante à mi-mai 2026). Cette version majeure consolide l’intégration GraalVM Native Image : un projet Spring Boot peut désormais se compiler en exécutable natif via les plugins Maven/Gradle officiels sans configuration spéciale, démarre en moins de 100 ms et consomme 50-80 Mo de mémoire au lieu de 2-5 secondes et 200-500 Mo pour la JVM classique. Pour des services serverless (AWS Lambda, GCP Cloud Run, Azure Container Apps) ou des CLI Java, ce gain est transformateur. Ce tutoriel reprend la mise en place complète : ajustements code, hints, build, déploiement.

Prérequis

  • Java 25 LTS (cf. Virtual threads)
  • GraalVM 25+ ou Liberica GraalVM 25 (distribution Java + native-image)
  • Maven 3.9+ ou Gradle 8.10+
  • Une app Spring Boot 4.0+ fonctionnelle (cf. Première app Spring Boot)
  • Temps estimé : 90 minutes

Étape 1 — Installer GraalVM 25

Plusieurs distributions GraalVM existent : Oracle GraalVM (officielle, Community + Enterprise), Liberica GraalVM (BellSoft), Mandrel (Red Hat, Quarkus-aligned). Pour Spring Boot, Oracle GraalVM ou Liberica conviennent identiquement. L’outil SDKMAN simplifie l’installation et la commutation.

# Avec SDKMAN (recommandé)
curl -s "https://get.sdkman.io" | bash
source ~/.sdkman/bin/sdkman-init.sh

# Lister les distributions Java disponibles
sdk list java | grep grl

# Installer Oracle GraalVM 25
sdk install java 25-graal

# Vérifier
java -version
# openjdk 25 2025-09-16
# Java HotSpot(TM) 64-Bit Server VM GraalVM CE 25+...
native-image --version

native-image est l’outil clé : il compile le bytecode Java en exécutable natif à build-time, en utilisant l’analyse statique pour ne garder que les classes/méthodes effectivement atteignables. Le résultat : binaire 30-150 Mo selon l’application, démarrage instantané, plus de JVM nécessaire à l’exécution. Le coût : build long (1-5 min vs 10-30 s pour un jar classique) et certaines limitations sur la réflexion et le chargement dynamique.

Étape 2 — Configurer le plugin GraalVM dans Spring Boot

Le plugin org.graalvm.buildtools.native orchestre la compilation native. Spring Boot 4.0 l’intègre dans Spring Initializr (option « GraalVM Native Support »). Pour un projet existant, on ajoute manuellement dans pom.xml ou build.gradle.

<!-- pom.xml -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.6</version>
</parent>

<build>
    <plugins>
        <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <configuration>
                <buildArgs>
                    <arg>-O2</arg>
                    <arg>--no-fallback</arg>
                    <arg>-H:+ReportExceptionStackTraces</arg>
                </buildArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

Trois options structurantes. -O2 active l’optimisation niveau 2 (équilibre temps de build / perf runtime). Pour le développement, -Ob (build optimized) accélère le build au prix de la perf. --no-fallback force l’échec si l’analyse rencontre un problème non résoluble, plutôt que de produire un binaire dégradé (mode fallback qui embarque la JVM, défaisant l’intérêt). +ReportExceptionStackTraces conserve les traces d’erreurs lisibles.

Étape 3 — Builder l’exécutable natif

Deux commandes principales : mvn -Pnative native:compile (Maven) ou ./gradlew nativeCompile (Gradle). Le build crée un binaire dans target/ ou build/native/nativeCompile/.

# Build natif
./mvnw -Pnative native:compile

# Output progressif (5-10 minutes en moyenne) :
# [1/8] Initializing...                                     (3.5s @ 0.20GB)
# [2/8] Performing analysis... [* * * * *]                  (45s @ 0.94GB)
# [3/8] Building universe...                                (2.1s)
# [4/8] Parsing methods... [* *]                            (8s @ 1.20GB)
# [5/8] Inlining methods... [* *]                           (6s)
# [6/8] Compiling methods... [* * * *]                      (35s)
# [7/8] Layouting methods... [* *]                          (4s)
# [8/8] Creating image...                                   (3s)

# Tester
./target/mon-service
# Started MonServiceApplication in 0.072 seconds

# Mesurer la taille
ls -lh target/mon-service
# -rwxr-xr-x  1 user  staff   78M mai 18 10:30 target/mon-service

Le démarrage en < 100 ms remplace les 2-3 secondes JVM classiques. La taille (typiquement 60-150 Mo selon dépendances) reste raisonnable pour un conteneur. La consommation mémoire au repos chute à 30-80 Mo (vs 200-400 Mo JVM). Pour des fonctions serverless avec coût par GB-seconde, le gain peut atteindre 50-70 % de la facture.

Étape 4 — Hints de réflexion pour les bibliothèques non aware

GraalVM analyse statiquement le code pour déterminer quelles classes/méthodes sont accessibles. Cette analyse manque tout ce qui passe par réflexion dynamique : Class.forName(stringInconnu), accès JSON par champ via Jackson, sérialisation, proxies dynamiques. Pour les bibliothèques bien intégrées (Spring Boot, Hibernate 6.4+, Jackson récent), les hints sont auto-générés. Pour le code applicatif ou les bibliothèques tierces, on ajoute des reachability metadata.

// Approche moderne (Spring Boot 4) : @RegisterReflectionForBinding
@RegisterReflectionForBinding({UserDto.class, OrderDto.class})
@SpringBootApplication
public class MonApp { ... }

// Hints programmatiques
@Component
public class ReflectionHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        hints.reflection()
            .registerType(MaClasseLegacy.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
                MemberCategory.DECLARED_FIELDS);

        hints.resources().registerPattern("templates/*.html");
    }
}

// Activation
@SpringBootApplication
@ImportRuntimeHints(ReflectionHints.class)
public class MonApp { ... }

Pour les bibliothèques tierces qui ne fournissent pas leurs hints, le GraalVM Reachability Metadata Repository (github.com/oracle/graalvm-reachability-metadata) contient des metadata curated pour des centaines de bibliothèques courantes (Logback, Caffeine, HikariCP, …). Le plugin GraalVM les télécharge automatiquement à la build. C’est ce qui rend le travail de hints applicatif minimal en 2026.

Étape 5 — Compilation AOT et code généré Spring

Spring Boot 4 introduit une phase d’AOT (Ahead-of-Time) processing distincte de la compilation GraalVM. Cette phase analyse le contexte Spring à la build, génère du code Java statique pour remplacer la réflexion à l’exécution, et accélère le démarrage même sans GraalVM native.

# Build AOT processing (sans native)
./mvnw spring-boot:process-aot

# Lancer avec le runtime AOT
./mvnw spring-boot:run -Pnative

# Mesurer le démarrage
# - JVM classique : 2.5 s
# - JVM + AOT processing : 1.4 s
# - Native image : 0.07 s

Le code généré (dans target/spring-aot/) contient des @Configuration aplaties, des BeanFactory pré-câblées, et des hints de réflexion auto-déduits. C’est aussi cette phase qui détecte les incompatibilités avant la compilation native — gain de temps énorme par rapport à debugger un binaire natif qui crashe à l’exécution.

Étape 6 — Construire une image Docker native

Pour le déploiement, on conteneurise le binaire natif dans une image minimale (Alpine, distroless, ou scratch). Spring Boot fournit l’intégration Cloud Native Buildpacks qui produit l’image directement.

# Via Cloud Native Buildpacks (recommandé, zéro Dockerfile)
./mvnw -Pnative spring-boot:build-image

# L'image apparaît dans Docker local
docker images | grep mon-service
# mon-service  latest  abc123  3 minutes ago  178MB

# Dockerfile manuel équivalent
FROM ghcr.io/graalvm/graalvm-community:25 AS build
WORKDIR /app
COPY . .
RUN ./mvnw -Pnative native:compile -DskipTests

FROM debian:stable-slim
COPY --from=build /app/target/mon-service /app/mon-service
EXPOSE 8080
ENTRYPOINT ["/app/mon-service"]

Sur Cloud Run ou App Runner, le démarrage à froid d’une image Spring Boot native est typiquement de 200-400 ms, contre 5-15 secondes pour une image JVM équivalente. Pour les workloads avec scaling à zéro (scale-to-zero), cette différence change l’expérience utilisateur : premier appel à 300 ms vs 10 secondes.

Étape 7 — Limitations à connaître

GraalVM native impose des contraintes qu’on ne rencontre pas en JVM classique. Connaître ces limites évite de buter dessus en plein milieu d’une migration.

// Limitations principales :
// 1. Pas de réflexion dynamique sans hints
//    Class.forName("com.foo." + suffix) → ÉCHEC sauf si registerType

// 2. Pas de proxies dynamiques arbitraires
//    Spring AOP, mock-objects, MapStruct dynamic : OK car hints connus
//    Bibliothèque tierce sans hints : nécessite registerProxy

// 3. Pas de class loading dynamique
//    OSGi, plugins runtime via URLClassLoader : impossible

// 4. ServiceLoader doit être déclaré
//    Si une bibliothèque utilise META-INF/services/...,
//    les implémentations doivent être annotées ou listées dans hints

// 5. Pas de JIT runtime
//    Profile-Guided Optimization (PGO) compense en partie au build-time

Pour 95 % des applications Spring Boot classiques (REST, JPA, Kafka, Redis), ces limites ne posent aucun problème en 2026 grâce aux hints embarqués. Les 5 % restants sont les apps qui chargent du bytecode à l’exécution (Drools, Camel avec scripts inline, OSGi runtimes) — pour celles-ci, rester en JVM classique reste raisonnable.

Étape 8 — Profile-Guided Optimization (PGO)

PGO est la technique pour récupérer une partie de la perf du JIT en compilation native : on lance le binaire en mode instrumented, on collecte un profil d’exécution réel, et on recompile en utilisant ce profil. Le résultat : 20-40 % de perf en plus sur les chemins chauds par rapport à un binaire native sans PGO.

# Étape 1 : compiler avec instrumentation
./mvnw -Pnative native:compile -Dnative.image.args="--pgo-instrument"

# Étape 2 : lancer avec charge représentative
./target/mon-service &
ab -n 10000 -c 50 http://localhost:8080/api/products

# Le binaire écrit default.iprof à l'arrêt

# Étape 3 : recompiler avec le profil
./mvnw -Pnative native:compile -Dnative.image.args="--pgo=default.iprof"

# Le binaire final est ~20-40% plus rapide que sans PGO

Pour les services à débit élevé (10 000+ req/s) où le coût CPU domine, PGO restaure une grande partie du gap perf JIT vs AOT. La complexité du workflow CI (lancer une charge représentative entre deux builds) est le principal frein à l’adoption. Une stratégie viable : générer un profil mensuel sur staging, le commiter, l’utiliser pour les builds prod.

Erreurs fréquentes

Symptôme Cause Solution
« NoClassDefFoundError » à runtime native Réflexion non déclarée Ajouter hints via @RegisterReflectionForBinding ou RuntimeHints
Build natif consomme 12 Go RAM Phase analysis intensive Augmenter heap GraalVM : -J-Xmx12g
Image native qui crashe au démarrage Initialization à build-time d’un singleton avec ressource manquante Forcer initialization runtime : --initialize-at-run-time=com.foo
Cloud Native Buildpacks lent Téléchargement de la base à chaque build Cache Docker layer + builder local
Erreur ServiceLoader introuvable META-INF/services pas dans le metadata Ajouter --initialize-at-build-time=... ou registerType programmatique
Application beaucoup plus lente que JVM Pas de PGO + chemin chaud non vectorisé Activer PGO ou rester JVM pour ce service

Foire aux questions

Quand choisir native vs JVM ?
Native pour serverless (cold start critique), CLI, sidecars, microservices à scaling agressif. JVM pour services long-running avec besoins de débit max et hot-reload dev.

Compatibilité Hibernate ?
Hibernate 6.4+ supporte native. Les anciens 5.x nécessitent un travail de hints important — déconseillé.

Combien de temps économisé en cold start ?
Typiquement 95-98 % du cold start : 5 s JVM → 100 ms native. Sur AWS Lambda avec 100k invocations/jour, économies de coûts substantielles.

Quarkus ou Spring Boot native ?
Quarkus est plus mature côté native (architecture pensée native dès l’origine). Spring Boot native a rattrapé en 4.0. Pour un projet vert sans contraintes : Spring Boot pour l’écosystème, Quarkus pour native pur.

Coût mémoire à build ?
4-8 Go RAM nécessaires pour la phase analyse native. Sur des runners CI 2 Go, échec. Prévoir des runners dédiés ou des builds en cache.

Pour aller plus loin

Native compilation maîtrisée, les étapes suivantes sont JPA/Hibernate avancé pour la persistance et Spring Cloud Gateway pour exposer un ensemble de microservices natifs. Vue panoramique : Java Enterprise moderne.

Ressources et références

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité