Le déploiement d'une application Java/J2EE

Premier épisode dans la série d'articles « Mise en production et exploitation d'une application Java/J2EE ».

Le Jour J approche pour la mise en ligne de l'application de ChocoCorp. Les développements sont terminés, et l'application va bientôt être déployée en production.

Pour l'hébergement de son application, ChocoCorp a fait appel à un hébergeur spécialisé qui lui a mis à disposition un serveur sous Linux avec le serveur d'applications Glassfish et MySQL pré installé. Ils n'ont, normalement, plus qu'à déployer leur EAR sur ce serveur et valider que tout fonctionne avant d'ouvrir le site au public.

Confiants parce que leur application passe tous les tests unitaires et qu'il n'y a pas eu de grosses modifications depuis plusieurs semaines, ils décident de prendre le fichier EAR généré par Maven2 et de le déployer via l'interface d'administration de Glassfish. Et c'est là que les problèmes commencent...

Classloader et Classpath

La gestion du classloader et du classpath est un problème assez courant dès qu'on déploie un EAR.

Il y a plusieurs problèmes dans la gestion du classpath d'une application :

  • L'utilisation de classes communes entre plusieurs modules de l'application
  • Les librairies communes ou manquantes entre l'application et le serveur d'applications

Le problème des classes communes

Pendant les phases de développements, on détecte assez rapidement le problème quand on développe et déploie l'application dans son ensemble. Beaucoup moins quand on développe indépendamment des modules et qu'on assemble au final l'application. Dans ce cas on aura beaucoup de chances d'avoir des erreurs de type ClassCastException ou NoClassDefFoundError.

Alors, comment éviter ces erreurs ?

C'est un problème d'architecture et d'assemblage de l'application. Il n'y a pas de solution toute simple à ce problème. Il faut bien comprendre comment fonctionne le Classloader du serveur d'applications. Il n'y a malheureusement rien dans les spécifications JEE qui imposent une gestion standard du Classpath. C'est généralement dépendant du serveur. Heureusement, tous les serveurs utilisent plus ou moins la même technique à savoir un classloader hiérarchisé.

La solution la plus efficace consiste à déplacer à la racine de l'EAR l'ensemble des classes et librairies, ainsi que leurs dépendances, communes à tous les modules de l'application. Il ne faut pas oublier de référencer le classpath dans le fichier META-INF/Manifest.mf de l'EAR . Ce classpath devra contenir toutes les librairies accessibles pas les autres modules.

Les librairies

Il se peut que certaines librairies, non obligatoires, soient manquantes et que cela bloque le lancement de l'application. En fait, le serveur d'applications va analyser le classpath déclaré dans le fichier META-INF/Manifest.mf présent dans tous les fichiers EAR, WAR, JAR. Si certaines dépendances ne sont pas remplies, alors le serveur peut refuser le démarrage de l'application.

Pour résoudre le problème, vous pouvez simplement inclure la librairie manquante ou supprimer la référence à la librairie dans le fichier META-INF/Manifest.mf.

Certaines librairies fournies par le serveur d'applications peuvent avoir des versions différentes. Le problème ne sera peut-être pas détecté au moment le déploiement. Il pourra apparaitre lors de l'utilisation de l'application.

Soit, vous voulez utiliser une version spécifique d'une librairie auquel cas vous devez l'inclure dans votre EAR. Soit, vous souhaitez utiliser la version fournie par le serveur d'applications. Dans ce cas, vous devez utiliser, pendant la phase de développement, le même serveur et supprimer de votre classpath la librairie.

Connexion JDBC

Une application sans connexion à une base de données est assez rare de nos jours. Cependant, on rencontre assez souvent un problème au moment du déploiement de l'application : configurer la connexion JDBC à la base de données. La plus part du temps la connexion est configurée en dur dans un fichier de propriétés. Il est rare d'avoir le même mot de passe sur l'environnement de Test et celui de Production. D'où un problème de connexion à la base de données au démarrage de l'application.

Comment résoudre le problème ?

Il existe plusieurs solutions dont :

  • Créer un utilisateur identique (même login et même mot de passe) sur tous les environnements
  • Avoir un fichier de properties différents pour chaque environnement et l'inclure au moment du build pour un environnement donné
  • Externaliser la connexion JDBC via JNDI

Pour des raisons évidentes de sécurité, je ne trouve pas la première solution satisfaisante. La deuxième solution quant à elle oblige à maintenir plusieurs fichiers de configuration et générer des builds différents avec le risque qu'un bug soit introduit entre 2 builds.

La dernière est de loin celle que je préfère et préconise. La connexion est configurée sur le serveur d'applications (via une interface d'admin ou via un fichier de configuration). C'est devenu aujourd'hui relativement simple à configurer et à utiliser, tout particulièrement quand on utilise Spring. Quelques lignes suffisent.

C'est également le même principe pour la configuration des Queues JMS.

Les chemins d'accès

L'application de ChocoCorp démarre maintenant correctement et crée correctement la base de données. Après plusieurs tests, un autre problème apparait : les factures ne semblent pas être générées. Après investigation le problème vient d'un répertoire qui n'est pas trouvé.

La encore c'est un problème assez récurrent : les chemins d'accès vers des fichiers ou des répertoires sont également codés en dur dans l'application.

Plusieurs solutions sont possibles dont :

  • Utiliser un chemin relatif au serveur d'applications
  • Stocker dans un répertoire du serveur d'applications
  • Externaliser le chemin d'accès via une ressource JNDI
  • Utiliser un fichier de configuration externe, référençant tous les autres chemins, accessibles via JNDI

Les solutions 1 et 2 sont faciles à mettre en place et ne demandent pas beaucoup de modifications dans le code source. Par contre, je ne les trouve pas vraiment utilisable sur du long terme pour la raison suivante : Si on upgrade ou qu'on change le chemin d'accès au serveur d'applications, il ne faudra pas oublier déplacer les fichiers et répertoires spécifiques à l'application dans ce nouveau répertoire.

Avec ces deux premières solutions, on dépend fortement de l'emplacement du serveur d'applications. Je trouve beaucoup plus propre et plus logique de stocker les données en dehors de celui-ci. C'est pour cela que je préfère les solutions 3 et 4. Elles vont cependant dépendre fortement de l'architecture de votre application. Si vous n'avez qu'un répertoire ou un fichier à exporter, la troisième solution me parait plus adaptée.

Contextes et nom de domaines

Cette fois l'application de ChocoCorp se déploie et fonctionne correctement. Il ne reste plus qu'à mettre en place un frontal web (Apache, HAProxy ou autre) et à faire pointer le nom de domaine vers le bon contexte. Une fois le frontal configuré, certaines pages ne s'affichent plus correctement et des liens sont cassés.

C'est aussi un grand classique des problèmes qu'on peut rencontrer quand on déploie une application J2EE. Pour que l'application soit accessible au travers d'un frontal, il n'y a pas beaucoup de solutions possibles :

  1. Utiliser un module d'Apache qui va modifier à la volée dans les pages générées par l'application les liens. Même si dans la pratique ça fonctionne bien, je ne conseillerais pas cette solution pour les raisons suivantes :
    • La configuration est difficile à mettre en place et il faut beaucoup d'ajustements pour que ça fonctionne
    • Ce type de configuration génère une charge supplémentaire sur le serveur frontal qui pourrait être évitée.
  2. Corriger dans le code source (Java, JSP et JavaScript) toutes les références au contexte et travailler en chemin relatif.

Quelques autres bonnes pratiques

Dés qu'on travaille dans un environnement de production industrialisé, un certain nombre de processus sont standardisés. Généralement avant de déployer votre application on vous demande de compléter un document contenant toutes les informations nécessaires à votre application.

Sans aller jusqu'à ces pratiques, je trouve qu'il est important de rédiger et de maintenir dans une procédure d'installation ou du moins un document référençant les différents paramètres de l'application à configurer sur votre serveur d'applications :

  • Variables JNDI
  • Bases de données, utilisateurs, à créer
  • Paramètres d'authentification
  • Répertoires, fichiers properties, scripts
  • Scripts SQL à exécuter

Avoir un environnement d'intégration ou recette proche voir identique à l'environnement de production.

Utiliser le même build pour toutes les phases de tests, d'intégration/recette et de production. Comme je l'expliquais plus haut, cela évite qu'un bug soit introduit durant ou par le processus de build. 

Quant à vous, avez-vous déjà rencontré ce type de problèmes dans le déploiement d'une application ?

Et comment les avez-vous résolus ?