juil.
2009
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 :
package net.thecodersbreakfast.spring.service; public interface HelloService { void sayHello(); }
Trois implémentations sont disponibles (EnglishHello
, FrenchHello
, GermanHello
), toutes construites sur le même modèle :
package net.thecodersbreakfast.spring.service.impl; public class FrenchHello implements HelloService { @Override public void sayHello() { System.out.println("Bonjour"); } }
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 :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <!-- TODO : détecter et injecter les implémentations du service HelloService --> </beans>
Et la classe de test est également rudimentaire :
package net.thecodersbreakfast.spring; public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); // TODO : récupérer les implémentations du service HelloService } }
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 :
public interface FactoryBean { /** * Return an instance (possibly shared or independent) of the object * managed by this factory. */ Object getObject() throws Exception; /** * Return the type of object that this FactoryBean creates. */ Class getObjectType(); /** * Is the object managed by this factory a singleton? That is, * will getObject() always return the same object * (a reference that can be cached) ? */ boolean isSingleton(); }
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 :
package net.thecodersbreakfast.spring.beanfactory; public class ServiceLoaderBeanFactory<T> implements FactoryBean { private Collection<T> implementations; public ServiceLoaderFactoryBean(Class<T> serviceClass) { this.implementations = new ArrayList<T>(); ServiceLoader<T> loader = ServiceLoader.load(serviceClass); for (T impl : loader) { implementations.add(impl); } } @Override public Collection<T> getObject() throws Exception { return implementations; } @Override public Class<?> getObjectType() { return implementations.getClass(); } @Override public boolean isSingleton() { return true; } }
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 :
<?xml version="1.0" encoding="UTF-8"?> <beans (...) > <bean id="hellos" class="net.thecodersbreakfast.spring.beanfactory.ServiceLoaderFactoryBean"> <constructor-arg value="net.thecodersbreakfast.spring.service.HelloService"/> </bean> </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.
package net.thecodersbreakfast.spring; public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Collection<HelloService> hellos = (Collection<HelloService>) ctx.getBean("hellos"); for (HelloService hello : hellos) { hello.sayHello(); } } }
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 :
package net.thecodersbreakfast.spring; public class Greeter { private Collection<HelloService> hellos; public Collection<HelloService> getHellos() { return hellos; } public void setHellos(Collection<HelloService> hellos) { this.hellos = hellos; } }
Injectons-lui la collection :
<?xml version="1.0" encoding="UTF-8"?> <beans (...) > <bean id="hellos" class="net.thecodersbreakfast.spring.beanfactory.ServiceLoaderFactoryBean"> <constructor-arg value="net.thecodersbreakfast.spring.service.HelloService"/> </bean> <bean id="greeter" class="net.thecodersbreakfast.spring.Greeter"> <property name="hellos" ref="hellos"/> </bean> </beans>
Et vérifions que tout fonctionne correctement :
package net.thecodersbreakfast.spring; public class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); Greeter greeter = (Greeter) ctx.getBean("greeter"); for (HelloService hello : greeter.getHellos()) { hello.sayHello(); } } }
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 !