Développer une application modulaire en Java


précédentsommairesuivant

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 :

IModule.java
Sélectionnez
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
Sélectionnez
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.MF
Sélectionnez
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 :

Utilisation des services de Java 6
Sélectionnez
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.

ModuleLoader.java
Sélectionnez
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 :

loadModules()
Sélectionnez
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 :

Ajout de classes dans le ClassLoader système
Sélectionnez
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.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  










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 ni 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.