Compiler une application Spring en natif avec GraalVM
Photo de SpaceX sur Unsplash

Avec la sortie cette semaine de Spring Native Beta en version 0.9.0, il est intéressant de faire un état des lieux de la compilation d’applications Spring en exécutables natifs à l’aide de GraalVM et de son mode native-image.

L’exécution d’une application en code natif a, en effet, de nombreux intérêts comparée à celle en Bytecode dans une JVM :

  • Le dĂ©marrage est instantanĂ©
  • La performance est optimale dès le dĂ©marrage
  • La consommation de la mĂ©moire est fortement rĂ©duite

La version de Spring Native est, toutefois, en Beta ce qui signifie que tous les composants de Spring ne sont pas encore fonctionnels en mode natif. Voyons en détails son fonctionnement.



Configuration requise de base

Tout d’abord, vous devrez installer GraalVM et ensuite son compilateur en code natif native-image :


Génération du squelette d’application

L’arrivée de la version Beta implique que Spring Native est désormais supporté par Spring Initializr, une interface web qui permet de composer son application Spring puis de générer son squelette.

Utilisons-la pour définir notre application démo :

  • Renseignez les mĂ©tadonnĂ©es du projet
  • SĂ©lectionnez la dĂ©pendance Spring Native [Experimental] pour bĂ©nĂ©ficier de la compilation native
  • Ajoutez la dĂ©pendance Spring Web dans le cadre de cette dĂ©mo
  • TĂ©lĂ©chargez le code gĂ©nĂ©rĂ© en cliquant sur le bouton Generate
Interface Spring Initializr pour l'application démo
Interface Spring Initializr pour l'application démo

Modules Spring Native

Vous trouverez, dans le POM, la liste de modules Spring configurés en tant que dépendances Maven :

  • La dĂ©pendance Spring Native et sa version :
<properties>
<java.version>11</java.version>
<spring-native.version>0.9.1-SNAPSHOT</spring-native.version>
</properties>
.../...
<dependency>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-native</artifactId>
  <version>${spring-native.version}</version>
</dependency>
  • Le plugin Spring Boot Maven et sa configuration pour exĂ©cuter le build d’une image native dans un conteneur Buildpacks :
<plugin>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-maven-plugin</artifactId>
	<configuration>
	  <image>
	    <builder>paketobuildpacks/builder:tiny</builder>
	    <env>
	      <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
	    </env>
	  </image>
	</configuration>
</plugin>
  • Le plugin AOT Maven qui sert Ă  configurer Spring pour sa compilation Ahead-Of-Time ainsi qu’à gĂ©nĂ©rer du code pour la configuration et le classpath de l’application :
<plugin>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-aot-maven-plugin</artifactId>
  <version>${spring-native.version}</version>
  <executions>
    <execution>
      <id>test-generate</id>
      <goals>
        <goal>test-generate</goal>
      </goals>
    </execution>
    <execution>
      <id>generate</id>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Remarques

Dépendances non supportées

Au cas où vous sélectionneriez une dépendance Spring non encore supportée dans le mode natif, le fichier HELP.md contiendra un avertissement :

Avertissement dans le fichier HELP.md
Avertissement dans le fichier HELP.md

Dépendances supportées

  • Dans le cas des dĂ©pendances supportĂ©es par Spring, l’initializr va configurer tous les plugins nĂ©cessaires pour que le build et l’exĂ©cution de l’application Spring fonctionnent out-of-the-box !

Dans l’exemple de Spring Data JPA, Maven sera configuré pour que les classes Hibernate soient compilées au moment du build de l’application et non pas lors de son runtime comme c’est le cas pour une JVM :

<plugin>
  <groupId>org.hibernate.orm.tooling</groupId>
  <artifactId>hibernate-enhance-maven-plugin</artifactId>
  <version>${hibernate.version}</version>
  <executions>
    <execution>
      <id>enhance</id>
      <goals>
        <goal>enhance</goal>
      </goals>
      <configuration>
        <failOnError>true</failOnError>
        <enableLazyInitialization>true</enableLazyInitialization>
        <enableDirtyTracking>true</enableDirtyTracking>
        <enableAssociationManagement>true</enableAssociationManagement>
        <enableExtendedEnhancement>false</enableExtendedEnhancement>
      </configuration>
    </execution>
  </executions>
</plugin>

Tout cela est très rassurant ! J’avais testé auparavant la version 0.7.1 de Spring Native (nommé spring-graalvm-native à l’époque) et il y avait alors beaucoup de modifications manuelles à apporter.

But affiché de l'équipe en charge de Spring Native

  • Fournir une configuration automatiquement afin qu’il n’y ait pas besoin de modifier le code Java, que l’application soit exĂ©cutĂ©e en mode natif ou dans une JVM.
  • Faire en sorte que les tests unitaires s’exĂ©cutent de la mĂŞme façon dans une image native ou dans une JVM.
  • RĂ©duire encore plus la taille de l’image native gĂ©nĂ©rĂ©e dans la prochaine version 0.10 de Spring Native.

Ajout d’un Controller Web

  • DĂ©zippez le fichier gĂ©nĂ©rĂ© par Spring Initializr et ouvrez le rĂ©pertoire avec votre IDE prĂ©fĂ©rĂ©.

  • CrĂ©ez un nouveau Controller Ă  la racine du package de votre projet avec le code ci-dessous :

package io.scalastic.demo.demo_spring_native;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoSpringNativeController {
@GetMapping("/")
public String hello() {
return "Hello!";
}
}
Le projet et son Controller dans IntelliJ IDEA
Le projet et son Controller dans IntelliJ IDEA

Compilation en code natif

Il existe deux façons de compiler une application Spring en code natif :

  • En utilisant le Buildpack Spring Boot intĂ©grĂ© Ă  Spring et qui va produire un conteneur lĂ©ger contenant le code natif de l’application
  • En utilisant le plugin Maven native-image-maven-plugin qui va produire un exĂ©cutable natif

Remarque

La configuration Maven générée par Spring Initializr fait le choix de Buildpacks :

  • Nous n’aborderons par consĂ©quent que cet aspect dans cet article.
  • Nous verrons le build natif Ă  l’aide du plugin Maven native-image qui nĂ©cessite des modifications importantes du POM, dans un prochain article.

Utilisation du Buildpack Spring Boot

Cette procédure permet d’obtenir un conteneur Docker qui contient l’application compilée en code natif. Il est léger et peut être déployé directement dans un orchestrateur de conteneurs.

Pré-requis

Docker doit être installé afin de pouvoir lancer le Buildpack Spring Boot. C’est un conteneur qui contient tout le nécessaire pour builder une application Spring en code natif.

  • Vous pouvez installer Docker Ă  partir de Docker Installation
  • Pour MacOS, il est recommandĂ© d’allouer au moins 8Go de mĂ©moire Ă  Docker
  • Pour Windows, il faut activer Docker WSL 2 Backend pour avoir de meilleures performances

Compilation en mode natif avec Buildpacks

  • L’application native peut ĂŞtre compilĂ©e en lançant la commande suivante :
% mvn spring-boot:build-image
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------< io.scalastic.demo:demo_spring_native >----------------
[INFO] Building demo_spring_native 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] >>> spring-boot-maven-plugin:2.4.4:build-image (default-cli) > package @ demo_spring_native >>>
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ demo_spring_native ---

[.../...]

[INFO] Successfully built image 'docker.io/library/demo_spring_native:0.0.1-SNAPSHOT'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  03:03 min
[INFO] Finished at: 2021-03-21T20:57:29+01:00
[INFO] ------------------------------------------------------------------------

Process finished with exit code 0

Cette commande va créer, en local, un conteneur Linux pour compiler l’application native à partir du compilateur native-image de GraalVM.

  • Regardons les images prĂ©sentes, dans le registre Docker local et qui viennent d’être mises en oeuvre dans ce build :
% docker images
REPOSITORY                 TAG              IMAGE ID       CREATED        SIZE
paketobuildpacks/run       tiny-cnb         e85a0fe734d7   17 hours ago   17.3MB
paketobuildpacks/builder   tiny             1cbb20e3de7e   41 years ago   401MB
demo_spring_native         0.0.1-SNAPSHOT   a423116a12a8   41 years ago   81.9MB

On constate que ce processus produit 3 images Docker :

  • paketobuildpacks/run:tiny-cnb : Le runner basĂ© sur distroless bionic + glibc + openssl + CA certs pour exĂ©cuter une application en code natif. C’est le conteneur de base servant Ă  encapsuler une application en code natif.
  • paketobuildpacks/builder:tiny : Le builder basĂ© sur une stack distroless ubuntu:bionic + openssl + CA certs + compilers + shell utilities. C’est un Buildpack servant Ă  compiler la plupart des applications en Go et les applications Java en code natif avec GraalVM.
  • demo_spring_native:0.0.1-SNAPSHOT : L’application, en code natif, encapsulĂ©e dans un runner de base distroless.

Pour aller plus loin

  • Les images issues du Buildpack datent de 1980, du 1er janvier 1980 exactement ! C’est tout Ă  fait voulu et l’explication se trouve lĂ  : Time Travel with Pack
  • Les stacks Distroless sont des images minimalistes, dĂ©veloppĂ©es par Google et qui amĂ©liorent la sĂ©curitĂ© et la taille des conteneurs en diminuant la surface des attaques et le nombre de composants qu’elles intègrent.
  • La notion de Runner et Builder dans les Buildpacks.

Exécution de l’application

  • Pour dĂ©marrer l’application issue du Buildpack, tapez la commande suivante:
% docker run -p 8080:8080 docker.io/library/demo_spring_native:0.0.1-SNAPSHOT
2021-03-21 19:32:54.188  INFO 1 --- [           main] o.s.nativex.NativeListener               : This application is bootstrapped with code generated with Spring AOT

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.4)

2021-03-21 19:32:54.190  INFO 1 --- [           main] i.s.d.d.DemoSpringNativeApplication      : Starting DemoSpringNativeApplication using Java 11.0.10 on 91a2f0962a8e with PID 1 (/workspace/io.scalastic.demo.demo_spring_native.DemoSpringNativeApplication started by cnb in /workspace)
2021-03-21 19:32:54.190  INFO 1 --- [           main] i.s.d.d.DemoSpringNativeApplication      : No active profile set, falling back to default profiles: default
2021-03-21 19:32:54.218  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
Mar 21, 2021 7:32:54 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
Mar 21, 2021 7:32:54 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
Mar 21, 2021 7:32:54 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/9.0.44]
Mar 21, 2021 7:32:54 PM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring embedded WebApplicationContext
2021-03-21 19:32:54.220  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 29 ms
2021-03-21 19:32:54.231  INFO 1 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
Mar 21, 2021 7:32:54 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
2021-03-21 19:32:54.240  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-03-21 19:32:54.241  INFO 1 --- [           main] i.s.d.d.DemoSpringNativeApplication      : Started DemoSpringNativeApplication in 0.057 seconds (JVM running for 0.06)
  • Testez son fonctionnement avec :
% curl http://127.0.0.1:8080
Hello!

Ca marche ! Magnifique !!

Caractéristiques du Buildpacks

  • La compilation dure 3 min (avec les images Docker et les artefacts Maven en local)
  • L’application dĂ©marre en 0.06 s
  • L’image Docker contenant l’application Spring et l’OS, fait une taille de 82 Mo

Conclusion

  • La version Spring Native 0.9.0 nous a permis de compiler facilement une application Spring en mode natif.
  • Comme attendu, les bĂ©nĂ©fices du mode natif sont un dĂ©marrage instantanĂ© et une taille de conteneur fortement rĂ©duite.

Points intéressants, cela engendre de nouvelles utilisations :

  • la gestion du High Availability peut se faire avec une seule instance, le dĂ©marrage d’une seconde Ă©tant instantanĂ©e.
  • le dĂ©marrage instantanĂ© permet aussi Ă  une application web d’être serverless, sans avoir besoin d’être redĂ©veloppĂ©e.
  • Avec Knative (un redesign de Kubernetes qui dĂ©marre des conteneurs serverless), GraalVM Native est une solution très bien adaptĂ©e.

Spring Native sera, à terme, intégré dans Spring Boot 3 et Spring Framework 6, le but étant de spécifier uniquement dans le build Maven ou Graddle, la cible attendue (native ou autre). Le travail restant consiste à optimiser la taille du code natif générée, prendre en compte plus d’APIs Spring et améliorer l’exécution des tests dans l’image native (JUnit 5,…)

A suivre de près donc !

Cheers…

Jean-Jerome Levy

Ecrit par

Jean-JĂ©rĂ´me LĂ©vy

Consultant DevOps

Professionnel chevronné dans le domaine de l’informatique, cumulant plus de 20 années d’expérience au sein de DSI de grandes entreprises, mon expertise diversifiée m’a permis de jouer un rôle clé dans de nombreux projets, caractérisés par la mise en place de pratiques DevOps innovantes.