3. Implémentation des modules▲
Maintenant que nous avons vu en détails les caractéristiques des modules, nous allons voir comment mettre en oeuvre 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 Jars.
Nos modules seront donc contenus dans des fichiers Jars.
Voici pour ce qui est de la répré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 simple pour le moment. Un module doit tout d'abord être rattachable et ensuite détachable. C'est-à -dire le moment ou 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 tout simple décrivant notre module :
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 :
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 chacun 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-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 Jars, 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 :
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 jars dans le classpath pour les charger étant donné que l'application principale ne les connaît pas.
4. 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 son nom l'indique est un chargeur de classes.
4.1. 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 classe 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 classe 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 URLs des fichiers Jar
- La seconde va charger les différents modules en se servant d'un ClassLoader créé à partir des URLs 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.
public
class
ModuleLoader {
private
static
List<
URL>
urls =
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);
urls.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 parcours les fichiers Jar existant 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 URLs.
Bien entendu ce code est perfectible, il faudrait traiter le cas où le fichier JAR n'a pas de manifest ou alors le cas ou 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 :
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
(
urls.toArray
(
new
URL[urls.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 URLs qu'on a récupéré avec les fichiers JARs. On va utiliser un URLClassLoader qui permet de charger des classes depuis des emplacements définis par des URLs. 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 intéraction 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éfléction 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 comme 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).
4.2. 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 Jars 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 Jars 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 rajouter une URL dans la liste des endroits ou le ClassLoader va chercher pour charger une classe.
Voici une méthode permettant d'ajouter une URL au 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.