TP nº2: Injection de dépendances dans OSGi
Étape nº1: Récupération naïve des références de services
Dans cette étape, nous allons développer un troisième bundle OSGi qui accédera et utilisera le service que nous avons développé lors de l'étape précédente. Pour cela, copiez le projetDateBundle et renommez le en
DateBundleUser. N'oubliez pas de mettre
à jour le contenu des fichiers
build.xml, manifest.mf et
de renommer les paquetages du projet. Comme ce bundle
se contentera d'accéder à des services existants, il
vous suffit de créer une nouvelle classe
Activator. Cette classe récupérera la
référence du service DateService et
l'utilisera.
Mise à jour du fichier manifest.mf
Le fichiermanifest.mf doit être légèrement modifié
pour importer le service que vous allez utiliser.
Vous devez utiliser la propriété
Import-Package à la fin du fichier.
Cette propriété est nécessaire pour importer
l'interface du service à utiliser. Le contenu de
votre fichier manifest.mf doit donc
ressembler à:
Manifest-Version: 1.0
Bundle-Name: DateBundleUser
Bundle-SymbolicName: DateBundleUser
Bundle-Version: 1.0.0
Bundle-Description: Demo Bundle
Bundle-Vendor: University of Lille 1
Bundle-Activator: datebundle.user.impl.Activator
Bundle-Category: example
Import-Package: org.osgi.framework, datebundle
Récupération de la référence d'un service – la mauvaise façon
Une chose importante à comprendre avec OSGi est que l'environnement d'exécution est très dynamique et qu'à tout moment, un service requis peut être présent ou non. Il est donc très important de toujours vérifier que vous avez bien récupéré une référence valide de service ou non à chaque fois que vous récupérez le service. Dès que vous n'utilisez plus le service, vous devez libérer la référence de manière à informer l'environnement que vous n'utiliserez plus le service. La plus simple façon – mais aussi la moins propre – d'utiliser un service est de récupérer la référence deDateService depuis l'objet
BundleContext et d'utiliser le service
ainsi récupéré. Nous verrons ensuite pourquoi ce code
est problématique.
package datebundle.user.impl;
import java.util.Date;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import datebundle.DateService;
public class Activator implements BundleActivator {
public static BundleContext bc = null;
public void start(BundleContext bc) throws Exception {
System.out.println(bc.getBundle().getHeaders().get(Constants.BUNDLE_NAME) + " starting...");
Activator.bc = bc;
ServiceReference reference = bc.getServiceReference(DateService.class.getName());
DateService service = (DateService)bc.getService(reference);
System.out.println("Using DateService: formatting date: " + service.getFormattedDate(new Date()));
bc.ungetService(reference);
}
public void stop(BundleContext bc) throws Exception {
System.out.println(bc.getBundle().getHeaders().get(Constants.BUNDLE_NAME) + " stopping...");
Activator.bc = null;
}}
Dans un premier temps, nous récupérons un objet
ServiceReference depuis l'objet
BundleContext. La méthode
getServiceReference() prend simplement
en paramètre le nom du service que nous souhaitons
utiliser. Une fois que nous avons récupéré un objet
ServiceReference, nous pouvons utiliser
la méthode getService() pour récupérer
la référence du service à utiliser.
Vous pouvez maintenant compiler le projet et installer le bundle produit via l'interface Knopflerfish OSGi Desktop. Si votre service
DateService est déjà démarré, tout
devrait bien se passer et vous devriez obtenir les
messages correspondant au nouveau bundle.
Le problème est qu'il n'y a aucune garantie que le service
DateService soit justement
disponible. Essayez la manipulation suivante: arrêtez
les bundles DateBundle et
DateBundleUser puis démarrez uniquement
le bundle DateBundleUser. Vous
devriez obtenir une exception
NullPointerException car la méthode
getServiceReference() a retourné
null (aucun service n'est enregistré
dans l'annuaire, l'environnement ne peut donc pas
vous fournir la référence que vous demandez). Une
solution plus propre consisterait donc à vérifier que
la valeur retournée par la méthode
getServiceReference() n'est pas
null:
ServiceReference reference = bc.getServiceReference(DateService.class.getName());
if (reference != null) {
FirstService service = (DateService)bc.getService(reference);
System.out.println("Using DateService: formatting date: "+service.getFormattedDate(new Date()));
bc.ungetService(reference);
}
else
System.out.println("No Service available!");
Cela résoud le problème de l'exception
NullPointerException, mais si le service
DateService n'est pas disponible au
démarrage de l'environnement d'exécution OSGi, vous
ne pourrez jamais utiliser ce service! Il faudrait
donc vérifier régulièrement si le service est
disponible ou non. Ou encore mieux, laisser
l'environnement d'exécution nous informer quand un
service requis est disponible. Pour ce faire, nous
allons utiliser les ServiceListeners...
Étape nº2: Utiliser ServiceListener pour lier dynamiquement les services
En utilisant l'objetBundleContext, il est possible
d'enregistrer un ServiceListener dans
l'environnement d'exécution. À l'aide d'un objet
filtre optionnel, vous pouvez spécifier exactement
pour quel(s) service(s) vous souhaitez être notifié
des changements via des événements
ServiceEvent. Les événements
ServiceEvent sont déclenchés par
l'environnement d'exécution OSGi à chaque fois qu'un
service est enregistré, supprimé ou que ses
propriétés sont modifiées.
L'exemple suivant montre une modification du code de la méthode
start(). Tout d'abord, un
ServiceListener est enregistré dans
l'environnement d'exécution OSGi. Le chaîne de
caractères utilisée comme filtre suit le style
LDAP et indique à
l'environnement d'exécution de nous envoyer
uniquement les événements
ServiceEvent relatifs au service
DateService. Notez bien qu'à cet
instant nous ne récupérons pas la référence du
service DateService directement
dans la méthode start(). Par
contre, la modification que nous avons réalisé
nous permet de récupérer toutes les références
des services DateService conformes
à notre filtre. Pour initialiser notre
bundle avec l'ensemble des services
déjà disponibles dans l'environnement
d'exécution OSGi, nous utilisons donc la méthode
getServiceReferences() de l'objet
BundleContext et nous déclenchons
manuellement des événements
ServiceEvent.REGISTERED pour que
notre bundle les prenne en compte.
Attention, n'oubliez pas d'ajouter l'interface
ServiceListener à votre activateur
pour que cela fonctionne.
public void start(BundleContext bc) throws Exception {
System.out.println("start " + getClass().getName());
Activator.bc = bc;
String filter = "(objectclass=" + DateService.class.getName() + ")";
bc.addServiceListener(this, filter);
ServiceReference references[] = bc.getServiceReferences(null, filter);
for (int i = 0; references != null && i < references.length; i++)
this.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED,references[i]));
}
La méthode
serviceChanged() va donc
recevoir tous les événements
ServiceEvent pour les changements
relatifs au service DateService. La
méthode mise en œuvre ci-dessous commence à utiliser
un service aussitôt qu'un service
DateService est enregistré dans
l'annuaire et arrête aussitôt que le service est
supprimé. Si les propriétés du service changent, nous
arrêtons d'utiliser ce service et nous récupérons la
référence d'un autre service compatible.
public void serviceChanged(ServiceEvent event) {
switch (event.getType()) {
case ServiceEvent.REGISTERED:
log("ServiceEvent.REGISTERED");
this.service = (DateService) Activator.bc.getService(event.getServiceReference());
this.startUsingService();
break;
case ServiceEvent.MODIFIED:
log("ServiceEvent.MODIFIED received");
this.stopUsingService();
this.service = (DateService) Activator.bc.getService(event.getServiceReference());
this.startUsingService();
break;
case ServiceEvent.UNREGISTERING:
log("ServiceEvent.UNREGISTERING");
this.stopUsingService();
break;
}}
private void stopUsingService() {
this.thread.stopThread();
try {
this.thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.service = null;
}
private void startUsingService() {
this.thread = new ServiceUserThread(this.service);
this.thread.start();
}
private void log(String message) {
System.out.println(Activator.bc.getBundle().getHeaders().get(Constants.BUNDLE_NAME) + ": " + message);
}
Vous pouvez maintenant compiler votre projet et installer (ou mettre à jour) le bundle généré. Vous verrez que votre bundle commencera à afficher vos messages aussitôt que le service
DateService deviendra
disponible. De façon analogue, les messages cesseront
dès que le service DateService sera
supprimé et le bundle attendra l'arrivée d'un autre
service pour démarrer de nouveau.
Comme vous avez pu le constater, le code à écrire consiste principalement à démarrer et arrêter votre service en fonction de la disponibilité des autres services. Ce code n'est pas forcément intéressant à écrire et peut être sujet à de nombreuses erreurs. Heureusement, il existe une classe utilitaire dans OSGi qui vous aide à gérer ce problème. La classe
ServiceTracker permet en effet de
surveiller la disponibilité des services. Nous allons
donc voir comment utiliser cette classe.
Étape nº3: Utiliser ServiceTracker pour surveiller les services
UnServiceTracker est un objet qui
surveille automatiquement tous les évévenements
ServiceEvent relatifs à un service
particulier et qui vous donne la possibilité de
personnaliser ce qui doit se passer lorsque les
services apparaissent ou disparaissent de
l'environnement d'exécution. Pour réaliser cette
personnalisation, vous devez implanter l'interface
ServiceTrackerCustomizer et la passer en
référence à l'objet ServiceTracker.
Le code ci-dessous est une version mise-à-jour de la méthode
start() de la classe
d'activation du bundle DateBundleUser.
Vous pouvez constater qu'une grosse partie du code
précédent a été supprimée et remplacée par
l'utilisation du ServiceTracker.
public void start(BundleContext bc) throws Exception {
System.out.println(bc.getBundle().getHeaders().get(Constants.BUNDLE_NAME)+ " starting...");
Activator.bc = bc;
customizer = new MyServiceTrackerCustomizer(bc);
tracker = new ServiceTracker(bc, DateService.class.getName(),customizer);
tracker.open();
}
Maintenant, jetons un œil à la classe
MyServiceTrackerCustomizer qui implante
l'interface ServiceTrackerCustomizer:
public class MyServiceTrackerCustomizer implements ServiceTrackerCustomizer {
private ServiceUserThread thread = null;
private BundleContext bc;
public MyServiceTrackerCustomizer(BundleContext bc) {
this.bc = bc;
}
public Object addingService(ServiceReference reference) {
DateService service = (DateService) bc.getService(reference);
if (this.thread == null) {
this.thread = new ServiceUserThread(service);
this.thread.start();
}
return service;
}
public void modifiedService(ServiceReference reference, Object serviceObject) {
this.thread.stopThread();
try {
this.thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
DateService service = (DateService) bc.getService(reference);
this.thread = new ServiceUserThread(service);
this.thread.start();
}
public void removedService(ServiceReference reference, Object serviceObject) {
this.thread.stopThread();
try {
this.thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.thread = null;
}}
La méthode
addingService() récupère la
référence du service et démarre un nouveau
thread si aucun n'existe. Dans le cas où
plusieurs service DateService seraient
enregistrés, un seul de ces services sera utilisé.
La méthode
modifiedService() stoppe
l'exécution du thread et redémarre avec le
nouveau service. Une amélioration possible serait de
vérifier que la référence du service qui a été
modifié est bien celle que nous utilisons et de ne
redémarrer le thread que dans ce cas là.
Finallement, la méthode
removedService()
stoppe l'exécution du thread. Le service
n'est donc plus utilisé dans ce cas.
Exercice: Proposez une autre implantation de l'interface ServiceTrackerCustomizer qui continue de fonctionner tant que des services DateService sont disponibles dans l'environnement d'exécution OSGi.