11 mars 2009

JBoss TattleTale, un outil de vérification de dépendances

Actuellement, j'effectue la migration vers Maven de quelques applications historiques. La difficulté de cette tâche réside dans le dépouillement et l'inventaire des fichiers JAR desquels dépend une application. En effet, on se retrouve face à un amas de fichiers JAR parmi lesquels se trouvent des fichiers JAR doublons (avec un nom différent ou une version différente), des fichiers JAR appartenant au serveur d'applications (par exemple, servlet.jar ou connector.jar), d'autres qui ne sont pas ou plus utilisés par l'application et parfois même des fichiers JAR de bibliothèques de tests unitaires (par exemple junit.jar). Le travail consiste alors à ne garder que les JAR qui sont réellement utilisés et d'éliminer (avec la plus grande précaution) tous les autres. Il va sans dire que s’attaquer à un tel problème à « mains nues » n'est pas une mince affaire.

C'est en cherchant un outil de vérification de dépendances que je suis tombé sur l'utilitaire « JBoss Tattletale », qui à ma grande chance vient de sortir (en version bêta) des laboratoires de JBoss (voir l'annonce sur le Blog de JBoss).
Cet outil permet de vérifier les dépendances entre les fichiers JAR présents dans un même répertoire (considéré comme un classpath). Ses fonctionnalités les plus remarquables sont :
  • Identifier les dépendances entre les fichiers JAR (par exemple, hibernate-3.1.3.jar dépendant de antlr-2.7.6rc1.jar)
  • Lister les classes dont dépend un fichier JAR et lister celles qu'il expose.
  • Lister les classes dont dépend un JAR, mais qui sont absentes du classpath.
  • Lister les classes présentes dans le classepath et les fichiers JAR dans lesquels elles se trouvent
  • Alerter si une classe est présente dans plusieurs fichiers JAR.

L'outil s'utilise en ligne de commande et sa prise en main est rapide. Il génère un rapport au format HTML dans lequel on peut naviguer aisément (voir la capture d'écran ci-contre).

Cet outil m'a vraiment aidé à effectuer la vérification des dépendances lors de la phase préliminaire de migration vers Maven. Malheureusement, il ne dispose pas (pour l'instant?) de plugin Maven permettant l'automatisation de cette tâche (de son coté, ANT dispose déjà d'une tâche tattletale).
Enfin, si vous vous demandez ce que veut dire «tattletale» sa définition se trouve ici. En résumé, «tattletale» est une personne commère qui révèle les secrets. On voit bien que e nom n'a pas été choisi par hasard !
L'outil, qui me semble très prometteur, en est tout juste à ces débuts. Souhaitons-lui un bel avenir!

Liens utiles :

Téléchargement : http://sourceforge.net/project/showfiles.php?group_id=22866&package_id=311046&release_id=665534 (800ko environ)

JIRA : https://jira.jboss.org/jira/browse/TTALE


6 mars 2009

Comment lire la version d'un JAR à partir du fichier « Manifest » ?


Toute application doit avoir un numéro de version. Il permet d'identifier aisément, entre autres, la branche du code source à l'origine de sa création (pour corriger des bogues, effectuer des évolutions, etc.). La technique la plus répandue c'est d'écrire le numéro de version dans un fichier de propriétés (properties) dont le contenu pourrait ressembler à ce qui suit :

application.version=1.0b

Il est alors possible à l'application d'extraire le numéro de version pour l'afficher dans la barre de titre, par exemple, ou l'utiliser dans la trace applicative. Ce procédé vous oblige à maintenir le fichier et à veiller à incrémenter correctement le numéro de version.

Bonne nouvelle si vous utilisez Maven : vous n’aurez plus à effectuer cette tache ! En effet, Maven inscrit systématiquement le numéro de version dans le Manifest (MANIFEST.MF) des JAR qu'il produit (le numéro de version est exactement le même que celui qui figure dans le POM du projet Maven).

Comment extraire le numéro de version ?

Commençons par un code simple. Cela se fait en deux étapes : charger le fichier MANIFEST.MF, puis lire son contenu grâce à la classe java.util.jar.Manifest.

La classe suivante est « packagée » dans un jar nommé version.jar.
public class VersionUtil {

public static void main(String[] args) throws IOException {
System.out.println(readVersion());
}

public static String readVersion() throws IOException {

InputStream in = VersionUtil.class.getResourceAsStream("/META-INF/MANIFEST.MF");

Manifest manifest = new Manifest(in);

// Lire la propriété "Implementation-Version" du Manifest

String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);

return version;

}
}

Le code ci-dessus fonctionne sans erreur, mais affiche quand même un résultat erroné : 1.5.0_10. Il correspond en réalité au numéro de version du JDK. Le ficher MANIFEST.MF chargé n'était «manifestement» pas celui du JAR en question, mais celui de rt.jar (le jar qui contient les classes de base de Java). L'explication est la suivante : la méthode getResourceAsStream(..) délègue la lecture de MANIFEST.MF au chargeur de classe (classloader) de la classe VersionUtil.class, or ce chargeur de classe se trouve dans rt.jar et non dans version.jar. Pour qu'une classe puisse charger le fichier MANIFEST.MF du JAR dans lequel elle se trouve, elle recourt à une méthode utilitaire qui calcule le chemin dudit JAR. Elle déduit ensuite l'emplacement du Manifest.

L'extrait de code Java montre comment le chemin vers le MANIFEST.MF est trouvé. J'avoue que le code est un peu alambiqué, mais heureusement il est commenté. La méthode présente l'avantage de fonctionner, et pour une classe se trouvant dans un jar, et pour une classe se trouvant dans un répertoire. Pour une meilleure compréhension, j'ai mis en commentaires le contenu des variables à chaque étape, et ce, pour les deux cas précités.
private static String getPathToManifest(){

// 1 - Lire le nom de la classe
String classSimpleName = VersionUtil.class.getSimpleName() + ".class";
// classSimpleName = VersionUtil.class

// 2 - Récupérer le chemin physique de la classe
String pathToClass = VersionUtil.class.getResource(classSimpleName).toString();

// pathToClass = file:/C:/workspace/VersionUtil/bin/com/abdennebi/version/VersionUtil.class
// pathToClass = jar:file:/C:/version.jar!/com/abdennebi/version/VersionUtil.class

// 3 - Récupérer le chemin de la classe à partir de la racine du classpath
String classFullName = VersionUtil.class.getName().replace('.', '/') + ".class";
// classFullName = com/abdennebi/version/VersionUtil.class

// 4 - Récupérer le chemin complet vers MANIFEST.MF
String pathToManifest = pathToClass.substring( 0, pathToClass.length() - (classFullName.length())) + "META-INF/MANIFEST.MF";
// pathToManifest = file:/C:/workspace/VersionUtil/bin/META-INF/MANIFEST.MF
// pathToManifest = jar:file:/C:/version.jar!/META-INF/MANIFEST.MF

return pathToManifest;
}
L'exemple complet se trouve à cet endroit VersionUtil.java. Une modification pour Java 1.4 se trouve ici : VersionUtil14.java.