Développer une application modulaire en Java

Baptiste Wicht

Cet article présente les bases du développement d'une application modulaire. Il présente également un exemple complet d'implémentation en Java.

Commentez

- Titre : Développer une application modulaire en Java

- Auteur : Baptiste Wicht

- Parution : 21 mai 2009

- Mise à jour : 2 juin 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2009 Baptiste Wicht. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Bases

- II. Modules

- II-A. Extension par les modules

- II-B. Services fournis aux modules

- II-C. Types de module

- II-D. Dépendance entre modules

- III. Implémentation des modules

- IV. Chargement des modules

- IV-A. Création d'un nouveau ClassLoader

- IV-B. Ajout des classes dans le ClassLoader système

- V. Séparation de l'application

- VI. Exemple complet

- VI-A. Le core

- VI-A-1. La classe Core

- VI-A-2. Le chargement des modules

- VI-A-3. Les extensions et services

- VI-B. Un module d'exemple

- VI-C. L'application

- VI-D. Résultat

- VII. Conclusion

- VII-A. Les sources

- VII-B. Remerciements

I - Bases

Dans cet article, je vais commencer par évoquer les bases de conception d'une application modulaire, c'est-à-dire, définir ce qu'est une application modulaire, ce qu'est un module et bien entendu ce que ça apporte. Dans les prochains chapitres, je vais essayer de décrire les différentes problématiques inhérentes au sujet et leurs résolutions. Les exemples de code seront en Java, mais les concepts décrits dans cet article peuvent s'appliquer à tous les langages.

Bien entendu, je ne me considère pas comme un spécialiste dans les applications modulaires et les différentes informations que je vais donner dans cet article ne découlent que de ma façon de voir, il y en a certainement d'autres et des meilleures, donc si vous n'êtes pas d'accord avec moi ou si vous pensez qu'on peut faire mieux, n'hésitez pas à me contacter pour me donner votre avis. Toute l'expérience que j'ai réside dans le développement d'une application modulaire comme projet perso.

Pour commencer, qu'est-ce qu'une application modulaire ? Une application modulaire est constituée de deux parties bien distinctes :

- le cœur : comme son nom l'indique, il s'agit de la partie centrale de l'application. Cette partie doit être complètement indépendante des modules.

- les modules : il s'agit cette fois des parties qu'on va ajouter dynamiquement au cœur de l'application pour lui rajouter des fonctionnalités. On leur donne d'autres noms : extensions, briques, plugins, adons… mais le concept est toujours le même.

Il y a à mon avis deux sortes d'applications modulaires :

- le premier type est une application somme toute normale qui propose une série de fonctionnalités et qui propose en plus d'étendre ces fonctionnalités par le biais de modules ;

- le second type est une application qui propose un cœur complètement dénué de fonctionnalités. Toutes les fonctionnalités sont proposées par les modules, c'est donc une application dans laquelle « tout est module ».

La différence principale est que la première peut être utilisée tout à fait normalement sans aucun module, alors que la deuxième ne propose pas (ou presque pas) de fonctionnalités sans module.

Au contraire d'une application normale, il faut donc prévoir des points d'extensions que les modules pourront utiliser pour ajouter des fonctionnalités à l'application ou pour modifier l'existant.

Je me suis basé sur le deuxième type pour réaliser mon logiciel. Cela permet d'avoir un socle solide qu'on peut éventuellement réutiliser pour d'autres applications sous réserve de quelques modifications (le mieux serait bien entendu de penser ce core de manière tout à fait générique dès le début).

Comme application modulaire connue, on peut citer Eclipse qui est entièrement modulaire et qui est du deuxième type.

Maintenant, la grande question est : c’est bien beau tout ça, mais ça sert à quoi ? Tout d'abord, du point de vue du développeur, cela permet de séparer clairement les différentes parties de l'application. On a donc des modules distincts plus faciles à maintenir qu'une grosse application. Cela permet également d'être très souple pour rajouter des fonctionnalités à l'application, en effet, pour cela, il suffit de créer un nouveau module, d'y coder la fonctionnalité et de rajouter ce module dans l'application. Du point de vue de l'utilisateur, cela ajoute de la souplesse au programme. En effet, l'utilisateur va pouvoir choisir quels modules il veut lancer, rajouter de nouveaux modules, voire même en créer s'il en est capable. Il pourra donc personnaliser son application. Néanmoins, cela va peut-être compliquer la tâche de l'utilisateur. Par contre, cela va beaucoup simplifier le travail en équipe sur une application. En effet, si un module ne nécessite pas plus d'une personne, il sera très aisé de travailler chacun sur un module et de ne faire les tests qu'avec son module et ensuite de tester l'application avec les modules des autres, une fois l'application terminée.

Bien entendu, cela ne se fait pas tout seul et ce n'est pas des plus simples à concevoir et à implémenter. En effet, cela pose beaucoup de problématiques qu'il faut régler et cela implique beaucoup de questions qu'il faut se poser.

C'est ce que nous allons voir maintenant dans les prochains chapitres.

II - Modules

Après avoir décrit dans le premier chapitre en quoi consistait une application modulaire et ce qu'était un module, nous allons maintenant nous pencher plus spécialement sur les modules.

Nous avons dit qu'un module permettait de rajouter des fonctionnalités à l'application principale. Mais avant de se lancer dans le moindre code, il va falloir définir exactement ce que seront nos modules, ce qu'ils pourront faire, s'il y aura plusieurs types de modules…

II-A - Extension par les modules

La première question à se poser est : « Que vont pouvoir faire mes modules ? » C'est-à-dire, qu'est-ce qu'un module pourra ajouter à l'application principale.

Nous avons dit qu'un module rajoutait des fonctionnalités à l'application principale, mais il faut maintenant spécifier ce que les modules pourront ajouter et la manière dont ils vont pouvoir les ajouter. En plus d'ajouter, un module pourra éventuellement modifier l'application principale.

Ce que les modules pourront faire va dépendre également du type d'application.

- Dans le cas du premier type (application normale avec possibilité d'extension par module), le module va rajouter des fonctionnalités de types spécifiques, c'est-à-dire en accord avec l'application. Prenons le cas d'une application qui permet de regarder la télévision via Internet. Dans ce cas-là, les modules pourront par exemple rajouter une chaîne à l'application ou rajouter un format de conversion vidéo pour l'enregistrement. Il peut aussi rajouter une fonctionnalité complète comme des statistiques qui indiqueraient le nombre d'heures pendant lesquelles on a regardé la télé sur ce programme et la chaîne la plus regardée.

- Dans le cas du second type (application vide de base et modules qui rendent l'application utilisable), les modules ne vont pas rajouter des fonctionnalités à l'application principale étant donné qu'elle ne permet rien, mais vont définir ce que va faire l'application. Cette fois, les modules, c'est l'application. Prenons mon application (JTheque) qui est destinée à gérer des collections diverses. J'ai pour le moment un module Films et un module Livres. J'ai ensuite des plus petits modules comme le module Stats ou un module qui permet de gérer une liste de films à acheter. On voit qu'on a déjà des modules beaucoup plus larges. En plus, avec quelque chose de ce type, on pourrait imaginer un module qui ferait quelque chose de complètement différent de l'application de base, comme un module Calculette qui ajouterait une calculatrice à l'application, ce qui n'est pas du tout le but de l'application de base, mais qui est rendu réalisable par les possibilités d'extension du cœur de l'application.

Dans ces deux cas, il faut donc définir certains points d'extension. Dans le cas de JTheque, voici les points d'extension que j'ai créés pour mes modules :

- ajouter des onglets dans la vue principale ;

- ajouter des composants dans la barre d'état ;

- ajouter des éléments dans le menu de la vue principale ;

- ajouter des onglets dans la vue de configuration.

À partir de ces points d'extension, les modules peuvent faire beaucoup de choses. Par exemple, si on veut faire une calculette, on peut imaginer rajouter un onglet principal avec une calculette, ou alors faire une fenêtre dédiée et rajouter un élément dans le menu qui ouvrirait cette fenêtre.

Ceci est donc laissé au libre choix du développeur, mais il faut y penser avant et bien réfléchir à ce que ça implique.

II-B - Services fournis aux modules

En plus de points d'extension, le cœur de l'application fournit aussi une série de services aux modules. Ces services peuvent être de simples classes utilitaires ou alors un gestionnaire de persistance d'entités, un gestionnaire de log ou encore un gestionnaire permettant aux modules d'enregistrer des informations (exceptions par exemple) pour les retrouver après l'arrêt de l'application.

Là encore ces points sont laissés à la discrétion du développeur. Ces services ne sont pas obligatoires, mais cela permet d'éviter que chaque module doive tout refaire. En plus, cela permet également une standardisation. En effet, les modules vont (normalement) utiliser le même type de persistance, le même type de log… Ce qui est plus facile à gérer ensuite. Bien entendu, rien n'empêche un module d'utiliser son propre système de log si celui du cœur ne lui convient pas.

Pour la petite histoire, voilà quelques services que j'offre à mes modules dans JTheque :

- gestionnaire de logs : permet aux modules d'obtenir des loggers (Log4J) pour logger différentes actions ;

- gestionnaire d'erreurs : permet aux modules d'afficher des erreurs facilement ;

- gestionnaire de vue : permet aux modules d'afficher des messages, de demander quelque chose à l'utilisateur… ;

- gestionnaire de beans : permet aux modules d'utiliser de l'IOC (Spring) ;

- gestionnaire de ressources : permet aux modules de bénéficier de l'internationalisation (Spring) ainsi que d'un cache d'image ;

- gestionnaire de persistance : permet aux modules de gérer des entités persistantes (JPA/Hibernate) ;

- gestionnaire d'états : permet aux modules de stocker des états.En général, il s'agit de la configuration du module.

Vous pouvez bien sûr ajouter autant de services que vous le désirez. Mais pensez néanmoins à ne par surcharger le core. Quitte à le rendre modulaire au niveau des services. En effet, certaines applications n'auront peut-être pas besoin de certains services, alors pourquoi les charger ?

II-C - Types de module

On peut aussi réfléchir si l'on veut un seul ou plusieurs types de modules ou s'il y en aura plusieurs. Dans mon cas, j'ai fait une petite distinction entre des modules dits principaux et des modules dits secondaires. Les fonctionnalités sont exactement les mêmes sauf qu'un seul module principal peut être lancé au même moment. Par exemple, le module films est un module principal et le module ajoutant une liste de films à acheter est un module secondaire.

Vous pouvez bien sûr imaginer encore d'autres types de modules, mais pensez à définir clairement chacun des types.

II-D - Dépendance entre modules

Le dernier point que je vois à traiter au niveau des modules est la dépendance entre modules. On peut permettre ou non qu'un module dépende d'un autre module. Cela ajoute un niveau de difficulté supplémentaire au niveau de l'implémentation étant donné qu'il faudra vérifier que la dépendance est remplie avant de pouvoir lancer le module. Cela implique également que le module principal doit être lancé avant le module qui en dépend. Et que faire des dépendances circulaires ? Bref, cela est également laissé au choix du développeur qui peut permettre les dépendances intramodules ou non. Dans mon cas, je l'ai permis, j'ai par exemple, le module Stats qui dépend du module Films étant donné qu'il a besoin des informations sur les films, acteurs et réalisateurs pour générer ses statistiques.

En plus de permettre ou non les dépendances entre modules, il faut voir si l'on ajoute des fonctionnalités permettant de communiquer entre les modules. Pour cela, on pourrait demander au gestionnaire de modules l'instance d'un module (en fonction de son nom par exemple ou d'une interface qu'il implémente) et ensuite utiliser cette instance pour effectuer des actions sur le module. Ce peut être simplement de récupérer un dao pour enregistrer un listener, mais ce peut être des modifications, dans ce cas, il faut prévoir que le module pourra être accédé de l'extérieur et pas et ce qui sera faisable sur un module depuis un autre module. Bref, cela n'entre pas dans le cadre de cet article, mais dans le cas d'une application complexe, il est possible que vous ayez à considérer ce point.

III - Implémentation des modules

Maintenant que nous avons vu en détail les caractéristiques des modules, nous allons voir comment mettre en œuvre un module.

Il nous faut donc un conteneur permettant de charger nos modules depuis l'application principale.

Nous allons maintenant voir ce que sera un module au niveau Java.

Comme un module est complètement indépendant de l'application principale, on ne peut inclure directement le module dans l'application principale, il faut distinguer le fichier du module de celui de l'application principale.

Il faut maintenant définir ce que vont être ces fichiers. Comme est au niveau Java, on peut s'imaginer les modules comme des extensions de l'application principale. Or pour étendre une application Java, on utilise des fichiers JAR.

Nos modules seront donc contenus dans des fichiers JAR.

Voici pour ce qui est de la représentation d'un module, nous allons voir comment le représenter pour qu'il soit utilisable au niveau Java.

Nous allons donc créer une interface permet de décrire les caractéristiques principales d'un module. Nous allons rester simples pour le moment. Un module doit tout d'abord être rattachable et ensuite détachable. C'est-à-dire le moment où il va se rattacher à l'application, c'est-à-dire l'étendre et le moment où il va se détacher de l'application. Il doit également avoir un nom. Pour le moment, c'est ce dont nous avons besoin.

On va donc avoir une interface toute simple décrivant notre module :

IModule.java

public interface IModule {
public void plug();
public void unplug();
public String getName();
}

On pourrait donc créer un module très simple affichant quelque chose sur la console :

SimpleModule.java

package org.modules.simple;

public class SimpleModule implements IModule {
public void plug(){
System.out.println("Hello kernel !");
}

public void unplug(){
System.out.println("Bye kernel !");
}

public String getName(){
return "Simple module";
}
}

Nous allons donc créer un fichier Jar contenant notre classe.

Il faut maintenant une solution pour que l'application principale connaisse cette classe pour la lancer. Il faut donc un moyen pour qu'elle trouve cette classe.

On pourrait imaginer parcourir le fichier jar et tester pour chacune des classes si c'est un module ou non. Cette méthode bien que tout à fait faisable et fonctionnelle n'est pas du tout efficace, car elle implique de parcourir toute l'archive ce qui risque de prendre du temps dans le cas de gros module. Alors, on va utiliser les outils que nous offre Java et ajouter une mention dans le manifest du jar pour indiquer à l'application principale quelle est la classe du module à charger.

Voilà à quoi ressemblerait notre Manifest pour notre petit module :

MANIFEST.MF

Manifest-Version: 1.0
Module-Class: org.modules.simple.SimpleModule

Cela permettra à l'application principale de savoir quelle classe charger.

Une autre solution serait d'utiliser la notion de « services » introduit dans Java 6. Pour cela, on peut créer un fichier « /Meta-Inf/services/name » ou name est le nom complet (avec le package) de notre interface de module. Dans ce fichier, on va déclarer toutes les classes implémentant notre interface, par exemple notre SimpleModule. Lorsque Java va charger ces JAR, il va lire ces fichiers et garder une trace des classes concrètes. Ensuite, on peut utiliser la classe ServiceLoader pour rechercher et instancier tous nos modules. Par exemple :

Utilisation des services de Java 6

for (IModule module : ServiceLoader.load(IModule.class)) {
System.out.println( module.getName() );
}

Pour plus d'informations sur cette fonctionnalité, je vous invite à consulter le site officiel.

Je montrerai dans le prochain chapitre une manière de charger ces modules. Car il ne suffit pas de mettre ces fichiers JAR dans le classpath pour les charger étant donné que l'application principale ne les connait pas.

IV - Chargement des modules

Maintenant que nous avons vu comment représenter un module au niveau Java, nous allons voir comment charger dynamiquement ces modules dans notre application.

En Java, les classes sont chargées depuis des ClassLoader qui comme leur nom l'indique sont des chargeurs de classes.

IV-A - Création d'un nouveau ClassLoader

De base, Java utilise le ClassLoader système pour charger les classes dont notre application a besoin et ce ClassLoader contient les classes de notre application et toutes les classes qu'il a pu découvrir dans le classpath de celle-ci.

Le problème est que dans notre cas, on ne peut pas vraiment ajouter les fichiers Jar des modules au classpath étant donné que l'application ne les connait pas.

Nous allons donc devoir créer un nouveau ClassLoader avec lequel nous allons charger les classes de nos modules.

Il nous faudra donc procéder en deux phases :

- la première servira à explorer les fichiers des modules, à en sortir la classe du module et à récolter les URL des fichiers Jar ;

- la seconde va charger les différents modules en se servant d'un ClassLoader créé à partir des URL collectées durant la première phase.

On va donc créer une classe ModuleLoader qui va nous permettre d'effectuer ces phases.

Prenons maintenant la première phase et créons une méthode qui retourne la liste des classes à charger.

ModuleLoader.java

public class ModuleLoader {
private static List<URL> URL = new ArrayList<URL>();

private static List<String> getModuleClasses(){
List<String> classes = new ArrayList<String>();

//On liste les fichiers de module
File[] files = new File("dossier").listFiles(new ModuleFilter());

for(File f : files){
JarFile jarFile = null;

try {
//On ouvre le fichier JAR
jarFile = new JarFile(f);

//On récupère le manifest
Manifest manifest = jarFile.getManifest();

//On récupère la classe
String moduleClassName = manifest.getMainAttributes().getValue("Module-Class");

classes.add(moduleClassName);

URL.add(f.toURI().toURL());
} catch (IOException e) {
e.printStackTrace();
} finally {
if(jarFile != null){
try {
jarFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

return classes;
}

private static class ModuleFilter implements FileFilter {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().toLowerCase().endsWith(".jar");
}
}
}

Comme vous le voyez, ce n'est pas très compliqué, on parcourt les fichiers Jar existants dans le dossier des modules, on ouvre le fichier Jar, on récupère le manifest et on récupère la classe du module. Ensuite, on ajoute l'URL du fichier Jar à la liste des URL.

Bien entendu ce code est perfectible, il faudrait traiter le cas où le fichier JAR n'a pas de manifest ou alors le cas où le manifest n'a pas de classe de module et il faudrait traiter les erreurs correctement, mais ce n'est pas le but de ce billet.

On peut maintenant passer à la deuxième méthode qui va créer le ClassLoader et instancier les modules puis les retourner :

loadModules()

private static ClassLoader classLoader;

public static List<IModule> loadModules(){
List<IModule> modules = new ArrayList<IModule>();

List<String> classes = getModuleClasses();

AccessController.doPrivileged(new PrivilegedAction<Object>(){
@Override
public Object run() {
classLoader = new URLClassLoader(
URL.toArray(new URL[URL.size()]),
ModuleLoader.class.getClassLoader());

return null;
}
});

for(String c : classes){
try {
Class<?> moduleClass = Class.forName(c, true, classLoader);

if(IModule.class.isAssignableFrom(moduleClass)){
Class<IModule> castedClass = (Class<IModule>)moduleClass;

IModule module = castedClass.newInstance();

modules.add(module);
}
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

return modules;
}

On commence donc à créer un nouveau ClassLoader à partir des URL qu'on a récupéré avec les fichiers JAR. On va utiliser un URLClassLoader qui permet de charger des classes depuis des emplacements définis par des URL. On va lui donner comme parent le ClassLoader de la classe. Comme cela les modules qui utiliseront ce classloader pourront également utiliser les classes de l'application, ce qui est le but d'une application modulaire. Une fois qu'on a créé notre ClassLoader, on va parcourir toutes nos classes et les instancier si elles implémentent bien la bonne interface. Ensuite, on va pouvoir les ajouter à la liste et les retourner.

Encore une fois, ce code est perfectible, il faudrait traiter les erreurs et réagir aux différents cas possibles, mais j'ai essayé de faire au plus court.

Notre chargeur de modules est donc terminé.

La technique présentée dans ce billet pose néanmoins un problème pour ce qui est du déploiement à la volée de module. En effet, si on charge un module après le chargement des premiers modules, on devra recréer un nouveau ClassLoader et on aura donc une partie des modules dans le premier ClassLoader et une partie des modules dans le second ClassLoader. Néanmoins, si les modules n'ont aucune interaction entre eux, vous pouvez tout à fait envisager cette méthode pour le déploiement à la volée. Si ce n'est pas le cas, vous serez obligé de vous rabattre sur la méthode par réflexion pour ajouter des modules dans votre ClassLoader. Je présenterai peut-être cette méthode dans un prochain billet.

Le fait d'avoir un deuxième ClassLoader pose également un problème pour les librairies qui chargent des classes à la volée par exemple Spring IOC ou Hibernate. Il faut voir au cas par cas pour ces librairies comment on peut leur spécifier d'utiliser notre ClassLoader. Souvent ceci est faisable en spécifiant le contextClassLoader via la méthode Thread.currentThread().setContextClassLoader(ClassLoader cl).

IV-B - Ajout des classes dans le ClassLoader système

Le ClassLoader système n'est, de base, pas fait pour être modifié. Néanmoins, au vu des complications qui se pose avec l'emploi d'un nouveau ClassLoader, il peut sembler beaucoup plus pratique d'ajouter nos fichiers JAR directement dans le ClassLoader système.

On peut assez facilement réaliser cette opération. Néanmoins, cette façon de faire nécessite d'utiliser des méthodes privées du ClassLoader pour ajouter des JAR après sa création.

En effet, l'URLClassLoader a une méthode addUrl qui permet de lui rajouter une URL. On peut donc invoquer cette méthode pour ajouter une URL dans la liste des endroits où le ClassLoader va chercher pour charger une classe.

Voici une méthode permettant d'ajouter une URL au ClassLoader système :

Ajout de classes dans le ClassLoader système

public static void addURLToSystemClassLoader(URL u) throws IOException {
URLClassLoader systemClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
Class<URLClassLoader> classLoaderClass = URLClassLoader.class;

try {
Method method = classLoaderClass.getDeclaredMethod("addURL", new Class[]{URL.class});
method.setAccessible(true);
method.invoke(systemClassLoader, new Object[]{u});
} catch (Throwable t) {
t.printStackTrace();
throw new IOException("Impossible d'ajouter l'URL au ClassLoader. ");
}
}

Comme vous le voyez, on ne fait qu'utiliser la Reflection pour ajouter une URL au ClassLoader. Cette méthode est valable pour tout URLClassLoader.

Néanmoins, je vous déconseille d'utiliser cette méthode à moins d'en avoir vraiment besoin. Il est plus propre d'utiliser son propre ClassLoader.

V - Séparation de l'application

Une autre chose qu'il est intéressant de considérer quand on pense à une application modulaire est l'application en elle-même. Doit-elle être intégrée au core ? Doit-elle être complètement indépendante de l'application qui l'emploie.

L'une et l'autre de ses façons de voir ont leurs avantages et inconvénients :

Le core est lié à l'application : ce type de core est fait pour une application bien spécifique. C'est-à-dire qu'il n'est destiné à être utilisé que par cette application et des modules pour cette application. Si on prend un exemple tout bête, cela voudra dire que le core connait le nom de l'application et c'est lui qui a les informations sur les crédits de l'application. C'est peut être simpliste comme exemple, mais imaginez que vous développiez une nouvelle application et que vous vous basiez sur un Core de ce genre et que vous ne pouvez pas changer le nom de l'application ni l'auteur dans les crédits, ce n'est pas des plus pratiques. Ce type de core peut être une application modulaire de type 1 ou 2.

Le core est complètement indépendant de l'application : ce précédent type de core est entièrement générique et peut être utilisé par n'importe quelle application. Si on reprend l'exemple d'avant, cette fois-ci, le core ne connaitra pas le nom de l'application, c'est cette dernière qui doit configurer le core pour qu'il affiche son nom. Donc vous n'êtes plus embêté si vous voulez l'utiliser avec votre application alors qu'il a été créé pour une autre. Ce core est obligatoirement de type 2. En effet, cela n'aurait pas de sens d'y mettre des fonctionnalités si on veut pouvoir utiliser ce core avec n'importe quelle application. Imaginez une application de jeux pour enfants qui contiendrait une calculatrice scientifique en anglais de base ainsi que les cours de la bourse. Pas vraiment pratique, non ?

Ceci est donc encore un point qu'il faut définir avant de concevoir son application modulaire. Voici quelques questions qu'il faut se poser :

- Est-ce que mon core devra contenir des fonctionnalités ou non ?

- Est-ce que mon core est mon application ?

- Est-ce que mon core pourra être utilisé par plusieurs applications ?

En fonction des réponses, ce n'est pas très difficile de s'orienter vers un choix entre les deux types de Core. Il faut quand même noter qu'il n'est pas très lourd de séparer la partie application de la partie core, même après coup.

VI - Exemple complet

Maintenant que nous avons vu tous les concepts de base, ainsi que les points spécifiques de l'implémentation. Nous allons développer une petite application modulaire exemple qui va utiliser tous les concepts présentés dans cet article.

On va créer une application du type 2, c'est-à-dire sans aucune fonctionnalité de base, elle ne sert à rien sans module. De plus, on va créer un core qui puisse être utilisé par n'importe quelle application.

Cette application sera des plus simples, une simple vue avec une zone de texte dans laquelle les modules pourront ajouter des lignes.

On va ajouter un service permettant de logguer des messages. Et un autre service fournira une méthode pour ajouter du texte dans la vue, ce sera notre point d'extension.

VI-A - Le core

On va maintenant implémenter le Core de l'application.

VI-A-1 - La classe Core

La classe Core sera responsable de charger les modules et de démarrer les différents services, ainsi que de la gestion du cycle de vie de l'application. Comme on va partir sur un Core complètement indépendant de l'application, il possèdera également un attribut applicationName et un attribut applicationFolder.

De plus, le Core aura donc une méthode launch() et une méthode exit().

Méthode launch() :

launch()

public void launch(){
modules = ModuleLoader.loadModules();

LogManager.getInstance().init();
ViewManager.getInstance().init();

LogManager.getInstance().log("Chargement des modules");

for(IModule module : modules){
module.plug();

LogManager.getInstance().log(module.getName() + " OK. ");
}

ViewManager.getInstance().display();
}

Voilà ce qu'on y fait :

1. Chargement des modules ;

2. Chargement des services ;

3. Utilisation d'un service pour logguer quelque chose ;

4. Démarrage des modules ;

5. Affichage de la vue de l'application.

On n'y fait rien de bien compliqué.

Méthode exit() :

exit()

public void exit(){
for(IModule module : modules){
module.unplug();
}

ViewManager.getInstance().close();

LogManager.getInstance().log("Fermeture de l'application");
}

Encore plus simple. Voilà ce qu'on y fait :

1. Extinction des modules ;

2. Fermeture de la vue ;

3. Logge d'un message de fermeture.

Et voici maintenant le code complet avec les getters/setters, les champs et la javadoc. Ça se passe de commentaire :

Core.java

package org.dvp.core;

import java.util.List;

/**
* Le coeur de notre application modulaire.
*
* @author Baptiste Wicht
*/
public class Core {
private static final Core instance = new Core();

private String applicationFolder;

private List<IModule> modules;
private String applicationName;

/**
* Retourne l'instance unique du core.
*
* @return L'instance unique du core.
*/
public static Core getInstance() {
return instance;
}

/**
* Lance l'application et démarre tous les modules.
*/
public void launch(){
modules = ModuleLoader.loadModules();

LogManager.getInstance().init();
ViewManager.getInstance().init();

LogManager.getInstance().log("Chargement des modules");

for(IModule module : modules){
module.plug();

LogManager.getInstance().log(module.getName() + " OK. ");
}

ViewManager.getInstance().display();
}

/**
* Ferme l'application et stoppe tous les modules.
*/
public void exit(){
for(IModule module : modules){
module.unplug();
}

ViewManager.getInstance().close();

LogManager.getInstance().log("Fermeture de l'application");
}

/**
* Retourne le dossier dans lequel l'application se trouve.
*
* @return Le dossier de l'application.
*/
public String getApplicationFolder() {
return applicationFolder;
}

/**
* Modifie le dossier dans lequel l'application se trouve.
*
* @param applicationFolder Le dossier de l'application.
*/
public void setApplicationFolder(String applicationFolder) {
this.applicationFolder = applicationFolder;
}

/**
* Retourne le nom de l'application.
*
* @return Le nom de l'application.
*/
public String getApplicationName() {
return applicationName;
}

/**
* Modifie le nom de l'application.
*
* @param applicationName Le nom de l'application.
*/
public void setApplicationName(String applicationName) {
this.applicationName = applicationName;
}
}

VI-A-2 - Le chargement des modules

On va maintenant s'occuper du chargement des modules. On reprend ici le code proposé au chapitre 4. dans une seule et même classe :

ModuleLoader.java

package org.dvp.core;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.jar.Manifest;
import java.util.jar.JarFile;
import java.util.ArrayList;
import java.util.List;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;

/**
* Classe responsable du chargement des modules.
*
* @author Baptiste Wicht
*/
public class ModuleLoader {
private static List<URL> URL = new ArrayList<URL>();

private static ClassLoader classLoader;

/**
* Charge tous les modules et les retourne.
*
* @return Une List contenant tous les modules préalablement chargés.
*/
public static List<IModule> loadModules() {
List<IModule> modules = new ArrayList<IModule>();

List<String> classes = getModuleClasses();

AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
classLoader = new URLClassLoader(
URL.toArray(new URL[URL.size()]),
ModuleLoader.class.getClassLoader());

return null;
}
});

for (String c : classes) {
try {
Class<?> moduleClass = Class.forName(c, true, classLoader);

if (IModule.class.isAssignableFrom(moduleClass)) {
Class<IModule> castedClass = (Class<IModule>) moduleClass;

IModule module = castedClass.newInstance();

modules.add(module);
}
} catch (ClassNotFoundException e1) {
e1.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}

return modules;
}

/**
* Retourne toutes les classes de modules à charger.
*
* @return Une List contenant les noms des classes à instancier.
*/
private static List<String> getModuleClasses() {
List<String> classes = new ArrayList<String>();

//On liste les fichiers de module
File[] files = new File(Core.getInstance().getApplicationFolder() + "/modules/").listFiles(new ModuleFilter());

for (File f : files) {
JarFile jarFile = null;

try {
//On ouvre le fichier JAR
jarFile = new JarFile(f);

//On récupère le manifest
Manifest manifest = jarFile.getManifest();

//On récupère la classe
String moduleClassName = manifest.getMainAttributes().getValue("Module-Class");

classes.add(moduleClassName);

URL.add(f.toURI().toURL());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

return classes;
}

/**
* Un filtre de fichiers pour les modules. Ce filtre ne prend en compte que les fichiers .jar.
*
* @author Baptiste Wicht
*/
private static class ModuleFilter implements FileFilter {
@Override
public boolean accept(File file) {
return file.isFile() && file.getName().toLowerCase().endsWith(".jar");
}
}
}

La seule différence est qu'on parcourt le dossier « modules » du dossier de l'application. Pour plus d'explications, référez-vous au chapitre 4 où ce code est expliqué.

VI-A-3 - Les extensions et services

On va maintenant créer nos « services ». Le premier fournit aux modules la possibilité de logguer des messages. Dans la pratique, on va utiliser un framework de logging évolué comme log4j, mais pour notre article, on va simplement l'afficher en console avec une indication devant le message.

Voici donc ce que donnerait notre classe :

LogManager.java

package org.dvp.core;

/**
* Manager donnant aux modules la possibilité de logguer des messages.
*
* @author Baptiste Wicht
*/
public class LogManager {
private static final LogManager instance = new LogManager();

/**
* Retourne l'instance unique de LogManager.
*
* @return L'instance unique de LogManager.
*/
public static LogManager getInstance() {
return instance;
}

/**
* Initialise le service.
*/
public void init(){
//Rien à faire ici (c'est pour l'exemple)
}

/**
* Loggue un message.
*
* @param message Le message à logguer.
*/
public void log(String message){
System.out.println("Log : " + message);
}
}

Comme vous pouvez le voir, il n'y a rien de compliqué dans cette classe. J'ai ajouté une méthode init() pour l'exemple, car en pratique, vous aurez presque toujours quelque chose à faire pour charger le service.

On va passer maintenant, au service de vue. Ce service va simplement proposer d'afficher la vue, de la fermer et d'y ajouter du texte. Voici ce qu'il va donner :

ViewManager.java

package org.dvp.core;

/**
* Manager permettant de gérer la vue de l'application.
*
* @author Baptiste Wicht
*/
public class ViewManager {
private static final ViewManager instance = new ViewManager();

private CoreView view;

/**
* Retourne l'instance unique de ViewManager.
*
* @return L'instance unique de ViewManager.
*/
public static ViewManager getInstance() {
return instance;
}

/**
* Initialise le service.
*/
public void init() {
view = new CoreView();
}

/**
* Affiche la vue.
*/
public void display() {
view.setVisible(true);
}

/**
* Ferme la vue.
*/
public void close() {
view.dispose();
}

/**
* Affiche du texte sur la vue.
*
* @param text Le texte à afficher.
*/
public void displayText(String text) {
view.addText(text);
}
}

Là encore, rien de spécial à redire. On va maintenant définir la vue. On va utiliser une JFrame contenant un JTextArea pour afficher le texte. Voici ce que ça pourrait donner :

CoreView.java

package org.dvp.core;

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

/**
* Vue principale de l'application.
*
* @author Baptiste Wicht
*/
public class CoreView extends JFrame {
private final JTextArea textArea;

/**
* Construit la vue.
*/
public CoreView(){
super();

setSize(640, 480);
setTitle(Core.getInstance().getApplicationName());
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowCloseListener());

textArea = new JTextArea("Texte des modules : \n");

add(new JScrollPane(textArea), BorderLayout.CENTER);

setLocationRelativeTo(null);
}

/**
* Ajoute un texte dans la vue.
*
* @param message Le message à ajouter.
*/
public void addText(String message){
textArea.append('\t' + message + '\n');
}

/**
* Le listener pour fermer l'application complète lorsqu'on ferme la fenêtre.
*
* @author Baptiste Wicht
*/
private static class WindowCloseListener extends WindowAdapter {
@Override
public void windowClosing(WindowEvent e) {
Core.getInstance().exit();
}
}
}

Ce qu'on peut noter, c'est qu'on récupère le nom de l'application pour le titre de la fenêtre. Il faut aussi avertir le core de la fermeture de l'application. Dans notre cas, ce n'est pas des plus utiles étant donné qu'il n'y a aucune ressource à libérer. Mais en pratique, on aura toujours quelque chose à libérer dans une application.

VI-B - Un module d'exemple

Maintenant que notre Core est terminé, on va créer un petit module qui va utiliser notre Core.

Ce module sera tout simple, il va juste ajouter deux lignes de texte dans la vue et utiliser la fonction de log du service LogManager pour afficher quelque chose en console.

Voici donc notre module :

package org.dvp.module;

import org.dvp.core.IModule;
import org.dvp.core.LogManager;
import org.dvp.core.ViewManager;

/**
* Module d'exemple tout simple.
*
* @author Baptiste Wicht
*/
public class SimpleModule implements IModule {
@Override
public void plug() {
LogManager.getInstance().log("I'm in !");

ViewManager.getInstance().displayText("Je suis pluggé. ");
ViewManager.getInstance().displayText("J'affiche du texte. ");
}

@Override
public void unplug() {
LogManager.getInstance().log("I'm out !");
}

@Override
public String getName() {
return "Module super simple";
}
}

Rien de spécial non plus à ce niveau-là.

VI-C - L'application

Maintenant qu'on a un core et un module, on va créer l'application permettant de configurer le core et de le lancer.

ApplicationModulaire.java

package org.dvp.application;

import org.dvp.core.Core;

/**
* L'application de notre projet modulaire.
*
* @author Baptiste Wicht
*/
public class ApplicationModulaire {
/**
* Lance le core de l'application.
*
* @param args Les arguments de ligne de commandes. Ignorés.
*/
public static void main(String[] args) {
Core.getInstance().setApplicationFolder(System.getProperty("user.dir"));
Core.getInstance().setApplicationName("Application modulaire 1.0. ");
Core.getInstance().launch();
}
}

On donne donc le dossier courant et le nom de l'application au core et on le démarre.

VI-D - Résultat

On va donc lancer maintenant notre application.

Pour cela, il va falloir déployer nos différentes parties sous forme de fichier .jar.

Le core : on va exporter notre core dans le fichier Core.jar à la racine du dossier de l'application. Rien de spécial à mettre dans le manifest pour cette partie.

Le module : on va exporter notre module dans le fichier SimpleModule.jar dans le sous-dossier modules du dossier de l'application. Il faut spécifier le nom de la classe de module dans le manifest :

MANIFEST.MF

Manifest-Version: 1.0
Module-Class: org.dvp.module.SimpleModule

L'application : on va exporter notre application dans le fichier Application.jar dans le dossier de l'application. Ici, il faut spécifier la classe main dans le fichier manifest et il faut ajouter Core.jar dans le classPath :

MANIFEST.MF

Manifest-Version: 1.0
Main-Class: org.dvp.application.ApplicationModulaire
Class-Path: Core.jar

Maintenant que nos différents fichiers sont créés, on va lancer notre programme. Pour cela, on peut soit lancer Application.jar comme une application normale, soit utiliser la commande java -jar pour bénéficier de la console.

Avec cette deuxième option, voici le résultat :

Vue de l'application

Affichage console

Comme nous le voyons, notre module a bien affiché ses deux lignes dans la vue et ses deux messages dans la console. Notre test est concluant. Nous avons créé une application modulaire !

VII - Conclusion

Voilà, vous avez maintenant les bases pour créer votre première application modulaire. Les concepts restent globalement les mêmes en passant à une application plus grosse. Néanmoins, il faut faire plus attention du côté de la conception, à ne pas surcharger les modules et les services. Il devient également intéressant d'utiliser des interfaces de spécifications pour les services. On peut ainsi donner accès aux modules à un certain panel de fonctionnalité alors que le Core a accès à la totalité des fonctionnalités.

Mais après ce ne sont que des concepts de programmation généraux qui ne se rattachent pas à la programmation modulaire.

VII-A - Les sources

Les sources sont disponibles sous deux formes :

Projets Intellij Idea
Le core
L'application
Le module

Sources uniquement
Le core
L'application
Le module

VII-B - Remerciements

Merci à dgouyette pour sa correction orthographique ainsi qu'à Mikrob pour ses commentaires.