Il n’y a pas de magie dans spring

0

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:

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

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.

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:

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

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 !

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:

 

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.

Plus de détails dans la documentation officiel ici.

Finalement, il n’y a pas de magie dans spring, uniquement des magiciens !

Partagez cet article.

A propos de l'auteur

Développeur / Formateur backend autour de Java et son ecosytème, de la conception à l'industrialisation, du déploiement au monitoring, j'aime le code bien fait et comprendre les paradigmes des outils pour en tirer le meilleur

Ajouter un commentaire