Il n'y a pas de magie dans spring

Depuis l’avènement de spring-boot, on entend parler du fait qu’il est désormais possible de compiler un jar qui contient tout le nécessaire pour s’exécuter. Cela inclut l’application, ses dépendances et le serveur. On a tous utilisé cela en développement, mais jusque là je finissais par compiler un war que je déployais dans une infrastructure existante.

Mais actuellement, je me trouve confronté à un client qui n’a aucune expérience dans l’exploitation d’un serveur comme tomcat. Je pourrais faire ma première mise en production d’un jar, mais pas avant d’avoir compris tout ce qui se cache derrière la magie qui l’entoure !

Le serveur

Avant tout, notre jar a besoin de contenir 1oo% des éléments que nous avons défini dans notre fichier de dépendance. Cela inclut les dépendances avec le scope compile , mais aussi celles du runtime . La principale, dont on sous-estime l’impact, c’est javax.servlet. Celle-là, est responsable de connecter une socket à votre servlet. En pratique, c’est Tomcat, Jetty, Undertow, … qui implémentent cette API. Pour que notre jar soit auto-suffisant, il faut donc que celui-ci contienne un de ces serveurs. Par défaut spring embarque un Tomcat.
Le lancement du jar déclenche le démarrage du serveur qui, dès qu’il sera prêt, s’occupera d’exécuter le code de votre war. Mais le fait de cacher le serveur dans un jar permet-il encore de le configurer toujours aussi finement en cas de besoin?
Bien sûr, c’est la marque de fabrique de spring-boot. Une configuration par défaut qui fonctionne dans 9o% des cas, mais pour les 1o% restant, laisser le système ouvert et permettre de tout paramétrer.
En ce qui concerne tomcat, cela se passe dans la classe TomcatEmbeddedServletContainerFactory . Elle va permettre de redéfinir tout ce qui se trouve dans le fichier context.xml  de manière programmatique. Cela permet par exemple de changer de connecteur et d’utiliser le non-blocking-io si vous en ressentiez le besoin: spring-boot-sample-tomcat-multi-connectors
Pas d’inquiétude donc, avec le jar de spring-boot, nous gardons toutes les possibilités de tuning de notre serveur

Mes dépendances

Il faut maintenant que notre jar contienne notre application mais aussi toutes les dépendances. Pour ce faire, il va falloir repackager tous nos jars en un seul. Cette technique est appelée le uber-jar (ou aussi fat-jar). L’idée est de prendre tous les jars qui composent les dépendances de notre application, tous les décompresser dans un même répertoire, puis de créer une nouvelle archive qui contiendra l’ensemble des classes.
Si on voulait le faire manuellement, cela pourrait ressembler à cela:

# compile our code
mvn clean install
# generate the list of jar file we need
mvn dependency:build-classpath -Dmdep.outputFile=target/dependencies
# flatten the dependencies near our classes
cd target/classes
sed "s/:/\n/g" ../dependencies | xargs -n 1 jar -xf
# create the uber-jar
jar -cfe ../da-application.jar app.Bootstrap *

Comme c’est tout de même un peu fastidieux, le plugin de maven qui permet de faire cela facilement c’est maven-shade-plugin. Il propose en plus un certain nombre d’opérations qui peuvent transformer les classes, les renommer pour éviter les collisions, en exclure, en concaténer, en ajouter…
Vous trouverez  sur github.com/mathieu-pousse/auto-executable un mini projet qui en fait la démonstration. C’est une application avec une classe app.Bootstrap et deux dépendances vers apache commons. Après l’exécution de la commande mvn clean install, on trouve dans le répertoire target:

  • original-auto-executable-1.0-SNAPSHOT.jar contient uniquement la classe de notre application
  • auto-executable-1.0-SNAPSHOT.jar l’uber jar qui contient bien notre application et les classes d’apache commons
jar -tf target/auto-executable-1.0-SNAPSHOT.jar | grep -v class | grep -v META
app/ <-- our application
org/ <-- apache common classes start here
org/apache/ org/apache/commons/
org/apache/commons/lang3/
org/apache/commons/lang3/builder/
...

Ce plugin propose donc (relativement) simplement de repackager notre application en une seule archive auto suffisante.

Mon service systemd

Hors de question de déployer mon service en production si il n’est pas installé en tant que service. Systemd est le successeur du System V et de ses fichiers que l’on trouvait dans /etc/init.d. C’est le système de service qui est désormais embarqué par défaut dans la plupart des distributions Linux. C’est une refonte globale du système de gestion des services. Il s’appuie sur des fichiers de description des services.
Donc pour notre application spring-boot, il ‘suffit’ d’en créer un pour notre besoin.

cat > /lib/systemd/system/da-application.service << EOF
[Unit]
Description=Da Application
After=syslog.target
[Service]
User=da-user
ExecStart=/opt/da-application.jar
SuccessExitStatus=143
[Install]
WantedBy=multi-user.target
EOF

C’est simple et efficace ! Une petite Description , le tag After  permet de configurer les dépendances nécessaires pour notre service. La section [Service]  spécifie l’utilisateur à utiliser pour lancer le service, le chemin de l’executable, et un code d’erreur “normal” (143 c’est le code renvoyé en cas de SIGTERM par l’application et qui n’est pas une erreur mais une fin normale).
Ensuite, il suffit de contrôler le service avec systemctl  pour lancer le service.
Oui mais voilà, si vous exécutez un jar fraîchement compilé, voilà ce qui se passe:

./da-application.jar
invalid file (bad magic number): Exec format error

SFX

En effet, un jar est une archive, un zip, pas un fichier exécutable. Mais alors, comment faire pour pouvoir exécuter un jar comme un programme? La solution réside dans une astuce d’organisation d’un fichier zip. La norme autorise à ce qu’un fichier zip soit préfixé par des données qui n’ont pas de rapport avec le contenu. Au moment de l’extraction, cette zone sera simplement ignorée et n’interfère donc pas avec les données de l’archive. Mais cela nous permet par exemple de préfixer notre fichier zip avec un programme qui se charge d’exécuter notre application.
Il suffit donc de concaténer un script de démarrage et notre zip pour obtenir un zip auto-executable

#!/bin/bash
if [ $(java -version &>/dev/null) ]; then
    JAVA_EXE=java
else
    # Ensure we have somewhere a running version of java
    [ -z ${JAVA_HOME} ] && (echo "Must set JAVA_HOME" && exit 1)
    JAVA_EXE="${JAVA_HOME}/bin/java"
    [ ! -x ${JAVA_EXE} ] && (echo "Must set JAVA_HOME with a real java binary" && exit 1)
fi
SELF=$0
${JAVA_EXE} -Dsun.misc.URLClassPath.disableJarChecking=true -jar ${SELF} "$@"
exit 0

Notez l’utilisation de la propriété sun.misc.URLClassPath.disableJarChecking qui permet de désactiver une sécurité de la JVM qui par défaut, vérifie le magic number du jar qu’il va exécuter pour s’assurer que c’est bien un zip. Comme nous avons préfixé notre archive avec notre bash, le magic number est décalé de quelques octets et il faut donc désactiver cette protection.
Ensuite il suffit de concaténer et exécuter !

cat launcher.sh da-application.jar > da-application-sfx.jar
chmod +x da-application-sfx.jar
./da-application-sfx.jar arguments are there
Running w/ [arguments, are, there]
Complex hash = D76lIjuKr0yTdprnWfUYMeEvN8jeMVE15UGuyKvZlr9c3KtxPERZqjAqYTgouhBq%

spring-boot

Comme nous l’avons vu, il est possible de faire nous même ce travail de packaging, il n’y a rien de magique, mais c’est un peu fastidieux, et c’est là qu’intervient spring-boot.
Dans votre projet, il suffit d’ajouter cette configuration pour obtenir automatiquement ce jar auto suffisant et executable:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <configuration>
            <executable>true</executable>
        </configuration>
    </plugins>
</build>

 
Spring gère pour vous ce repackaging, le jar sfx et beaucoup d’autres problématiques pour que tout cela ne soit qu’une opération transparente pour l’utilisateur.
D’ailleurs, il utilise une technique légèrement différente pour repackager son Jar et embarquer toutes les dépendances. Afin d’éviter les collisions de noms de classe, les jars sont embarqués tel quel et un Launcher spécifique va s’occuper de charger les classes des dépendances. La Main-Class de votre jar n’est plus celle de votre application, mais celle du Launcher de spring qui charger les classes des jars puis rendre la main à votre classe applicative.

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: app.Bootstrap

Plus de détails dans la documentation officiel ici.
Finalement, il n’y a pas de magie dans spring, uniquement des magiciens !

Auteur/Autrice

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :