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 !

Interfacer Spring et le Service Provider API

Je vous ai déjà présenté le Service Provider API de Java 6+, qui permet de découvrir dynamiquement au runtime les implémentations disponibles d'une interface donnée.

Nous avions également vu qu'il était possible de parvenir à un résultat similaire dans un environnement Spring, grâce à l'import dynamique de ses fichiers de configuration et à un BeanPostProcessor. Toutefois, cela demandait davantage de travail et ne permettait de découvrir que sur des classes expressément déclarées comme beans Spring.

Dans cet article, nous verrons comment interfacer ces deux technologies, de manière à pouvoir injecter dans un contexte Spring des implémentations non-Spring détectés par le Service Provider API.

Le service et ses implémentations

Premièrement, il nous faut définir un service, dont les implémentations seront découvertes dynamiquement.
Voici son interface, volontairement très simple :

  1. package net.thecodersbreakfast.spring.service;
  2. public interface HelloService
  3. {
  4. void sayHello();
  5. }

Trois implémentations sont disponibles (EnglishHello, FrenchHello, GermanHello), toutes construites sur le même modèle :

  1. package net.thecodersbreakfast.spring.service.impl;
  2. public class FrenchHello implements HelloService
  3. {
  4. @Override
  5. public void sayHello()
  6. { System.out.println("Bonjour");
  7. }
  8. }

Enfin, ces implémentations sont déclarées auprès du Service Provider API dans le fichier "META-INF/services/net.thecodersbreakfast.spring.service.HelloService" :

net.thecodersbreakfast.spring.service.impl.EnglishHello
net.thecodersbreakfast.spring.service.impl.FrenchHello
net.thecodersbreakfast.spring.service.impl.GermanHello

Vous pourrez trouver plus de détails sur le fonctionnement de tout ceci dans l'article de présentation du Service Provider API.

Mise en place du contexte Spring

Il nous faut maintenant mettre en place notre environnement Spring de test.

Le fichier de contexte (applicationContext.xml) est vide pour le moment :

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="
  5. http://www.springframework.org/schema/beans
  6. http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
  7. <!-- TODO : détecter et injecter les implémentations du service HelloService -->
  8. </beans>

Et la classe de test est également rudimentaire :

  1. package net.thecodersbreakfast.spring;
  2. public class Main
  3. {
  4. public static void main(String[] args)
  5. {
  6. ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
  7. // TODO : récupérer les implémentations du service HelloService
  8. }
  9. }

Intégration avec le Service Provider API

Dans l'immense majorité des cas, Spring prend parfaitement en charge l'instanciation (constructeur, méthode factory...) et l'initialisation des beans qu'il gèrera par la suite. Pour les situations exceptionnelles où le développeur doit avoir un contrôle total sur ce processus, Spring propose un mécanisme d'extension : les FactoryBeans (à ne pas confondre avec la BeanFactory, qui représente le registre des beans).

Un FactoryBean est un bean Spring spécialisé dans la construction et l'initialisation d'un objet de type prédéfini, et qui est injecté en lieu et place de cet objet dans la configuration Spring ; il agit en quelque sorte comme un proxy de création dynamique.
C'est le mécanisme que nous allons utiliser pour intégrer nos deux technologies.

L'interface FactoryBean est définie comme suit :

  1. public interface FactoryBean {
  2.  
  3. /**
  4. * Return an instance (possibly shared or independent) of the object
  5. * managed by this factory.
  6. */
  7. Object getObject() throws Exception;
  8.  
  9. /**
  10. * Return the type of object that this FactoryBean creates.
  11. */
  12. Class getObjectType();
  13.  
  14. /**
  15. * Is the object managed by this factory a singleton? That is,
  16. * will getObject() always return the same object
  17. * (a reference that can be cached) ?
  18. */
  19. boolean isSingleton();
  20.  
  21. }

Réfléchissons maintenant à notre implémentation de cette interface.

  • Il sera nécessaire de lui préciser le type du service recherché
  • Elle renverra une Collection d'instances implémentant ce service
  • Afin de garantir que les instances renvoyées seront bien du type demandé, nous utiliserons les types génériques
  • Nous faisons le choix de toujours renvoyer la même collection (il serait possible de relancer la détection à chaque appel)

En voici le code final :

  1. package net.thecodersbreakfast.spring.beanfactory;
  2. public class ServiceLoaderBeanFactory<T> implements FactoryBean
  3. {
  4. private Collection<T> implementations;
  5.  
  6. public ServiceLoaderFactoryBean(Class<T> serviceClass)
  7. {
  8. this.implementations = new ArrayList<T>();
  9. ServiceLoader<T> loader = ServiceLoader.load(serviceClass);
  10. for (T impl : loader)
  11. { implementations.add(impl);
  12. }
  13. }
  14.  
  15. @Override
  16. public Collection<T> getObject() throws Exception
  17. { return implementations;
  18. }
  19.  
  20. @Override
  21. public Class<?> getObjectType()
  22. { return implementations.getClass();
  23. }
  24.  
  25. @Override
  26. public boolean isSingleton()
  27. { return true;
  28. }
  29. }

Test de la solution

Il est temps de tester notre solution.

Tout d'abord, nous devons déclarer notre FactoryBean en tant que bean Spring :

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans (...) >
  3.  
  4. <bean id="hellos" class="net.thecodersbreakfast.spring.beanfactory.ServiceLoaderFactoryBean">
  5. <constructor-arg value="net.thecodersbreakfast.spring.service.HelloService"/>
  6. </bean>
  7.  
  8. </beans>

En récupérant ce bean via un ApplicationContext, on obtient non pas un objet de type FactoryBean, mais bien l'objet que le FactoryBean aura créé - dans notre exemple, une Collection d'instances implémentant le service HelloService.

  1. package net.thecodersbreakfast.spring;
  2. public class Main
  3. {
  4. public static void main(String[] args)
  5. {
  6. ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
  7.  
  8. Collection<HelloService> hellos = (Collection<HelloService>) ctx.getBean("hellos");
  9. for (HelloService hello : hellos)
  10. { hello.sayHello();
  11. }
  12. }
  13. }

L'exécution de ce programme devrait donner un résultat équivalent à celui-ci (l'ordre de détection n'est pas garanti par le Service Provider API) :

Hello
Bonjour
Guten Tag

Notez qu'il est parfaitement possible d'injecter notre collection dans un autre bean, comme n'importe quelle dépendance.
Prenons l'exemple d'un bean Greeter suivant :

  1. package net.thecodersbreakfast.spring;
  2. public class Greeter
  3. {
  4. private Collection<HelloService> hellos;
  5.  
  6. public Collection<HelloService> getHellos()
  7. { return hellos;
  8. }
  9.  
  10. public void setHellos(Collection<HelloService> hellos)
  11. { this.hellos = hellos;
  12. }
  13. }

Injectons-lui la collection :

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans (...) >
  3.  
  4. <bean id="hellos" class="net.thecodersbreakfast.spring.beanfactory.ServiceLoaderFactoryBean">
  5. <constructor-arg value="net.thecodersbreakfast.spring.service.HelloService"/>
  6. </bean>
  7.  
  8. <bean id="greeter" class="net.thecodersbreakfast.spring.Greeter">
  9. <property name="hellos" ref="hellos"/>
  10. </bean>
  11.  
  12. </beans>

Et vérifions que tout fonctionne correctement :

  1. package net.thecodersbreakfast.spring;
  2. public class Main
  3. {
  4. public static void main(String[] args)
  5. {
  6. ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
  7.  
  8. Greeter greeter = (Greeter) ctx.getBean("greeter");
  9. for (HelloService hello : greeter.getHellos())
  10. { hello.sayHello();
  11. }
  12. }
  13. }

Conclusion

En combinant un FactoryBean et le Service Provider API, il est possible d'injecter des dépendances détectées dynamiquement au sein d'un environnement Spring, sans que celles-ci soient informées de l'existence de ce contexte.

Note : Cette fonctionnalité existe déjà dans Spring (cf. package org.springframework.beans.factory.serviceloader), sous une forme plus complexe/complète que celle développée ici. Mais comme on dit, l'important n'est pas la destination, mais bien le voyage lui-même !


Ajouter un commentaire

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