Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step8]
Dans le billet précédant [step7] nous avons montré l’interêt d’utiliser Spring Dynamic Modules pour déclarer l’enregistrement des services via <osgi:service. Nous avons vu aussi que le bundle Spring Extender s’occupait de créer les org.springframework.context.ApplicationContext des bundles avec des fichiers de configuration XML de Spring.
Dans ce billet nous allons utiliser Spring Dynamic Modules pour déclarer la consommation du service UserService, effectuée dans le bundle client org.dynaresume.simpleosgiclient via <osgi:reference.
Pour rappel la consommation du service UserService s’effectue dans le bundle org.dynaresume.simpleosgiclient dans le thread FindAllUsersThread.
Voici un schéma de ce que nous allons effectuer dans ce billet :
Ce schéma montre que la consommation du service UserService s’effectue déclarativement avec <osgi:reference. Cette déclaration se trouve dans le fichier XML Spring META-INF/spring/module-osgi-context.xml du bundle client org.dynaresume.simpleosgiclient.
Nous verrons dans ce billet trois techniques pour récupérer l’instance UserService déclarée :
- ServiceTracker – ApplicationContext : cette solution consiste à récupérer via ServiceTracker l’instance ApplicationContext du fichier XML Spring spring/module-osgi-context.xml. La Thread FindAllUsersThread utilise ensuite cette instance pour récupérer le service.
- ApplicationContextAware : cette solution consiste à déclarer la Thread FindAllUsersThread dans un fichier XML Spring. La classe FindAllUsersThread implémente ApplicationContextAwarepour récupérer l’instance ApplicationContext du bundle. La Thread FindAllUsersThread utilise ensuite cette instance pour récupérer le service.
- UserService – Injection de dépendances : cette solution consiste à laisser Spring injecter le service dans le Thread FindAllUsersThread. Cette solution est la plus simple et est indépendante de l’API Spring.
<osgi:reference = consommation services OSGi
Dans ce billet nous allons nous concentrer sur le bundle client org.dynaresume.simpleosgiclient qui consomme le service UserService dans son Thread FindAllUsersThread qui affiche toutes les 5 secondes la liste des Users récupérée du service UserService. Ce service est récupéré programmatiquement à l’aide d’un ServiceTracker OSGi initialisé dans l’Actvator du bundle client .
ServiceTracker userServiceTracker = new ServiceTracker(bundleContext, UserService.class.getName(), null); userServiceTracker.open(); ...
puis utilisé dans la Thread FindAllUsersThread comme ceci :
UserService userService = (UserService)userServiceTracker.getService();
Avec Spring Dynamic Module il est possible de déclarer la consommation du service UserService dans le registre de services OSGi en XML avec <osgi:reference comme ceci :
<osgi:reference id="userService" interface="org.dynaresume.services.UserService" />
Pour utiliser cette déclaration Spring, il faut charger ce contenu XML dans une instance org.springframework.context.ApplicationContext et pouvoir par exemple ensuite effectuer :
UserService userService = (UserService)applicationContext.getBean("userService");
Nous avons vu dans le billet précédant que les fichiers XML Spring n’étaient pas chargés par le bundle lui-même mais que ce dernier déléguait le chargement de ces fichiers au bundle Spring Extender. La question suivante se pose : comment récupérer dans notre bundle client, l’instance org.springframework.context.ApplicationContext chargée par le bundle Spring Extender?
Après avoir lu ce post très intéressant, j’ai tenté de mettre en oeuvre les solutions proposées dans notre bundle client. Les 2 solutions proposées sont :
- les instances ApplicationContext chargé par le bundle Spring Extender sont enregistrées dans le registre de services OSGi qui peuvent être récupéré par un ServiceTracker OSGi. Cette technique est mise en oeuvre dans la section ServiceTracker – ApplicationContext.
- l’interface ApplicationContextAware permet de récupérer l’ApplicationContext du bundle. Dans notre cas notre Thread implémentera cette interface pour récupérer l’applicationContext de notre bundle. Cette technique est mise en oeuvre dans la section ApplicationContextAware.
Ces 2 solutions impliquent que notre bundle client org.dynaresume.simpleosgiclient sera lié à l’API Spring via son MANIFEST.MF. Dans notre cas, il existe une 3eme solution qui est celle de la Dépendance d’Injection qui est la plus propre et qui ne requiert aucune dépendance à l’API Spring. Cette solution est mise en oeuvre dans la section UserService – Injection de dépendances.
Prérequis
Vous pouvez télécharger org.dynaresume_step8-start.zip qui contient les projets expliqués dans ce pré-requis.
Avant de démarrer ce billet nous devons préparer le workspace avec les projets suivants :
- org.dynaresume_step7-spring-osgi.zip qui contient les projets du code expliqué ci-dessous.
- org.dynaresume_step7_spring-target-platform.zip qui contient la Target Platform avec les bundles Spring DM requis.
FindAllUsersThread
Dans ce billet nous allons tracer l’arrêt/lancement de la Thread pour vérifier que nos solutions déclaratives fonctionnent correctement. Pour cela ajoutez le code suivant dans la classe org.dynaresume.simpleosgiclient.internal.FindAllUsersThread
@Override public synchronized void start() { System.out.println("Start Thread FindAllUsersThread"); super.start(); } @Override public void interrupt() { System.out.println("Interrupt Thread FindAllUsersThread"); super.interrupt(); }
Lors de la rédaction de ce billet je me suis rendu compte qu’il y avait un bug dans la classe FindAllUsersThread qui ne s’arrêtait pas correctement. Ce bug est génant pour les tests que nous allons faire dans la suite du billet lorsque nous arrêterons le bundle client org.dynaresume.simpleosgiclient.
Pour cela modifiez la méthode FindAllUsersThread#run() comme suit :
@Override public void run() { while (!super.isInterrupted()) { try { // 1. Get UserService UserService userService = getUserService(); if (userService != null) { // 2. Display users by using UserServive displayUsers(userService); } } catch (Throwable e) { e.printStackTrace(); } finally { try { if (!super.isInterrupted()) sleep(TIMER); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } } }
La différence avec le code précédemment est le test sur le while qui était
while (super.isAlive())
corrigé par
while (!super.isInterrupted())
Dans le catch InterruptedException, il est important d’effectuer :
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
pour bien arrêter le Thread.
ServiceTracker – ApplicationContext
Vous pouvez télécharger org.dynaresume_step8-applicationContext-servicetracker.zip qui contient les projets du code expliqué ci-dessous.
Dans cette section nous allons déclarer la consommation du service UserService dans le fichier XML Spring spring/module-osgi-context.xml de notre bundle client org.dynaresume.simpleosgiclient comme ceci :
<osgi:reference id="userService" interface="org.dynaresume.services.UserService" />
Ce fichier XML est chargé par le bundle Spring Extender dans une instance org.springframework.context.ApplicationContext ou il est ensuite possible de récupérer le service UserService comme un bean classique :
UserService userService = (UserService)applicationContext.getBean("userService");
Le bundle Spring Extender enregistre les instances ApplicationContext chargées, dans le registre de service OSGi, ce qui permet de les récupérer comme n’importe quel autre service. Autrement dit on peut initialiser le ServiceTracker OSGi pour récupérer l’instance ApplicationContext chargé par le bundle Spring Extender comme ceci :
ServiceTracker applicationContextServiceTracker = new ServiceTracker(context, ApplicationContext.class.getName(), null);
puis ensuite l’utiliser comme ceci :
ApplicationContext applicationContext = (ApplicationContext)applicationContextServiceTracker.getService();
module-osgi-context.xml
Créez le fichier XML Spring de configuration module-osgi-context.xml dans le répertoire spring du bundle org.dynaresume.simpleosgiclient qui déclare la consommation du service avec le contenu XML suivant :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:osgi="http://www.springframework.org/schema/osgi" xsi:schemaLocation="http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi-1.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <osgi:reference id="userService" interface="org.dynaresume.services.UserService" /> </beans>
L’ID du bean permettra ensuite de récupérer le service comme un bean classique :
UserService userService = (UserService)applicationContext.getBean("userService");
MANIFEST.MF
Pour pouvoir utiliser l’interface ApplicationContext vous devez importez les packages suivant :
- org.springframework.context
- org.springframework.core.io
- org.springframework.beans.factory
Activator client
Jusqu’a maintenant notre Activator client initialisait un ServiceTracker sur le service UserService, maintenant nous allons l’initialiser sur l’ApplicationContext Spring comme ceci :
ServiceTracker applicationContextServiceTracker = new ServiceTracker(context, ApplicationContext.class.getName(), null);
Pour cela modifier la classe org.dynaresume.simpleosgiclient.internal.Activator comme suit :
package org.dynaresume.simpleosgiclient.internal; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; import org.springframework.context.ApplicationContext; public class Activator implements BundleActivator { private ServiceTracker applicationContextServiceTracker = null; private FindAllUsersThread findAllUsersThread = null; public void start(BundleContext context) throws Exception { System.out.println("Start Bundle [" + context.getBundle().getSymbolicName() + "]"); // Create and open the MovieFinder ServiceTracker applicationContextServiceTracker = new ServiceTracker(context, ApplicationContext.class.getName(), null); applicationContextServiceTracker.open(); // Start Thread which call UserService#findAllUsers(); findAllUsersThread = new FindAllUsersThread(applicationContextServiceTracker); findAllUsersThread.start(); } public void stop(BundleContext context) throws Exception { System.out.println("Stop Bundle [" + context.getBundle().getSymbolicName() + "]"); // Stop Thread which call UserService#findAllUsers(); findAllUsersThread.interrupt(); findAllUsersThread = null; // Close the MovieFinder ServiceTracker applicationContextServiceTracker.close(); } }
FindAllUsersThread
Le service tracker OSGi permet de « tracker » les enregistrement/dés-enregistrement de l’instance ApplicationContext du bundle (et plus du service UserService). Nous devons modifier le Thread FindAllUsersThread qui consomme le service userService pour rechercher ce service dans l’instance ApplicationContext trackée :
ApplicationContext applicationContext = (ApplicationContext) applicationContextServiceTracker.getService(); ... UserService userService = (UserService)applicationContext.getBean("userService");
Pour cela modifiez la classe org.dynaresume.simpleosgiclient.internal.FindAllUsersThread comme suit :
package org.dynaresume.simpleosgiclient.internal; import java.util.Collection; import org.dynaresume.domain.User; import org.dynaresume.services.UserService; import org.osgi.util.tracker.ServiceTracker; import org.springframework.context.ApplicationContext; public class FindAllUsersThread extends Thread { private static final long TIMER = 5000; private ServiceTracker applicationContextServiceTracker; public FindAllUsersThread(ServiceTracker applicationContextServiceTracker) { this.applicationContextServiceTracker = applicationContextServiceTracker; } @Override public void run() { while (!super.isInterrupted()) { try { // 1. Get UserService UserService userService = getUserService(); if (userService != null) { // 2. Display users by using UserServive displayUsers(userService); } } catch (Throwable e) { e.printStackTrace(); } finally { try { if (!super.isInterrupted()) sleep(TIMER); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } } } private UserService getUserService() { System.out .println("--- Get ApplicationContext from OSGi services registry with ServiceTracker ---"); ApplicationContext applicationContext = (ApplicationContext) applicationContextServiceTracker.getService(); if (applicationContext != null) { return (UserService) applicationContext.getBean("userService"); } System.out.println(" Cannot get UserService=> ApplicationContext is null!"); return null; } private void displayUsers(UserService userService) { Collection users = userService.findAllUsers(); for (User user : users) { System.out.println("User [login=" + user.getLogin() + ", password=" + user.getPassword() + "]"); } } }
Test <osgi:reference
Ici nous allons tester si la déclaration de la consommation du service OSGi UserService fonctionne bien. Les tests que allons effectuer consistent à stopper/lancer les bundles client org.dynaresume.simpleosgiclient et service org.dynaresume.services.impl et vérifier que le service UserService soit disponibles ou non selon les bundles stoppés/lancés.
Start/Stop bundle services implémentation
Ici nous allons tester le lancement/arrêt du bundle service org.dynaresume.services.impl. Tappez ss (Short Status) dans la console pour identifier les ID des bundles pour ensuite les arrêter et stopper. Dans mon cas j’ai :
51 ACTIVE org.dynaresume.services.impl_1.0.0.qualifier
52 ACTIVE org.dynaresume.simpleosgiclient_1.0.0.qualifier
Arrêtez le bundle service en saisissant stop 51. La console OSGi affiche :
Stop Bundle [org.dynaresume.services.impl]
osgi> --- Get ApplicationContext from OSGi services registry with ServiceTracker ---
Cette trace montre que le bundle service s’arrête et que le bundle client tente de consommer le service UserService. La console OSGi reste bloquée. Ce comportement s’explique par le fait que le service déclaré est recherché dans le registre de services OSGi pendant 5 minutes. Au bout de 5 minutes (timeout), une erreur s’affiche pour indiquer qu’il est impossible de récupérer le service UserService. Pour éviter d’attendre 5 minutes, il est possible de configurer le timeout dans la déclaration de la consommation du service à l’aide de l’attribut timeout. Modifiez le fichier XML spring/module-osgi-context.xml en indiquant un timeout de 1000 ms comme ceci :
<osgi:reference id="userService" interface="org.dynaresume.services.UserService" timeout="1000" />
A peu près toutes les secondes la console affiche l’erreur suivante :
--- Get ApplicationContext from OSGi services registry with ServiceTracker ---
org.springframework.osgi.service.ServiceUnavailableException: service matching filter=[(objectClass=org.dynaresume.services.UserService)] unavailable
qui indique que le service UserService ne peut pas être récupéré. Relancez ensuite le bundle service en saississant start 51. La console OSGi affiche la trace suivante :
Start Bundle [org.dynaresume.services.impl]
15672 [OSGi Console] INFO org.springframework.osgi.extender.support.DefaultOsgiApplicationContextCreator - Discovered configurations {osgibundle:/META-INF/spring/*.xml} in bundle [DynaResume Services Implementation (org.dynaresume.services.impl)]
osgi> 15672 [SpringOsgiExtenderThread-5] INFO org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext - Refreshing ...
--- Get ApplicationContext from OSGi services registry with ServiceTracker ---
User [login=angelo, password=]
User [login=djo, password=]
User [login=keulkeul, password=]
User [login=pascal, password=]
Cette trace montre que le bundle service est relancé, ce qui provoque le chargement des fichiers XML Spring qui publie le service UserServiceImpl. Le bundle client peut a nouveau consommer le service pour afficher la liste des User.
Start/Stop bundle client
Arrêtez le bundle client en saisissant stop 52. La console OSGi affiche :
stop 52
...
90297 [Timer-1] INFO org.springframework.osgi.extender.internal.activator.ContextLoaderListener - Application context succesfully closed (OsgiBundleXmlApplicationContext(bundle=org.dynaresume.simpleosgiclient, config=osgibundle:/META-INF/spring/*.xml))
Stop Bundle [org.dynaresume.simpleosgiclient]
Interrupt Thread FindAllUsersThread
osgi> java.lang.InterruptedException: sleep interrupted
Interrupt Thread FindAllUsersThread
at java.lang.Thread.sleep(Native Method)
at org.dynaresume.simpleosgiclient.internal.FindAllUsersThread.run(FindAllUsersThread.java:47)
Cette trace montre que :
- le fichier XML Spring du bundle client est déchargé :
90297 [Timer-1] INFO org.springframework.osgi.extender.internal.activator.ContextLoaderListener - Application context succesfully closed (OsgiBundleXmlApplicationContext(bundle=org.dynaresume.simpleosgiclient, config=osgibundle:/META-INF/spring/*.xml))
- le bundle client est arrêté :
Stop Bundle [org.dynaresume.simpleosgiclient]
- la Thread est arrêtée :
Interrupt Thread FindAllUsersThread
osgi> java.lang.InterruptedException: sleep interrupted
Interrupt Thread FindAllUsersThread
at java.lang.Thread.sleep(Native Method)
at org.dynaresume.simpleosgiclient.internal.FindAllUsersThread.run(FindAllUsersThread.java:47)
La console OSGi n’affiche plus la liste des User, ce qui montre que le bundle client arrêté, stoppe correctement la Thread FindAllUsersThread.
Relancez ensuite le bundle client en saississant start 52. La console OSGi affiche la trace suivante :
start 52
Start Bundle [org.dynaresume.simpleosgiclient]
Start Thread FindAllUsersThread
161281 [OSGi Console] INFO org.springframework.osgi.extender.support.DefaultOsgiApplicationContextCreator - Discovered configurations {osgibundle:/META-INF/spring/*.xml} in bundle [DynaResume Simple OSGi Client (org.dynaresume.simpleosgiclient)]
--- Get ApplicationContext from OSGi services registry with ServiceTracker ---
User [login=angelo, password=]
User [login=djo, password=]
User [login=keulkeul, password=]
User [login=pascal, password=]
Cette trace montre que :
- le bundle client est lancé :
Start Bundle [org.dynaresume.simpleosgiclient]
- la Thread FindAllUsersThread est lancée :
Start Thread FindAllUsersThread
- le fichier XML Spring du bundl eest rechargé :
161281 [OSGi Console] INFO org.springframework.osgi.extender.support.DefaultOsgiApplicationContextCreator - Discovered configurations {osgibundle:/META-INF/spring/*.xml} in bundle [DynaResume Simple OSGi Client (org.dynaresume.simpleosgiclient)]
- la consommation du service UserService s’effectue à nouveau :
--- Get ApplicationContext from OSGi services registry with ServiceTracker ---
User [login=angelo, password=]
User [login=djo, password=]
User [login=keulkeul, password=]
User [login=pascal, password=]
ServiceTracker & filtre
Notre ServiceTracker permet de récupérer l’instance ApplicationContext de notre bundle client. Mais nous avons eu de la chance de récupérer cette instance et pas une autre d’un autre bundle. En effet pour s’en rendre compte nous allons appeler la méthode ServiceTracker#getServices() qui retourne la liste des services trackés par le ServiceTracker OSGi. Dans notre cas cette liste sera une liste de ApplicationContext, plus exactement l’implémentation org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext.
importez les packages org.springframework.osgi.context.support pour pouvoir utiliser la classe org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext.
Pour afficher la liste des ApplicationContext trackés par le ServiceTracker, modifiez la méthode FindAllUsersThread#getUserService() comme ceci :
private UserService getUserService() { System.out .println("--- Get ApplicationContext from OSGi services registry with ServiceTracker ---"); Object[] services = applicationContextServiceTracker.getServices(); if (services != null) { for (int i = 0; i < services.length; i++) { OsgiBundleXmlApplicationContext context = (OsgiBundleXmlApplicationContext )services[i]; System.out.println("ApplicationContext coming from : [" + context.getBundle().getSymbolicName() + "]"); } } ... }
Apres avoir relancé, la console OSGi affiche la trace suivante :
--- Get ApplicationContext from OSGi services registry with ServiceTracker ---
ApplicationContext coming from : [org.dynaresume.services.impl]
ApplicationContext coming from : [org.dynaresume.simpleosgiclient]
qui montre que 2 instances ApplicationContext sont trackées qui appartiennent aux 2 bundles qui hébérgent des fichiers XML Spring. Comme nous pouvons le voir nous avons eu la chance de récupérer l’ApplicationContext du bundle client. Pour s’assurer de récupérer l’ApplicationContext du bundle client, nous pouvons effectuer un filtre lors de l’initialisation du ServiceTracker :
String filter = "(&(objectClass=org.springframework.context.ApplicationContext)(org.springframework.context.service.name=" + context.getBundle().getSymbolicName() + "))"; applicationContextServiceTracker = new ServiceTracker(context, FrameworkUtil.createFilter(filter), null);
Pour cela modifiez la classe org.dynaresume.simpleosgiclient.internal.Activator comme suit :
package org.dynaresume.simpleosgiclient.internal; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.util.tracker.ServiceTracker; public class Activator implements BundleActivator { private ServiceTracker applicationContextServiceTracker = null; private FindAllUsersThread findAllUsersThread = null; public void start(BundleContext context) throws Exception { System.out.println("Start Bundle [" + context.getBundle().getSymbolicName() + "]"); String filter = "(&(objectClass=org.springframework.context.ApplicationContext)(org.springframework.context.service.name=" + context.getBundle().getSymbolicName() + "))"; // Create and open the MovieFinder ServiceTracker applicationContextServiceTracker = new ServiceTracker(context, FrameworkUtil.createFilter(filter), null); applicationContextServiceTracker.open(); // Start Thread which call UserService#findAllUsers(); findAllUsersThread = new FindAllUsersThread( applicationContextServiceTracker); findAllUsersThread.start(); } public void stop(BundleContext context) throws Exception { System.out.println("Stop Bundle [" + context.getBundle().getSymbolicName() + "]"); // Stop Thread which call UserService#findAllUsers(); findAllUsersThread.interrupt(); findAllUsersThread = null; // Close the MovieFinder ServiceTracker applicationContextServiceTracker.close(); } }
Relancez et vous pourrez constater que la console affiche que l’ApplicationContext du bundle client :
--- Get ApplicationContext from OSGi services registry with ServiceTracker ---
ApplicationContext coming from : [org.dynaresume.simpleosgiclient]
ApplicationContextAware
Vous pouvez télécharger org.dynaresume_step8-applicationContextAware.zip qui contient les projets du code expliqué ci-dessous.
Dans cette section nous allons toujours déclarer la consommation du service UserService dans le fichier XML Spring spring/module-osgi-context.xml de notre bundle client org.dynaresume.simpleosgiclient comme ceci :
<osgi:reference id="userService" interface="org.dynaresume.services.UserService" />
Mais aussi déclarer l’instanciation de la Thread FindAllUsersThread, son arrêt et son lancement dans le fichier XML Spring spring/module-context.xml de notre bundle client org.dynaresume.simpleosgiclient comme ceci :
<bean id="FindAllUsersThread" class="org.dynaresume.simpleosgiclient.internal.FindAllUsersThread" init-method="start" destroy-method="interrupt"> </bean>
Le Thread FindAllUsersThread étant instancié par le conteneur Spring, celle-ci est capable de récupérer l’ApplicationContext qui a permis de l’instancier en implémentatnt l’interface org.springframework.context.ApplicationContextAware :
package org.springframework.context; import org.springframework.beans.BeansException; public interface ApplicationContextAware { void setApplicationContext(ApplicationContext applicationContext) throws BeansException; }
La Thread FindAllUsersThread qui implémente ApplicationContextAware :
public class FindAllUsersThread extends Thread implements ApplicationContextAware { private ApplicationContext applicationContext; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }; ... }
peut ensuite utiliser l’instance applicationContext pour consommer le service UserService :
UserService userService = (UserService) applicationContext.getBean("userService");
MANIFEST.MF
Pour pouvoir utiliser l’interface ApplicationContext vous devez importez les packages suivant :
- org.springframework.beans
- org.springframework.beans.factory
- org.springframework.context
- org.springframework.core
Activator client
Le bundle Activator du client qui s’occupait jusqu’a maintenant d’instancier un ServiceTracker et la Thread FindAllUsersThread n’a plus besoin de s’occuper de ceci. En effet ici c’est le conteneur Spring qui va instancier la Thread, la démarrer et lui fournir l’instance ApplicationContext. Pour cela modifiez le code org.dynaresume.simpleosgiclient.internal.Activator comme suit :
package org.dynaresume.simpleosgiclient.internal; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { //private ServiceTracker userServiceTracker = null; //private FindAllUsersThread findAllUsersThread = null; public void start(BundleContext context) throws Exception { System.out.println("Start Bundle [" + context.getBundle().getSymbolicName() + "]"); // Create and open the MovieFinder ServiceTracker // userServiceTracker = new ServiceTracker(context, UserService.class // .getName(), null); // userServiceTracker.open(); // // // Start Thread which call UserService#findAllUsers(); // findAllUsersThread = new FindAllUsersThread(userServiceTracker); // findAllUsersThread.start(); } public void stop(BundleContext context) throws Exception { System.out.println("Stop Bundle [" + context.getBundle().getSymbolicName() + "]"); // Stop Thread which call UserService#findAllUsers(); // findAllUsersThread.interrupt(); // findAllUsersThread = null; // // // Close the MovieFinder ServiceTracker // userServiceTracker.close(); } }
module-osgi-context.xml
Créez le fichier XML Spring de configuration module-osgi-context.xml dans le répertoire spring du bundle org.dynaresume.simpleosgiclient qui déclare la consommation du service avec le contenu XML suivant :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:osgi="http://www.springframework.org/schema/osgi" xsi:schemaLocation="http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi-1.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <osgi:reference id="userService" interface="org.dynaresume.services.UserService" timeout="1000" /> </beans>
module-context.xml
Créez le fichier XML Spring de configuration module-context.xml dans le répertoire spring du bundle org.dynaresume.simpleosgiclient qui déclare l’instanciation/le lancement/arrêt de la Thread FindAllUsersThread avec le contenu XML suivant :
<?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"> <bean id="FindAllUsersThread" class="org.dynaresume.simpleosgiclient.internal.FindAllUsersThread" init-method="start" destroy-method="interrupt"></bean> </beans>
Cette déclaration permet d’instancier (singleton) la Thread FindAllUsersThread puis :
- lancer la Thread via FindAllUsersThread#start() lorsque le fichier XML Spring est chargé (effectué lors du lancement du bundle client). Cette information est déclaré via l’attribut init-method= »start ».
- arrêter la Thread via FindAllUsersThread#interrupt() lorsque le fichier XML Spring est déchargé (effectué lors de l’arrêt du bundle client). Cette information est déclaré via l’attribut destroy-method= »interrupt ».
REMARQUE : Lorsque le bundle Spring Extender charge ce fichier XML Spring il instancie automatiquement le Thread car le bean FindAllUsersThread est déclaré en tant que singleton. En effet par défaut tous les bean déclarés sont des singleton et Spring les instancie automatiquement. Pour ne pas avoir de singleton, ceci se gère via l’attribut scope du bean qui peut prendre plusieurs valeurs dont prototype qui instancie la classe à chaque fois que l’applicationContext est sollicitée en appeleant ApplicationContext#getBean(String name).
Si nous avions déclaré le bean FindAllUsersThread en scope= »prototype », notre Thread ne serait jamais instancié car aucune code n’appelle notre bean FindAllUsersThread.
FindAllUsersThread
Nous devons modifier la Thread FindAllUsersThread qui consomme le service UserService pour rechercher ce service dans l’instance ApplicationContext fournit par l’interface ApplicationContextAware qu’elle implémente. Pour cela modifiez la classe org.dynaresume.simpleosgiclient.internal.FindAllUsersThread comme suit :
package org.dynaresume.simpleosgiclient.internal; import java.util.Collection; import org.dynaresume.domain.User; import org.dynaresume.services.UserService; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; public class FindAllUsersThread extends Thread implements ApplicationContextAware { private static final long TIMER = 5000; // private ServiceTracker userServiceTracker; // // public FindAllUsersThread(ServiceTracker userServiceTracker) { // this.userServiceTracker = userServiceTracker; // } private ApplicationContext applicationContext; public void setApplicationContext( ApplicationContext applicationContext) throws org.springframework.beans.BeansException { this.applicationContext = applicationContext; }; @Override public void run() { while (!super.isInterrupted()) { try { // 1. Get UserService UserService userService = getUserService(); if (userService != null) { // 2. Display users by using UserServive displayUsers(userService); } } catch (Throwable e) { e.printStackTrace(); } finally { try { if (!super.isInterrupted()) sleep(TIMER); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } } } private UserService getUserService() { System.out.println("--- Get UserService from Spring ApplicationContext---"); UserService userService = (UserService) applicationContext.getBean("userService"); if (userService != null) { return userService; } System.out.println(" Cannot get UserService=> UserService is null!"); return null; } private void displayUsers(UserService userService) { Collection<User> users = userService.findAllUsers(); for (User user : users) { System.out.println("User [login=" + user.getLogin() + ", password=" + user.getPassword() + "]"); } } @Override public synchronized void start() { System.out.println("Start Thread FindAllUsersThread"); super.start(); } @Override public void interrupt() { System.out.println("Interrupt Thread FindAllUsersThread"); super.interrupt(); } }
Test <osgi:reference
Effectuez les mêmes tests que les tests de ServiceTracker – ApplicationContext pour vous rendre compte que la déclaration du bean FindAllUsersThread fonctionne et est capable de récupérer l’instance ApplicationContext. Le test intéressant à effectuer est d’arrêter le bundle client org.dynaresume.simpleosgiclient et de le relancer. A chaque lancement du bundle, le setter du singleton FindAllUsersThread
FindAllUsersThread#setApplicationContext(ApplicationContext applicationContext) throws BeansException
est appelé et met à jour l’instance ApplicationContext de type org.springframework.osgi.context.support.OsgiBundleXmlApplicationContext.
UserService – Injection de dépendances
Vous pouvez télécharger org.dynaresume_step8-reference-DI.zip qui contient les projets du code expliqué ci-dessous.
Les 2 solutions ServiceTracker – ApplicationContext et ApplicationContextAware fonctionnent bien mais nécéssitent des dépendances à Spring (via le MANIFEST.MF). Dans la solution ApplicationContextAware nous avons vu qu’il est possible de déclarer l’instanciation/lancement/arrêt de la Thread FindAllUsersThread. le seul point noir à cette solution est que l’on est fortement lié à l’API Spring. L’idéal serait d’avoir une solution totalement déclarative. Ceci est possible gràce au mécanisme puissant d‘Injection de Dépendances Spring. L’idée générale est de définir dans la classe FindAllUsersThread un setter FindAllUsersThread#setUserService(UserService userService) :
public class FindAllUsersThread extends Thread { ... private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } ... }
Ce setter permet d’indiquer l’instance du service UserService. Il est alimenté par le conteneur Spring. Pour cela il suffit de déclarer le XML suivant :
<bean id="FindAllUsersThread" class="org.dynaresume.simpleosgiclient.internal.FindAllUsersThread" init-method="start" destroy-method="interrupt"> <property name="userService" ref="userService"></property> </bean>
La déclaration
<property name="userService" ref="userService"></property>
indique que le conteneur Spring doit injecter dans l’instance org.dynaresume.simpleosgiclient.internal.FindAllUsersThread en utilisant le setter FindAllUsersThread#setUserService(UserService userService) (indiqué par name= »userService »), un bean identifié (ref= »userService ») par un ID de valeur id= » »userService ». Dans notre ce bean est celui définit avec Spring Dynamic Module :
<osgi:reference id="userService" interface="org.dynaresume.services.UserService" timeout="1000" />
Activator client
Comme dans la solution ApplicationContextAware, Le bundle Activator du client ne doit plus rien effectuer. Pour cela modifiez le code org.dynaresume.simpleosgiclient.internal.Activator comme suit :
package org.dynaresume.simpleosgiclient.internal; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { // private ServiceTracker userServiceTracker = null; // private FindAllUsersThread findAllUsersThread = null; public void start(BundleContext context) throws Exception { System.out.println("Start Bundle [" + context.getBundle().getSymbolicName() + "]"); // // Create and open the MovieFinder ServiceTracker // userServiceTracker = new ServiceTracker(context, UserService.class.getName(), null); // userServiceTracker.open(); // // // Start Thread which call UserService#findAllUsers(); // findAllUsersThread = new FindAllUsersThread(userServiceTracker); // findAllUsersThread.start(); } public void stop(BundleContext context) throws Exception { System.out.println("Stop Bundle [" + context.getBundle().getSymbolicName() + "]"); // Stop Thread which call UserService#findAllUsers(); // findAllUsersThread.interrupt(); // findAllUsersThread = null; // // // Close the MovieFinder ServiceTracker // userServiceTracker.close(); } }
module-osgi-context.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:osgi="http://www.springframework.org/schema/osgi" xsi:schemaLocation="http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi-1.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <osgi:reference id="userService" interface="org.dynaresume.services.UserService" timeout="1000" /> </beans>
module-context.xml
<?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"> <bean id="FindAllUsersThread" class="org.dynaresume.simpleosgiclient.internal.FindAllUsersThread" init-method="start" destroy-method="interrupt"> <property name="userService" ref="userService"></property> </bean> </beans>
FindAllUsersThread
package org.dynaresume.simpleosgiclient.internal; import java.util.Collection; import org.dynaresume.domain.User; import org.dynaresume.services.UserService; public class FindAllUsersThread extends Thread { private static final long TIMER = 5000; // private ServiceTracker userServiceTracker; // // public FindAllUsersThread(ServiceTracker userServiceTracker) { // this.userServiceTracker = userServiceTracker; // } private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } @Override public void run() { while (!super.isInterrupted()) { try { // 1. Get UserService UserService userService = getUserService(); if (userService != null) { // 2. Display users by using UserServive displayUsers(userService); } } catch (Throwable e) { e.printStackTrace(); } finally { try { if (!super.isInterrupted()) sleep(TIMER); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); } } } } private UserService getUserService() { System.out.println("--- Get UserService from OSGi Spring context ---"); // UserService userService = (UserService) userServiceTracker.getService(); if (userService != null) { return userService; } System.out.println(" Cannot get UserService=> UserService is null!"); return null; } private void displayUsers(UserService userService) { Collection<User> users = userService.findAllUsers(); for (User user : users) { System.out.println("User [login=" + user.getLogin() + ", password=" + user.getPassword() + "]"); } } @Override public synchronized void start() { System.out.println("Start Thread FindAllUsersThread"); super.start(); } @Override public void interrupt() { System.out.println("Interrupt Thread FindAllUsersThread"); super.interrupt(); } }
Test <osgi:reference
Effectuez les mêmes tests que les tests de ServiceTracker – ApplicationContext pour vous rendre compte que la déclaration du bean FindAllUsersThread avec injection de dépendance fonctionne et est capable de récupérer l’instance UserService. Le test intéressant à effectuer est d’arrêter le bundle client et de le relancer. A chaque lancement du bundle le setter du singleton FindAllUsersThread
FindAllUsersThread#setUserService(UserService userService)
est appelé et met à jour l’instance UserService.
Le deuxième test intéressant à effectuer est d’arrêter le bundle service org.dynaresume.services.impl qui renvoit aussi une exception :
org.springframework.osgi.service.ServiceUnavailableException: service matching filter=[(objectClass=org.dynaresume.services.UserService)] unavailable
Alors que le setter UserService n’est pas mis à jour? Comment cela peut il fonctionner? Si vous regardez en debug l’instance userService vous pourrez remarquez que nous avons pas la classe UserServiceImpl mais un Proxy org.springframework.aop.framework.JdkDynamicAopProxy qui est un proxy qui surveille les appels des méthodes du service UserServceImpl et lance l’exception ServiceUnavailableException dans le cas ou le service n’est pas disponible.
Conclusion
Nous avons vu 3 méthodes pour utiliser la déclaration de la consommation de service OSGi avec <osgi:reference. La 3éme solution avec dépendance d'injection est totalement déclarative et est la plus simple à mettre en place. Elle ne dépend pas de l'API de Spring. Cependant il y a des cas ou on est obligé d'utiliser par exemple la solution ServiceTracker – ApplicationContext dans les cas ou l’instance ne peut pas être créé via le conteneur Spring. C’est le cas par exemple des EditorParts ou des View de Eclipse RCP qui doivent consommer des services mais qui ne peuvent pas être instanciés par un conteneur comme Spring (mais par un mécanisme propre à Eclipse). C’est cette solution qui est proposé par Martin Lippert et qui en parle ici. Je vous conseille aussi de lire cette article qui utilise la classe org.eclipse.springframework.util.SpringExtensionFactory proposée par Martin Lippert et qui retrouve l’instance ApplicationContext dans le registre de services OSGi via ServiceTracker.
Vous pouvez lire le billet suivant [step9].
Bonjour,
Je suis un utilisateur occasionnel d’Eclipse (IDE, extensions & RCP) et je lis avec grand intérêt votre série de billets sur le couple (Spring , OSGi). Cette solution mène à une grande flexibilité dans la gestion/implémentation des objets et des services (démarrage de service, arrêt, mise à jour à chaud, inversion de contrôle …).
Je me demande souvent comment obtenir la même flexibilité au niveau du modèle métier. Comment mettre à jour le modèle sans gros impact sur le déploiement. Tout cela pour dire : « Pourquoi ne pas envisager d’utiliser dans dynaresume un framework comme dataNucleus JDO (par exemple) en lieu et place d’Hibernate » pour la gestion des Users. La partie exploratoire n’en serait que plus importante surtout si Hbase (Hadoop) est utilisée pour la partie stockage. La base stockerait des objets en version différente et convertis à la volée dans la version courante lors de leur utilisation. La partie cliente serait mise à jour également (par p2) pour éditer la dernière version en date du modèle.
Encore bravo pour vos billets,
Cyril.
Bonjour Cyril,
tout d’abord merci pour tes remarques.
En ce qui concerne le modèle métier, je pense qu’il faut distinguer modification mineures (ajouts) des modifications majeurs (gros refactor). Dans le premier cas, les impacts sont limités et je ne vois pas trop de problème de déploiement. Dans le second, c’est bien plus chaud et je n’ai pas de solution simple tant les impacts peuvent être important.
En fait le problème c’est que dès qu’on a des dépendances de compilation, il faut bien gérer la compatibilité ascendante, même en OSGi.
En ce qui concerne dataNucleus et Hbase, je n’ai pas vraiment d’opinion. Ma culture dans ces technos est quasi nulle. Mais le sujet m’intéresse.
Dans un premier temps, Dynaresume me sert de « Bac à sable » et de POC pour convaincre mon client préféré de paser à ce genre de techno.
Il est clair que le sujet mérite d’être creusé mais je ne pense pas dans l’immédiat.
Cordialement
Bonjour,
En fait, en ce qui concerne les applications web le modèle est dirigé par l’application. Certaines applications (Trac par exemple) ont un mécanisme de plugins et ceux-ci créent leur propre table (lors de l’installation) dans la base trac afin de persister leurs données. Opération assez dangereuse car dépendante de code tiers.
C’est en fait à ce genre de use case que je pense et je dois dire que je reste sans solution quand il s’agit de prévoir l’ajout de nouveaux modèles (provenant de sources inconnues) et leur mise à jour éventuelle sans administrateur de base chevronné.
Les applications web vont devoir devenir aussi souples et modulaires que les applications desktop.
Je te souhaite de bons moments dans ton bac à sable.
Cordialement,
Cyril.
Bonsoir Cyril,
Tout d’abord merci pour vos encouragements, j’espere que les billets vous seront profitables.
Concernant votre remarque sur la « flexibilité au niveau du modèle métier », elle est très pertinente mais je n’ai pas les compétences pour vous donner une réponse. L’idée est très bonne mais elle me paraît complexe. .
Angelo