Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Prochaines sessions inter-entreprises : 28-31 mars 2017 / 13-16 juin 2017
Sessions intra-entreprises sur demande.
Inscrivez-vous vite !

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 :

  1. public interface IPlugin
  2. {
  3. String getId();
  4. void run();
  5. }

A titre d'exemple, deux plugins ont été implémentés :

  1. public class HelloWorldPlugin implements IPlugin
  2. {
  3. public static final String ID = "net.thecodersbreakfast.spring.pluginsystem.plugin.HelloWorldPlugin";
  4.  
  5. @Override
  6. public String getId()
  7. { return HelloWorldPlugin.ID;
  8. }
  9.  
  10. @Override
  11. public void run()
  12. { System.out.println("Hello world !");
  13. }
  14. }
  15.  
  16. public class DatePlugin implements IPlugin
  17. {
  18. public static final String ID = "net.thecodersbreakfast.spring.pluginsystem.plugin.DatePlugin";
  19.  
  20. @Override
  21. public String getId()
  22. { return DatePlugin.ID;
  23. }
  24.  
  25. @Override
  26. public void run()
  27. { System.out.println("Il est : " + new Date());
  28. }
  29. }

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
  • ...
  1. public class PluginManager
  2. {
  3. private List<IPlugin> plugins = new ArrayList<IPlugin>();
  4.  
  5. public boolean addPlugin(IPlugin plugin)
  6. { return this.plugins.add(plugin);
  7. }
  8.  
  9. public boolean removePlugin(IPlugin plugin)
  10. { return this.plugins.remove(plugin);
  11. }
  12.  
  13. public List<IPlugin> getPlugins()
  14. { return Collections.unmodifiableList(this.plugins);
  15. }
  16. }

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 :

  1. public class Main
  2. {
  3. public static void main(String[] args)
  4. {
  5. // Recherche des plugins
  6. ClassPathXmlApplicationContext springContext = new ClassPathXmlApplicationContext("spring.xml");
  7. // TODO : détection et récupération des plugins
  8.  
  9. // Enregistrement auprès du PluginManager
  10. PluginManager manager = new PluginManager();
  11. for (IPlugin plugin : plugins.values())
  12. { manager.addPlugin(plugin);
  13. }
  14.  
  15. // Utilisation des plugins
  16. for (IPlugin plugin : manager.getPlugins())
  17. {
  18. System.out.println("\nPlugin : " + plugin.getId());
  19. plugin.run();
  20. }
  21. }
  22. }

Un premier système simple

Le premier système sera relativement simple.
Il comporte 3 étapes :

  1. packaging des plugins
  2. détection des plugins par Spring
  3. 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 :

  1. <beans (...namespaces...) >
  2. <bean class="net.thecodersbreakfast.spring.pluginsystem.plugin.HelloWorldPlugin"/>
  3. </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éfixe classpath: 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 :

  1. <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 :

  1. public class Main
  2. {
  3. public static void main(String[] args)
  4. {
  5. ClassPathXmlApplicationContext springContext = new ClassPathXmlApplicationContext("spring.xml");
  6. Map<String, IPlugin> plugins = springContext.getBeansOfType(IPlugin.class);
  7. for (IPlugin plugin : plugins.values())
  8. {
  9. System.out.println("\nPlugin : " + plugin.getId());
  10. plugin.run();
  11. }
  12. }
  13. }

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.

  1. public class PluginRegistrationBeanPostProcessor implements BeanPostProcessor
  2. {
  3. private PluginManager manager;
  4.  
  5. public PluginRegistrationBeanPostProcessor(PluginManager manager)
  6. { this.manager = manager;
  7. }
  8.  
  9. @Override
  10. public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
  11. {
  12. if (bean instanceof IPlugin)
  13. { manager.addPlugin((IPlugin) bean);
  14. }
  15. return bean;
  16. }
  17.  
  18. @Override
  19. public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
  20. { return bean;
  21. }
  22. }

Le fichier spring.xml s'étoffe un peu, pour déclarer le post-processeur et le PluginManager sur lequel il s'appuie :

  1. <beans (...namespaces...) >
  2.  
  3. <import resource="classpath*:/META-INF/plugin.xml"/>
  4.  
  5. <bean id="pluginManager" class="net.thecodersbreakfast.spring.pluginsystem.PluginManager"/>
  6.  
  7. <bean class="net.thecodersbreakfast.spring.pluginsystem.PluginRegistrationBeanPostProcessor">
  8. <constructor-arg ref="pluginManager"/>
  9. </bean>
  10.  
  11. </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 :

  1. public class Main
  2. {
  3. public static void main(String[] args)
  4. {
  5. // Recherche des plugins
  6. ClassPathXmlApplicationContext springContext = new ClassPathXmlApplicationContext("spring.xml");
  7.  
  8. // Récupération du PluginManager préconfiguré
  9. PluginManager manager = (PluginManager) springContext.getBean("pluginManager");
  10.  
  11. // Utilisation des plugins
  12. for (IPlugin plugin : manager.getPlugins())
  13. {
  14. System.out.println("\nPlugin : " + plugin.getId());
  15. plugin.run();
  16. }
  17. }
  18. }

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

1. Le jeudi 29 janvier 2009, 15:37 par Nicolas

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?)

2. Le jeudi 29 janvier 2009, 17:07 par Olivier Croisier

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

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.