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 !