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 projet DateBundle 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 fichier manifest.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 de DateService 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'objet BundleContext, 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

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