— Cyril HUGER, mars 2006
Le développement d’un nouveau plugin se fait rarement sans s’appuyer sur des librairies tierces, mais quelles sont les approches possibles pour intégrer ces fameux JARs externes à notre projet ?
Nous n’aborderons pas la solution triviale qui consiste à intégrer directement le JAR à notre projet. Cette solution n’est adaptée qu’aux plugins de petite taille et sans relations de dépendance avec le reste des plugins. Nous nous placerons plutôt dans le cas d’une contribution plus conséquente basée sur plusieurs plugins. Dans ce scénario il y a de fortes chances que nos différents plugins nécessitent une même librairie tierce partie. Intégrer le JAR externe à chacun de nos plugins est exclu pour des raisons évidentes de taille et de maintenance (chaque plugin serait impacté en cas de mise à jour du JAR externe).
Maintenant que le décor est posé nous voyons clairement qu’il nous faut un moyen élégant de partager un JAR externe entre plusieurs plugins. Nous verrons deux manières de répondre à ce problème :
Afin de rendre l’explication plus concrète nous allons supposer que notre contribution est découpé en deux plugins :
Ces deux plugins s’appuient sur la librairie Jakarta Commons Lang en version 2.1.
Comme souvent avec Eclipse nous n’aurons pas beaucoup à travailler, toutes les étapes sont prises en charge par un assistant.
Pour obtenir l’assistant il faut demander la création d’un nouveau projet de type plugin et choisir “Plug-in from existing JAR archives”.
Sur la page suivante nous indiquons à Eclipse où se trouve le(s) JAR(s) dont nous voulons faire un plugin.
Ensuite nous remplissons les propriétés du plugin en prêtant une attention particulière à la version du plugin (2.1.0 dans notre cas pour suivre le versionning du JAR original) ainsi qu’au fournisseur du plugin (ne nous attribuons pas la paternité du code mais citons plutôt le vrai créateur).
Et voila, le plugin est créé et entièrement paramétré. Je vous encourage à aller voir le contenu des onglets “runtime” et “build” du manifest du plugin afin de constater que l’assistant a réellement tout fait pour nous.
Tout commence par la création d’un nouveau plugin standard. On déroule l’assistant comme pour tout autre plugin. La seule subtilité est de décocher la case “Generate the Java class that controls the plug-in’s life cycle”. En effet notre plugin ne sera qu’un réservoir de librairies et il n’a pas besoin de gérer son cycle de vie.
Maintenant nous ajoutons le fichier “commons-lang-2.1.jar” au contenu de notre projet.
Après avoir ajouter ce JAR au “Build Path” de notre projet il faut aller dans l’onglet “Order and Export” et bien veiller à cocher notre JAR.
Maintenant le reste du travail se situe au niveau du fichier manifest du plugin.
Dans l’onglet “runtime” on ajoute tous les packages du JAR à la partie “Exported Packages”. Dans la partie “Classpath” on ajoute “.” (en faisant “New...” puis en saisissant un point) et on ajoute le jar en passant par le bouton “Add...”.
La dernière étape consiste à aller sur l’onglet “build” et cocher le répertoire “lib” dans la partie “Binary Build” afin que le JAR soit présent dans l’export de notre plugin.
Si par exemple nous ajoutons à notre plugin “libs” une librairie externe qui contient des composants SWT alors nous aboutirons à une NoClassDefFoundError à l’exécution. Comment expliquer cela ? Supposons que cette librairie nous fournit un composant basé sur la classe org.eclipse.swt.widgets.Composite. Notre plugin “ui” a une dépendance vers le plugin “libs” ainsi qu’une dépendance vers le plugin “org.eclipse.ui”. La classe Composite est donc visible de notre plugin “ui” et tout compile mais à l’exécution une NoClassDefFoundError surgit indiquant qu’elle ne trouve pas la classe Composite. Cette erreur provient du fait que chaque plugin possède son propre classloader. En effet c’est le classloader du plugin “libs” qui charge le composant SWT externe et qui a besoin de charger aussi la classe Composite. Or le plugin “libs” ne référence pas “org.eclipse.ui” et ne peut donc aboutir. La solution consiste donc à ajouter une dépendance du plugin “libs” vers le plugin “org.eclipse.ui”.
Maintenant que nous possédons deux solutions pour un même problème essayons de les départager.
Dans le cas où nous n’utilisons qu’une seule librairie tierce comme ici, alors on constate que la première solution est plus rapide à mettre en oeuvre. Cependant quand le nombre de librairies augmente alors la solution 2 devient la plus productive car le plugin n’est plus à créer, il suffit juste d’ajouter les librairies et effectuer le paramétrage dans l’onglet “runtime” du manifest. Tous les autres plugins bénéficient alors automatiquement de la nouvelle librairie (car ils dépendent du plugin libs) alors qu’avec la solution 1 il faut repasser sur les plugins pour ajouter une nouvelle dépendance.
Si on se concentre sur l’aspect versionning et mises à jour alors la solution 1 prend l’avantage. En effet cette solution permet d’avoir un versionning très précis de chaque librairie employée alors que la solution 2 nous offre un versionning lié au plugin qui n’a aucun lien avec les librairies contenues dans ce plugin. La solution 1 facilite les mises à jour en permettant de ne re-livrer que les plugins des librairies qui ont évoluées. La solution 2 nous oblige à re-livrer le plugin libs qui peut rapidement avoir une taille conséquente.
A la vue de ces arguments je ne conseille pas une approche plus que l’autre. Le choix sera basé en fonction du degré de contrôle que l’on veut avoir sur les librairies tierces.
Notons d’ailleurs que les deux approches peuvent être employées au sein d’une même contribution. Par exemple si notre contribution nécessite JMF (Java Media Framework) ainsi que différentes librairies génériques (du type CommonsLang) alors il est certainement judicieux d’utiliser la solution 1 pour les librairies JMF et la solution 2 pour le reste des librairies. En effet JMF propose des fonctionnalités très ciblées qu’il est intéressant d’isoler et qui ne seront pas utiles à tous nos plugins tandis que les autres librairies seront potentiellement utilisées par tous les plugins.
Notre plugin “ui” dépend du plugin “core” qui dépend du plugin “libs” (peut importe qu’il soit créé selon la méthode 1 ou 2. Malgré ce graphe de dépendances le plugin “ui” ne voit pas les classes contenues dans le plugin “libs”. En effet le plugin “ui” ne voit que les classes exportées par le plugin “core”. Il faut donc que le plugin “ui” décrive explicitement sa dépendance envers le plugin “libs” s’il veut avoir accès aux classes exportées par ce plugin.
Cet exemple montre qu’il n’existe pas de “transitivité des dépendances” entre les plugins Eclipse. Même si cela peut paraître contraignant au premier abord, cette non transitivité est une très bonne chose. Elle permet une plus grande indépendance entre les plugins et facilite les évolutions. Dans notre exemple, il n’est pas dans le contrat du plugin “core” de fournir un accès à “CommonsLang”. La description de ce plugin indiquait qu’il était responsable du modèle objet. Si dans l’avenir il est capable de fournir le même service sans nécessité l’utilisation de “CommonsLang” alors il supprimera sa dépendance vers le plugin “libs” sans que cela ne provoque une réaction en chaîne sur tous les plugins qui auraient pu utiliser “CommonsLang” à travers lui.
La façon de bien incorporer des librairies tierces dans une contribution Eclipse est un problème moins trivial qu’il n’y paraît.
L’avenir est peut-être d’appliquer la solution 1 pour toutes les librairies. Si cette solution est universellement adoptée alors la distribution de nos contributions sera allégée. De plus cela permettra de réduire l’utilisation mémoire d’Eclipse grâce à la factorisation des dépendances. Actuellement si une même librairies est nécessaire aux contributions de 2 éditeurs différents alors elle sera chargée 2 fois en mémoire. Ce problème n’apparaît pas si on applique la solution 1. Pour être complet mettons quand même un bémol : le partage d’une même librairie par les contributions de 2 éditeurs différents peut devenir problématique dans la cas d’un changement de version de cette librairie commune si la compatibilité ascendante n’est pas assurée.
How can I share a JAR among various plug-ins?
— Cyril HUGER, mars 2006