janv.
2009
Découverte automatique de services avec Spring
Vous avez aimé le principe de l'API Service Provider Interface mais vous (ou votre client) n'utilisez pas Java 6 ?
Le but de cet article est de démontrer comment on peut implémenter un système équivalent avec Spring, à travers l'exemple d'un système de plugins minimaliste.
Présentation de l'application d'exemple
Afin de matérialiser les plugins, nous disposons d'une interface IPlugin
:
public interface IPlugin { String getId(); void run(); }
A titre d'exemple, deux plugins ont été implémentés :
public class HelloWorldPlugin implements IPlugin { public static final String ID = "net.thecodersbreakfast.spring.pluginsystem.plugin.HelloWorldPlugin"; @Override public String getId() { return HelloWorldPlugin.ID; } @Override public void run() { System.out.println("Hello world !"); } } public class DatePlugin implements IPlugin { public static final String ID = "net.thecodersbreakfast.spring.pluginsystem.plugin.DatePlugin"; @Override public String getId() { return DatePlugin.ID; } @Override public void run() { System.out.println("Il est : " + new Date()); } }
Le but étant de pouvoir ajouter ou retirer des plugins facilement, ils seront packagés individuellement dans des jars que l'on pourra placer ou non dans le classpath de l'application.
Ensuite, nous avons un PluginManager
, qui représente le service haut niveau de gestion des plugins. Le nôtre restera très simple, mais on pourrait typiquement lui ajouter des fonctions :
- Exposition via JMX afin d'activer ou désactiver les plugins au runtime
- Gestion d'un cycle de vie des plugins
- Gestion des dépendances inter-plugins
- ...
public class PluginManager { private List<IPlugin> plugins = new ArrayList<IPlugin>(); public boolean addPlugin(IPlugin plugin) { return this.plugins.add(plugin); } public boolean removePlugin(IPlugin plugin) { return this.plugins.remove(plugin); } public List<IPlugin> getPlugins() { return Collections.unmodifiableList(this.plugins); } }
Pour finir, une classe Main
chargera le contexte Spring et permettra de tester que la détection des plugins a fonctionné, en affichant leurs IDs et en les exécutant :
public class Main { public static void main(String[] args) { // Recherche des plugins ClassPathXmlApplicationContext springContext = new ClassPathXmlApplicationContext("spring.xml"); // TODO : détection et récupération des plugins // Enregistrement auprès du PluginManager PluginManager manager = new PluginManager(); for (IPlugin plugin : plugins.values()) { manager.addPlugin(plugin); } // Utilisation des plugins for (IPlugin plugin : manager.getPlugins()) { System.out.println("\nPlugin : " + plugin.getId()); plugin.run(); } } }
Un premier système simple
Le premier système sera relativement simple.
Il comporte 3 étapes :
- packaging des plugins
- détection des plugins par Spring
- récupération et utilisation des plugins par la classe
Main
Packaging des plugins
Tout d'abord, comme indiqué plus haut, chaque plugin sera placé dans un jar, accompagné de sa définition Spring :
helloworldplugin.jar +-- net | +-- thecodersbreakfast | +-- spring | +-- pluginsystem | +-- plugin | +-- HelloWorldPlugin.class +-- META-INF +-- plugin.xml +-- MANIFEST.MF
Le fichier Spring contient simplement la déclaration du plugin comme un bean :
<beans (...namespaces...) > <bean class="net.thecodersbreakfast.spring.pluginsystem.plugin.HelloWorldPlugin"/> </beans>
Détection des plugins
Cette phase tire parti de l'association de deux fonctionnalités de Spring :
- L'import de fichiers de configuration depuis d'autres fichiers, grâce à la directive
<import resource="..."/>
. - Le préfixe spécial
classpath*:
, qui est une extension du préfixeclasspath:
et qui recherche toutes les instances de la ressource demandée présentes dans le classpath.
Ainsi, pour détecter tous les descripteurs des plugins présents dans le classpath, il suffit d'écrire dans notre fichier spring.xml
:
<import resource="classpath*:/META-INF/plugin.xml" />
Ainsi, tous les beans des plugins seront importés automatiquement dans l'ApplicationContext
de Spring.
Utilisation des plugins
Maintenant que tous les plugins sont chargés dans l'ApplicationContext
de Spring, voyons comment les récupérer dans notre classe Main
.
Nous ne pouvons pas savoir à l'avance quels plugins seront présents, ni sous quel nom ils seront déclarés par leur auteur (dans notre exemple plus haut, nous ne les avons même pas nommés !). Nous ne pouvons donc pas utiliser la traditionnelle méthode getBean(String beanName)
de l'ApplicationContext
.
En revanche, il existe une méthode getBeansOfType(Class class)
qui permet de récupérer tous les beans d'un type donné - au hasard, IPlugin
?
Voici donc notre classe Main
finalisée :
public class Main { public static void main(String[] args) { ClassPathXmlApplicationContext springContext = new ClassPathXmlApplicationContext("spring.xml"); Map<String, IPlugin> plugins = springContext.getBeansOfType(IPlugin.class); for (IPlugin plugin : plugins.values()) { System.out.println("\nPlugin : " + plugin.getId()); plugin.run(); } } }
Lorsque les jars contenant les deux plugins sont présents dans le classpath, voici l'affichage obtenu :
Plugin : net.thecodersbreakfast.spring.pluginsystem.plugin.HelloWorldPlugin Hello world ! Plugin : net.thecodersbreakfast.spring.pluginsystem.plugin.DatePlugin Il est : Thu Jan 29 01:03:38 CET 2009
Avantages et inconvénients
Cette solution est simple et fonctionnelle, mais n'est pas très satisfaisante, car :
- Le processus d'initialisation du
PluginManager
est manuel. - Le
PluginManager
peut être initialisé longtemps après que l'application est lancée; les éventuelles erreurs liées aux plugins (ex: dépendances non satisfaites) sont donc détectées très tardivement.
Voyons comment nous pouvons améliorer cela.
Un système amélioré
Il serait bien plus intéressant que les plugins soient enregistrés auprès du PluginManager
automatiquement, et surtout avant le lancement de l'application.
Pour réaliser cela, nous allons nous brancher directement sur le mécanisme d'instanciation des beans de Spring à l'aide d'un BeanPostProcessor
: ce type de composant est appelé juste après l'instanciation et la configuration de chaque bean par le conteneur. Dans notre cas, nous pratiquerons un petit test pour déterminer si le bean est de type IPlugin
, auquel cas nous l'enregistrerons auprès du PluginManager
.
Voici le PluginRegistrationBeanPostProcessor
. Notez que le post-processeur a besoin d'une référence sur le PluginManager
, qui lui est fournie en argument du constructeur.
public class PluginRegistrationBeanPostProcessor implements BeanPostProcessor { private PluginManager manager; public PluginRegistrationBeanPostProcessor(PluginManager manager) { this.manager = manager; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof IPlugin) { manager.addPlugin((IPlugin) bean); } return bean; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } }
Le fichier spring.xml s'étoffe un peu, pour déclarer le post-processeur et le PluginManager
sur lequel il s'appuie :
<beans (...namespaces...) > <import resource="classpath*:/META-INF/plugin.xml"/> <bean id="pluginManager" class="net.thecodersbreakfast.spring.pluginsystem.PluginManager"/> <bean class="net.thecodersbreakfast.spring.pluginsystem.PluginRegistrationBeanPostProcessor"> <constructor-arg ref="pluginManager"/> </bean> </beans>
En revanche, la classe Main
se trouve grandement simplifiée : il suffit de récupérer le PluginManager
, déjà pré-configuré, auprès de Spring :
public class Main { public static void main(String[] args) { // Recherche des plugins ClassPathXmlApplicationContext springContext = new ClassPathXmlApplicationContext("spring.xml"); // Récupération du PluginManager préconfiguré PluginManager manager = (PluginManager) springContext.getBean("pluginManager"); // Utilisation des plugins for (IPlugin plugin : manager.getPlugins()) { System.out.println("\nPlugin : " + plugin.getId()); plugin.run(); } } }
Grâce à cette nouvelle solution, toute erreur d'initialisation du PluginManager
empêche le démarrage de l'application, ce qui permet de détecter les problèmes plus rapidement. De plus, toute la plomberie est désormais masquée, et le code client est simplifié.
Conclusion
Une bonne connaissance des capacités et mécanismes de Spring permet de développer facilement des fonctionnalités puissantes et intéressantes. Ici, nous avons vu comment reproduire et même améliorer le système du Service Provider API, dans tout environnement Java 1.4+.
Les sources de la version simple et de la version améliorée sont disponibles en pièce jointe.
Commentaires
Un petit HS à propos de Spring : j'ai lu sur le blog de Spring Source que devais passer ta certification Spring (ça devrait être chose faite maintenant). Un petit retour quant à ton passage ? (obtenu ? difficulté ? avis/remarque?)
Il y a eu un problème technique au centre d'examen, je n'ai pas pu la passer. J'espère pouvoir réessayer bientôt...