Programmation par composant avec la technologie OSGi (Partie 1)

Thierry Templier (co-auteur du livre Spring par la pratique)

Cette série d'articles décrit la mise en œuvre de la programmation orientée composant et d'architectures orientées service en se fondant sur la technologie OSGi. Nous y détaillerons les différents concepts de cette technologie afin de permettre sa prise en main.

Un grand merci à Jérôme Benois, Hikage, Baptiste Wicht, djo.mos, keulkeul, azerr et woodwai pour leurs relectures techniques de l'article ainsi qu'à elitost pour ses corrections quant à l'orthographe. ♪

- Titre : Programmation par composant avec la technologie OSGi (Partie 1)

- Auteur : Thierry Templier (co-auteur du livre Spring par la pratique)

- Parution : 10 janvier 2008

- Mise à jour : 1 février 2008

- Licence : Ce document est issu de https://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

- I. Introduction

- II. Programmation orientée composant et architecture orientée service avec OSGi

- III. Architecture OSGi

- III-A. Gestion des composants OSGi

- III-B. Cycle de vie des bundles

- III-C. Services

- III-D. Conclusion

- IV. Bundle OSGi

- IV-A. Caractéristiques d'un bundle

- IV-B. en-têtes OSGi du fichier MANIFEST.MF

- V. Interactions entre bundles

- V-A. Mise à disposition de packages

- V-B. Importation de packages

- VI. Principales API OSGi

- VI-A. BundleActivator - Entité d'activation

- VI-B. BundleContext - Contexte de composant

- VII. Mise en œuvre de services

- VIII. Conclusion

- IX. Bibliographie

I - Introduction

Ce premier article a pour objectif de fournir une introduction à la technologie OSGi (Open Service Gateway Initiative), technologie qui après avoir été longtemps utilisée dans le monde de l'embarqué est de plus en plus mise en œuvre dans les applications classiques et serveurs.

Nous verrons dans un premier temps les différents concepts relatifs à OSGi et détaillerons à quelles problématiques cette technologie s'adresse tout en soulignant ses apports en terme d'architecture et de structuration des applications.

Nous rentrerons par la suite dans les aspects techniques d'OSGi afin de détailler ses caractéristiques et de voir concrètement comment mettre en œuvre cette technologie au sein d'applications Java. Nous laisserons le soin à un prochain article de décrire la manière d'utiliser OSGi dans des applications Java EE tout en se fondant sur des bibliothèques et frameworks Java EE.

II - Programmation orientée composant et architecture orientée service avec OSGi

Avant d'entrer dans le cœur de la technologie, nous allons détailler les caractéristiques de la programmation orientée composant et des architectures orientée service afin d'identifier les problématiques qu'elles visent à résoudre. Nous verrons alors que la technologie OSGi adresse ces deux types de technologies.

Tout d'abord, la programmation orientée composant [1] vise à adresser les limitations de la programmation orientée objet [2]. Cette dernière offre d'intéressants mécanismes afin de modulariser les traitements et les rendre réutilisables, mais n'apporte aucun support afin de mettre en relation les classes. De plus, plus les traitements de l'application augmentent et se complexifient, plus ces limitations sont visibles et pénalisantes au niveau de la maintenabilité et de l'évolutivité de l'application.

Ainsi, cet aspect peut devenir très problématique lors de la mise en œuvre de réseaux complexes d'objets. En effet, cela se traduit très souvent par des couplages forts entre objets, ce point nuisant énormément à la réutilisabilité des traitements. Chaque développeur a alors le choix des outils afin d'adresser au mieux au sein de ses applications ces aspects. L'approche la plus adaptée consiste en la mise en œuvre du patron de conception injection de dépendance par l'intermédiaire de frameworks tels que Spring [3] utilisés conjointement avec la programmation par interface.

Nous retrouvons ce problème notamment au niveau des instanciations des objets, directement ou par l'intermédiaire de l'introspection. En effet, même avec la programmation par interface, la classe appelante est liée à la classe appelée. Les patrons de conception tels que les fabriques [4] et l'injection de dépendance [5] permettent d'adresser cette problématique en déléguant les traitements d'instanciation à une entité autonome (respectivement la fabrique et le conteneur).

En parallèle de la programmation orientée composant, les architectures orientées service peuvent être mises en œuvre afin de mettre à disposition des traitements tout en diminuant les couplages entre les briques techniques. La SOA (Service Oriented Architecture) [6] n'implique pas nécessairement l'utilisation des services Web avec les technologies SOAP [7] et WSDL [8] et peut être mise en œuvre au sein d'un même processus Java notamment avec des conteneurs légers (entités mettant en œuvre l'injection de dépendances) tels que Spring[3] ou Hivemind [9]. Dans ce contexte, nous parlons de fournisseur de services et de consommateurs de services, les fournisseurs mettant à disposition des composants. La programmation orientée composant [1] et les architectures orientées service peuvent donc être mises en œuvre de manière complémentaire afin de bénéficier des avantages des deux types de technologies.

La figure suivante illustre différents composants exposant des services par l'intermédiaire d'interfaces et reliés entre eux par des services :

Il est à noter que des composants OSGi peuvent être également reliés entre eux par l'intermédiaire des packages, packages qu'ils exportent et importent. Dans ce cas, les services ne sont donc pas l'unique manière de les relier.

Dans ce contexte, la technologie OSGi vise à adresser les différentes problématiques vues précédemment, problématiques récapitulées ci-dessous :

- adresser les limitations de la programmation orientée objet ;

- permettre la gestion des applications complexes et de taille importante ;

- améliorer la qualité de service des applications en permettant une administration à chaud ;

- permettre la mise en œuvre d'architectures orientées service légères.

Une des autres caractéristiques de la technologie OSGi est sa portabilité puisqu'elle peut être mise en œuvre aussi bien dans des terminaux de manière embarquée que dans des applications classiques ou serveurs. Le premier aspect a été à la base d'OSGi, les seconds types d'application s'étant développés ces dernières années par l'intermédiaire d'outils tels qu'Eclipse [10]. Le fait qu'OSGi repose sur la technologie Java pour son exécution a été également un important facteur de portabilité.

III - Architecture OSGi

Dans cette section, nous allons décrire les caractéristiques des conteneurs et composants OSGi. Le composant correspond à l'entité centrale de la technologie et désignée par le terme bundle avec la terminologie de cette dernière. Nous verrons également les spécificités du cycle de vie des bundles, la manière de les gérer au niveau du conteneur ainsi que la façon de les configurer par l'intermédiaire de leur descripteur de déploiement.

La figure suivante décrit les différentes briques de l'architecture de la technologie OSGi, briques que nous allons détailler conceptuellement tout au long de cette section puis techniquement dans le reste de l'article :

La couche Module adresse la gestion des bundles, aussi bien au niveau du chargement des classes (classloading) que de la gestion de leurs visibilités et de leurs versions. La couche Cycle de vie (life cycle) prend quant à elle en charge les états des bundles supportés par le conteneur ainsi que les API correspondantes. Pour finir, la couche Services offre la possibilité de mettre à disposition des services au sein d'une même machine virtuelle tout en masquant leurs implémentations à leurs utilisateurs. Les applications OSGi peuvent se fonder et tirer parti de ces différentes couches afin de mettre en œuvre des composants et des services.

III-A - Gestion des composants OSGi

La couche module permet la mise en œuvre des bundles dans le conteneur. Elle a la responsabilité de la gestion des différents chargeurs de classes et des versions des dépendances.

En effet, une des caractéristiques de la technologie consiste en l'isolation des classloaders des composants. En effet, chaque composant ou bundle OSGi dispose d'un classloader indépendant pour ses traitements. Cet aspect permet par exemple la mise en œuvre dans deux bundles différents d'une même bibliothèque avec deux versions différentes et incompatibles.

Cet aspect offre la possibilité à OSGi de complètement maîtriser les classes et packages du composant visibles depuis l'extérieur et visibles par les autres composants. Par défaut, la stratégie est la plus restrictive possible, à savoir que rien n'est visible. Par contre, lors de la mise à disposition d'un package d'un composant, un numéro de version spécifique peut être spécifié. Ainsi, deux versions de packages peuvent coexister dans le conteneur OSGi sans aucun problème.

III-B - Cycle de vie des bundles

Comme dans tout conteneur, les éléments contenus dans ce dernier possèdent un cycle de vie bien particulier puisque le conteneur OSGi a la responsabilité de les gérer. Nous pouvons distinguer deux grandes parties dans les différents états supportés :

- Non opérationnel. Les états correspondent à la présence du bundle dans le conteneur, mais ce dernier n'est pas utilisable par les autres bundles pour leurs traitements;

- Opérationnel. Les états de ce type correspondent aux moments où le bundle est utilisable ou en phase de l'être ou de ne plus l'être.

La figure suivante illustre les différents états de gestion des bundles par le conteneur ainsi que leurs enchaînements possibles:

Nous verrons par la suite que le conteneur OSGi offre une API standardisée afin de gérer le cycle de vie des composants. Veuillez vous reporter à la section 5.2 BundleContext pour plus de précisions. De plus, ces différents états sont mis en œuvre par le bundle manager du conteneur qui a la responsabilité de gérer la plateforme.

Le tableau suivant récapitule les différents états supportés par le bundle manager des conteneurs OSGi ainsi que leurs principales caractéristiques :

État

Descriptif

Installé (installed)

État dans lequel se trouve un bundle juste après avoir été installé, la résolution des dépendances n'ayant pas encore été réalisée.

Résolu (resolved)

État dans lequel se trouve un bundle après avoir été installé, la résolution des dépendances ayant juste été réalisée.

En train de démarrer (starting)

État dans lequel se trouve un bundle lorsqu'il est en train d'être démarré. Cet état correspond à un état transitoire entre les événements Résolu et Actif.

Actif (active)

État dans lequel se trouve un bundle lorsqu'il a été démarré avec succès. Le bundle ainsi que les services qu'il expose sont disponibles pour les autres bundles.

En train de s'arrêter (stopping)

État dans lequel se trouve un bundle lorsqu'il est en train d'être arrêté. Cet état correspond à un état transitoire entre les événements Actif et Résolu.

Désinstallé (uninstalled)

État dans lequel se trouve un bundle une fois qu'il a été désinstallé.

Nous pouvons noter qu'une entité peut être associée au cycle de vie des bundles et correspond à l'entité d'activation, entité appelée lors du démarrage et de l'arrêt d'un bundle. Cette entité peut être mise en œuvre par l'intermédiaire de l'interface BundleActivator et configurée avec l'en-tête Bundle-Activtator. Nous reviendrons plus en détail sur ce mécanisme dans la section 5.1 BundleActivator.

Nous pouvons noter que la plupart des conteneurs OSGi fournissent des outils afin de gérer le cycle de vie des bundles et de visualiser leurs états. Dans cette optique, le conteneur OSGi
Felix [11] offre une console d'administration en ligne de commande possédant les commandes suivantes :

Commande

Descriptif

install

Installation de bundle(s).

ps

Affichage de la liste des bundles installés.

refresh

Rafraichissement des packages des bundles.

resolve

Tentative de résolution des bundles spécifiés.

services

Affichage de la liste des services enregistrés ou utilisés.

start

Démarrage de bundle(s).

stop

Arrêt de bundle(s).

uninstall

Désinstallation de bundle(s).

update

Mise à jour d'un bundle.

Avec la console de cet outil, le conteneur peut être complètement administré à chaud en ligne de commandes. Le scénario suivant peut être mis en œuvre afin d'installer un bundle, de le démarrer, puis l'arrêter avec les commandes respectives install, start et stop. Le code suivant illustre concrètement l'enchaînement de ces différentes commandes :

-> ps
START LEVEL 1
ID   State         Level  Name
[   0] [Active     ] [    0] System Bundle (1.0.0)
[   1] [Active     ] [    1] Apache Felix Shell Service (1.0.0)
[   2] [Active     ] [    1] Apache Felix Shell TUI (1.0.0)
-> install /home/templth/developpez/OSGi/simplebundle.jar
Bundle ID: 3
-> ps
START LEVEL 1
ID   State         Level  Name
[   0] [Active     ] [    0] System Bundle (1.0.0)
[   1] [Active     ] [    1] Apache Felix Shell Service (1.0.0)
[   2] [Active     ] [    1] Apache Felix Shell TUI (1.0.0)
[   3] [Installed  ] [    1] Simple Bundle (1.0.1)
-> start 3
-> ps
START LEVEL 1
ID   State         Level  Name
[   0] [Active     ] [    0] System Bundle (1.0.0)
[   1] [Active     ] [    1] Apache Felix Shell Service (1.0.0)
[   2] [Active     ] [    1] Apache Felix Shell TUI (1.0.0)
[   3] [Active     ] [    1] Simple Bundle (1.0.1)
-> stop 3
-> ps
START LEVEL 1
ID   State         Level  Name
[   0] [Active     ] [    0] System Bundle (1.0.0)
[   1] [Active     ] [    1] Apache Felix Shell Service (1.0.0)
[   2] [Active     ] [    1] Apache Felix Shell TUI (1.0.0)
[   3] [Resolved   ] [    1] Simple Bundle (1.0.1)

III-C - Services

La couche Services offre la possibilité de mettre à disposition certains traitements des composants par l'intermédiaire de services. Nous désignons par le terme service des traitements structurés de la manière suivante. Il s'agit d'une entité dont le contrat est clairement défini par l'intermédiaire d'une interface Java. Seule cette interface est connue par les consommateurs du service. La ou les implémentations du service doivent rester internes aux composants et implémenter la précédente interface.

Le mécanisme de gestion des services est complètement dynamique. En effet, dès qu'un service est enregistré, il est automatiquement utilisable par tous les composants disponibles dans le conteneur OSGi. Comme nous le verrons par la suite, une API est disponible afin d'enregistrer des implémentations de services. De plus, par convention, il convient de faire correspondre le nom du service au nom de l'interface implémentée.

Dans le cadre de la technologie OSGi, l'architecture orientée service a la caractéristique d'être très « légère » et de pouvoir fonctionner de manière autonome dans un seul processus Java. Elle correspond à un modèle de services dans une unique machine virtuelle (in-VM).

Nous verrons dans la section 5.1 comment mettre en œuvre concrètement des services dans le cadre de la technologie OSGi.

III-D - Conclusion

L'architecture de la technologie OSGi permet de mettre en œuvre des composants et de mettre à disposition des services applicatifs. Elle offre également un cycle de déploiement de services très rapides par l'intermédiaire du cycle de vie des bundles tout en offrant une meilleure structuration des fondations des applicatifs.

Nous pouvons remarquer que la sécurité est prévue en standard puisqu'elle fait partie intégrante de la spécification OSGi. Elle offre la possibilité de spécifier des permissions d'accès aux entités gérées par le conteneur. Une de ses principales caractéristiques consiste dans le fait que sa configuration est complètement dynamique et ne nécessite aucun redémarrage après une modification.

Rentrons maintenant dans le détail technique de la technologie OSGi et voyons concrètement comment la mettre en œuvre.

IV - Bundle OSGi

Dans cette section, nous allons détailler les différentes caractéristiques de l'entité centrale d'OSGi, le composant ou bundle. Nous verrons comment le mettre en œuvre par l'intermédiaire d'un simple fichier JAR de Java.

IV-A - Caractéristiques d'un bundle

Avec la technologie OSGi, le concept de composant est mis en œuvre par l'intermédiaire des bundles, un bundle correspondant à un composant. Ces derniers permettent de mettre en œuvre les différents concepts des composants et ce sont eux qui sont déployés dans les conteneurs OSGi.

Avec OSGi, un composant (bundle) est simplement stocké dans un fichier JAR de Java. Les informations de déploiement sont spécifiées par l'intermédiaire du fichier standard MANIFEST.MF présent dans le répertoire META-INF. En effet, OSGi reprend ce fichier en y ajoutant différents en-têtes afin de configurer le composant.

Il n'est pas nécessaire d'ajouter d'autres éléments au fichier JAR si ce n'est les classes bien sûr et les éventuelles bibliothèques utilisées, uniquement les bibliothèques n'étant pas définies en tant que dépendances. Nous pouvons constater que cet aspect favorise de manière importante la simplicité de mise en œuvre de composants dans des conteneurs de ce type. Les conteneurs OSGi sont de ce fait très légers. N'oublions pas qu'OSGi était à l'origine utilisé dans des systèmes embarqués donc très soucieux de la consommation mémoire.

Les conteneurs possèdent diverses caractéristiques quant à la gestion des composants. Tout d'abord, ils ont la particularité de permettre la gestion des dépendances entre composants. En effet, comme nous l'avons vu précédemment, un composant s'appuie la plupart du temps sur d'autres afin de réaliser ses traitements et de fonctionner correctement. Pour ce faire, OSGi offre la possibilité de rendre visibles dans le conteneur les packages de composants de manière très fine. La gestion des versions des dépendances est également supportée.

IV-B - en-têtes OSGi du fichier MANIFEST.MF

Comme nous l'avons précisé dans la précédente section, le fichier MANIFEST.MF correspond au descripteur de déploiement du bundle. Ce fichier est lu par le conteneur OSGi afin de configurer le bundle.

La spécification OSGi définit un ensemble d'en-têtes standards pour un composant, en-têtes utilisables dans le fichier MANIFEST.MF et dont la liste des principales est récapitulée dans le tableau suivant :

En-tête

Descriptif

Bundle-ManifestVersion

Correspond à la version de fichier MANIFEST du bundle

Bundle-SymbolicName

Spécifie l'identifiant symbolique du bundle

Bundle-Name

Spécifie le nom du bundle

Bundle-Version

Spécifie la version du bundle

Bundle-DocURL

Permet de préciser l'adresse de la documentation du bundle.

Bundle-Category

Spécifie la catégorie du bundle.

Import-Package

Spécifie les noms et les versions des packages utilisés par le bundle.

Export-Package

Spécifie les noms et les versions des packages mis à disposition par le bundle.

DynamicImport-Package

Spécifie les noms et les versions des packages utilisés par le bundle. Cet en-tête se différencie de Import-Package par le fait qu'il ne soit pas nécessaire que les dépendances soient présentes au démarrage du bundle. Il suffit qu'elles le soient au moment de l'exécution.

Bundle-NativeCode

Spécifie la liste des bibliothèques natives présentes dans le bundle.

Require-Bundle

Spécifie les identifiants symboliques des bundles nécessaires au bon fonctionnement du bundle.

Bundle-Activator

Spécifie le nom de la classe dont les traitements sont exécutés lors du démarrage et de l'arrêt du bundle. Cette classe doit être présente dans le bundle et implémenter l'interface BundleActivator.

Bundle-Classpath

Spécifie le classpath du bundle. Par défaut, la valeur implicite correspond à . et il faut veiller à ne pas l'oublier lorsque des bibliothèques sont spécifiées explicitement.

Le code suivant illustre la configuration d'un composant OSGi simple dans le fichier MANIFEST.MF. Ce bundle est identifié par simple.bundle, possède le nom « Simple Bundle », exporte le package org.developpez.osgi.service et utilise l'entité d'activation org.developpez.osgi.SimpleActivator.

Manifest-Version = 1
Bundle-Activator = org.developpez.osgi.SimpleActivator
Bundle-SymbolicName = simple-bundle
Bundle-Name = Simple Bundle
Bundle-Description = Simple Bundle.
Import-Package = org.osgi.framework;version=1.3
Export-Package = org.developpez.osgi.services
Bundle-Version = 1.0.1
Bundle-License = http://www.apache.org/licenses/LICENSE-2.0.txt

V - Interactions entre bundles

Comme nous l'évoquions dans la section précédente, un conteneur OSGi offre la possibilité de gérer la visibilité des ressources en composant. Par défaut, un composant est complètement opaque depuis l'extérieur et rien n'est accessible. Cet aspect implique de préciser toutes les dépendances utilisées par un composant et tous les packages mis à disposition.

La spécification OSGi offre la possibilité de jouer sur cet aspect par l'intermédiaire d'en-têtes dans le fichier MANIFEST.MF, fichier contenant les données relatives au déploiement et décrit dans la précédente section. Détaillons maintenant les spécificités de ces en-têtes et la manière de les mettre en œuvre.

Notons qu'il existe une manière complémentaire de faire interagir les bundles entre eux par l'intermédiaire des services. Nous décrirons cet aspect dans une prochaine section.

V-A - Mise à disposition de packages

Afin de mettre à des dispositions des packages (ou exporter) d'un bundle, l'en-tête Export-Package doit être mis en œuvre dans le fichier MANIFEST.MF du composant. Cet en-tête contiendra la liste des packages pour lesquels l'accès est possible.

Il est à noter que la spécification d'un package dans ce cadre autorise l'accès à toutes les classes du package tout en gardant inaccessibles les classes contenues dans les sous-packages du package.

Lors de l'exportation d'un package, OSGi offre la possibilité d'ajouter des informations complémentaires telles que la version et dont la liste est récapitulée dans le tableau ci-dessous :

Paramètre

Descriptif

uses

Spécifie la liste des packages utilisés par le package exporté.

mandatory

Spécifie une liste de paramètres obligatoires à spécifier lors de l'importation du package exporté.

include

Spécifie une liste de packages devant être visibles par le bundle important le package.

exclude

Spécifie une liste de packages devant être invisibles par le bundle important le package.

version

Spécifie la version avec laquelle est mis à disposition le package.

La syntaxe complète d'une valeur pour la propriété Export-Package est donc la suivante :

Export-Package: nom-package;parametre:=valeur,nom-package;parametre:=valeur,...

Notons que, si la valeur d'un paramètre est une liste, les éléments doivent être séparés par des virgules et la valeur globale doit être entourée par des guillemets.

Le code suivant correspond à un extrait de la valeur de l'en-tête Export-Package du fichier MANIFEST.MF d'un bundle OSGi pour Hibernate 3 [12]. Nous remarquons la présence des paramètres uses et version pour le packageorg.hibernate.stat.

Export-Package: org.hibernate.stat;uses:="org.hibernate.util,org.hibernate.cache,
org.apache.commons.logging,org.hibernate.engine";version=3.1.3,
org.hibernate.cache;uses:="org.hibernate.util,net.sf.swarmcache,
org.hibernate.cfg,org.hibernate,net.sf.ehcache,org.hibernate.impl,...

V-B - Importation de packages

La technologie OSGi offre la possibilité de spécifier les packages utilisés afin qu'ils soient visibles dans le bundle. Une fois spécifiées, les classes sont utilisables dans les classes implémentées par le bundle. Cet aspect se configure dans le fichier MANIFEST.MF par l'intermédiaire des en-têtes Import-Package et DynamicImport-Package. Détaillons tout d'abord l'usage du premier en-tête.

Lors de l'importation d'un package, OSGi offre la possibilité d'ajouter des informations complémentaires telles que la version et le mode de résolution dont la liste simplifiée est récapitulée dans le tableau ci-dessous :

Paramètre

Descriptif

resolution

Spécifie le type de résolution du package. La valeur par défaut est mandatory.

version

Spécifie la version du package à utiliser. Une plage de versions peut être spécifiée.

La syntaxe complète d'une valeur pour la propriété Import-Package est donc la suivante :

Import-Package: nom-package;parametre:=valeur,nom-package;parametre:=valeur,...

Notons que, si la valeur d'un paramètre est une liste, les éléments doivent être séparés par des virgules et la valeur globale doit être entourée par des guillemets.

Le code suivant correspond à un extrait de la valeur de l'en-tête Import-Package du fichier MANIFEST.MF du bundle OSGi de Spring AOP 2.0.5. Nous remarquons la présence des paramètres resolution pour le package com.jamonapi et version pour le package org.springframework.aop.

Import-Package: com.jamonapi;resolution:=optional,net.sf.cglib.core;resolution:=optional,
net.sf.cglib.proxy;resolution:=optional,net.sf.cglib.transform.impl;resolution:=optional,
...
org.springframework.aop;version=2.0.5,org.springframework.aop.aspectj;version=2.0.5,...

Dans le cas de l'importation de packages, l'en-tête DynamicImport-Package est également disponible. Il se différencie du précédent par le fait que la résolution des dépendances spécifiées à ce niveau n'est réalisée qu'au moment du chargement des classes et non lors du passage du bundle de l'état Installé à Résolu.

L'en-tête fonctionne sinon de la même manière que l'en-tête Import-Package, si ce n'est qu'il ne supporte logiquement pas le paramètre resolution.

VI - Principales API OSGi

Par cette section, nous allons introduire deux interfaces des API de la technologie OSGi relatives respectivement à l'entité d'activation et au contexte des bundles. Elles permettent notamment d'initialiser et de finaliser les ressources des bundles et d'interagir avec le conteneur OSGi.

L'entité relative au contexte OSGi nous sera utile dans la section relative aux services puisque cette interface met à disposition des méthodes afin de manipuler et d'avoir accès aux services présents dans le conteneur OSGi.

VI-A - BundleActivator - Entité d'activation

La technologie OSGi offre la possibilité de spécifier une entité appelée lors de l'activation (état Démarrage) et de la désactivation (état En cours d'arrêt) d'un composant, c'est-à-dire lorsque ce dernier est démarré ou arrêté. La classe implémentant cette entité doit nécessairement posséder l'interface BundleActivator et être configurée par l'intermédiaire de l'en-tête Bundle-Activator dans le fichier MANIFEST.MF.

L'interface BundleActivator définit la structure d'une classe d'activation en définissant les signatures des méthodes appelées lors des différents événements précédemment cités :

- Méthode start, appelée lors le démarrage du composant (état Démarrage);

- Méthode stop, appelée quant à elle lors de l'arrêt du composant (état En cours d'arrêt).

Ces deux méthodes prennent en paramètre le contexte OSGi du composant par l'intermédiaire de l'interface BundleContext, interface permettant d'interagir aussi bien avec le conteneur qu'avec le composant. Le contenu de l'interface BundleActivator est décrit ci-dessous :

public interface BundleActivator {
void start(BundleContext context);
void stop(BundleContext context);
}

Le code suivant illustre la mise en œuvre d'une entité d'activation pour un bundle par l'intermédiaire d'une implémentation simple appelée ici SimpleActivator :

public class SimpleActivator implements BundleActivator {
private ServiceRegistration serviceRegistration;

public void start(BundleContext context) {
// Enregistrement d'un service
SimpleService service  = new SimpleServiceImpl();
this.serviceRegistration = context.registerService(
SimpleService.class.getName(), service, null);
}

public void stop(BundleContext context) {
// Désenregistrement d'un service
if( this.serviceRegistration!=null ) {
this.serviceRegistration.unregister();
}
}
}

Le code suivant décrit la configuration de cette entité par l'intermédiaire de l'en-tête Bundle-Activator dans le fichier MANIFEST.MF du bundle :

Manifest-Version = 1
Bundle-Activator = org.developpez.OSGi.SimpleActivator
Bundle-SymbolicName = simple-bundle
Bundle-Name = Simple Bundle
Bundle-Description = Simple Bundle.
Import-package = org.OSGi.framework;version=1.3
Bundle-Version = 1.0.1
Bundle-License = http://www.apache.org/licenses/LICENSE-2.0.txt

VI-B - BundleContext - Contexte de composant

Nous avons vu dans la précédente section que l'entité d'activation s'appuie sur le contexte du composant. Détaillons maintenant les spécificités de l'interface BundleContext, interface relative au contexte du composant. Cette dernière permet d'interagir aussi bien au niveau du conteneur OSGi lui-même que du bundle dans lequel il est utilisé. Elle offre la possibilité de réaliser les différentes opérations suivantes :

Opération

Descriptif

Manipulation des bundles

Permet de récupérer les instances de bundles du conteneur et d'installer de nouvelles instances.

Manipulation des services

Permet de récupérer les instances de services du conteneur et d'installer de nouveau. Nous détaillerons cet aspect dans la section suivante.

Observateurs

Permet de spécifier et de supprimer des observateurs à différents niveaux.

Le contenu de l'interface BundleContext est décrit dans le code ci-dessous :

public interface BundleContext {
void addBundleListener(BundleListener listener);
void addFrameworkListener(FrameworkListener listener);
void addServiceListener(ServiceListener listener);
void addServiceListener(ServiceListener listener,
Java.lang.String filter);
Filter createFilter(Java.lang.String filter);
ServiceReference[] getAllServiceReferences(Java.lang.String clazz,
Java.lang.String filter);
Bundle getBundle();
Bundle getBundle(long id);
Bundle[] getBundles();
Java.io.File getDataFile(Java.lang.String filename);
Java.lang.String getProperty(Java.lang.String key);
Java.lang.Object getService(ServiceReference reference);
ServiceReference getServiceReference(Java.lang.String clazz);
ServiceReference[] getServiceReferences(Java.lang.String clazz,
Java.lang.String filter);
Bundle installBundle(Java.lang.String location);
Bundle installBundle(Java.lang.String location,
Java.io.InputStream input);
ServiceRegistration registerService(Java.lang.String[] clazzes,
Java.lang.Object service,
Java.util.Dictionary properties);
ServiceRegistration registerService(Java.lang.String clazz,
Java.lang.Object service,
Java.util.Dictionary properties);
void removeBundleListener(BundleListener listener);
void removeFrameworkListener(FrameworkListener listener);
void removeServiceListener(ServiceListener listener);
boolean ungetService(ServiceReference reference);
}

Détaillons maintenant les spécificités relatives aux bundles et aux observateurs. Cette interface offre tout d'abord la possibilité de gérer et récupérer les instances des bundles présents dans le conteneur. Tout d'abord, l'installation de bundles se réalise par l'intermédiaire des méthodes installBundle, méthode prenant en paramètre le chemin du fichier JAR du bundle. Les méthodes getBundle et getBundles retournent quant à elle une ou plusieurs instances de bundles. Sont supportées les récupérations du bundle courant, d'un bundle en se fondant sur son identifiant dans le conteneur ou de tous les bundles du conteneur.

Le code suivant illustre la mise en œuvre de ces méthodes dans une entité ayant accès au contexte OSGi :

// Installation d'un nouveau bundle
Bundle bundleInstalle = contexte.installBundle(
"file:///home/templth/developpez/OSGi/simplebundle.jar");
long idBundle = bundleInstalle.getBundleId();

// Récupération d'une instance d'un bundle présent dans le conteneur
Bundle bundle = contexte.getBundle(idBundle);
int etatBundle = bundle.getState();

Comme vous pouvez le constater, le code précédent repose sur l'interface Bundle correspondant à l'entité relative à un composant OSGi. Cette dernière permet de récupérer des informations sur le bundle mais également de gérer son état. Le code suivant décrit le contenu de l'interface Bundle :

public interface Bundle {
public static final int UNINSTALLED = 0x00000001;
public static final int INSTALLED = 0x00000002;
public static final int RESOLVED = 0x00000004;
public static final int STARTING = 0x00000008;
public static final int STOPPING = 0x00000010;
public static final int ACTIVE = 0x00000020;

Enumeration findEntries(String path, String filePattern, boolean recurse)
BundleContext getBundleContext()
long getBundleId()
URL getEntry(String path)
Enumeration getEntryPaths(String path)
Dictionary getHeaders()
Dictionary getHeaders(String locale)
long getLastModified()
String getLocation()
ServiceReference[] getRegisteredServices()
URL getResource(String name)
Enumeration getResources(String name)
ServiceReference[] getServicesInUse()
int getState()
String getSymbolicName()
boolean hasPermission(Object permission)
Class loadClass(String name)
void start()
void start(int options)
void stop()
void stop(int options)
void uninstall()
void update()
void update(InputStream in)
}

Il est ainsi possible par la programmation, après l'installation d'un bundle, de réaliser le démarrage du bundle et de récupérer les différents services mis à disposition par le bundle puis de l'arrêter, comme l'illustre le code suivant :

// Installation d'un nouveau bundle
Bundle bundleInstalle = contexte.installBundle(
"file:///home/templth/developpez/OSGi/simplebundle.jar");

// Démarrage du bundle (état installed vers active)
bundle.start();

// Récupération des services mis à disposition
ServiceReference[] serviceReferences = bundle.getRegisteredServices();

// Arrêt du bundle (état active vers resolved)
bundle.stop();

Pour finir, le contexte offre la possibilité de spécifier des observateurs d'événements sur le conteneur OSGi lui-même, sur les bundles et sur les services. Cet aspect est couramment utilisé dans les bonnes pratiques de mise en œuvre de la technologie OSGi, notamment par l'intermédiaire du patron de conception Extender [13] au niveau des bundles.

Le principe consiste en la mise en œuvre d'un observateur utilisant les données des bundles afin de réaliser des traitements. Ce patron permet de mieux gérer les états des bundles lors de traitements généraux, traitements pouvant être réalisés de manière paresseuse. Cette approche est mise en œuvre dans des outils tels que Declarative Services [14], iPOJO[15], Spring Dynamic Modules [16].

Un observateur de bundles peut être mis en œuvre par l'intermédiaire des interfaces BundleListener et SynchronousBundleListener, respectivement déclenchées de manière asynchrone et synchrone. Les deux interfaces mettent en œuvre la méthode bundleChanged afin de notifier un changement pour un bundle. Cette méthode prend en paramètre un objet de type BundleEvent, objet permettant d'avoir accès au bundle impacté et à l'événement déclencheur dans le cycle vie. Le code suivant illustre le contenu de l'interface BundleListener :

public interface BundleListener extends EventListener {
void bundleChanged(BundleEvent event);
}

La mise en œuvre d'un observateur de bundles se réalise de la manière suivante :

// Enregistrement de l'observateur
BundleListener observateur = new BundleListener() {
public void bundleChanged(BundleEvent evenement) {
// Récupération du bundle impacté
Bundle bundle = evenement.getBundle();
int etat = evenement.getType();
(...)
}
};

context.addBundleListener(observateur);

VII - Mise en œuvre de services

Comme nous l'avons évoqué précédemment, la technologie OSGi offre la possibilité de mettre des services au niveau des composants. Elle permet de bien séparer le contrat du service (interface) de la ou des implémentations fournies par les composants. Les consommateurs du service n'ont connaissance que de son interface. De plus, les services mis en œuvre avec OSGi n'ont aucune adhérence avec les API de conteneurs et consistent en de simples POJO [17].

La technologie OSGi, par l'intermédiaire de l'interface BundleContext, offre la possibilité à un composant de mettre à disposition des services par l'intermédiaire de la méthode registerService. Le premier paramètre de cette méthode correspond au nom du service, nom visible au niveau du conteneur. Un usage courant consiste à spécifier le nom de l'interface du service comme nom du service.

Le code suivant décrit la manière d'enregistrer un service dans un conteneur OSGi par l'intermédiaire du contexte OSGi :

SimpleService service  = new SimpleServiceImpl();
ServiceRegistration serviceRegistration = context.registerService(
SimpleService.class.getName(), service, null);

Le désenregistrement d'un service OSGi se réalise par l'intermédiaire de la méthode unregister de l'instance de ServiceRegistration renvoyée précédemment par la méthode register. Une bonne pratique consiste donc à le garder en variable d'instance (de l'activateur par exemple). Le code suivant illustre la mise en œuvre d'un désenregistrement de service :

ServiceRegistration serviceRegistration = (...)
serviceRegistration.unregister();

Différentes méthodes sont également fournies afin d'avoir accès aux services afin de les utiliser. La récupération d'une instance de service se réalise en deux étapes. La première consiste en la récupération d'une instance de l'interface ServiceReference pour le service par l'intermédiaire de la méthode getServiceReference ou getServiceReferences. L'utilisation de la méthode getService avec en paramètre l'instance précédente permet d'avoir accès à l'instance du service et de l'utiliser.

Le code suivant décrit la manière d'avoir accès à une instance d'un service à partir de son nom et par l'intermédiaire du contexte OSGi :

// Récupération de la référence du service
ServiceReference reference = context.getServiceReference(
SimpleService.class.getName());

// Récupération de l'instance du service
SimpleService service = context.getService(reference);
(...)

Il est à noter que, dans le cas d'une utilisation d'un service en dehors du conteneur, le transtypage dans le type du service ne fonctionnera pas. En effet, les classes et interfaces gérées par le conteneur ne sont visibles et utilisables qu'en son sein. Un service OSGi reste néanmoins utilisable en se fondant sur l'introspection.

Nous pouvons également noter la présence d'une méthode ungetService au niveau du contexte afin de libérer l'instance du service référencée par l'instance de ServiceReference correspondante.

VIII - Conclusion

Dans ce premier article, nous avons introduit la technologie OSGi et les différentes problématiques qu'elle vise à adresser. Nous avons également décrit les caractéristiques de la technologie et comment mettre en œuvre des composants OSGi, composants désignés par le terme bundle dans la terminologie d'OSGi. Nous avons ainsi abordé les différentes couches structurant les conteneurs de ce type, à savoir les couches Module, Cycle de vie et Service.

Dans le prochain article, nous verrons comment mettre concrètement en œuvre des composants OSGi dans un conteneur embarqué dans un processus Java. Pour ce faire, nous utiliserons le conteneur OSGi libre Felix. Nous verrons également comment tirer parti de l'environnement Eclipse afin de développer des bundles OSGi.

IX - Bibliographie

1 - Getting started with OSGi, de Neil Bartlett.
http://neilbartlett.name/blog/osgi-articles/

2 - Getting started with OSGi, de Neil Bartlett.
http://www.eclipse.org/resources/?author=Neil%20Bartlett

3 - Cours et Tutoriel sur OSGi, de Didier Donsez.
http://www-adele.imag.fr/users/Didier.Donsez/cours/exemplesosgi/

Références :

[1] https://fr.wikipedia.org/wiki/Programmation_orient%C3%A9e_composant
[2] https://fr.wikipedia.org/wiki/Programmation_orient%C3%A9e_objet
[3] http://www.springframework.org
[4] https://fr.wikipedia.org/wiki/Fabrique_%28Motif_de_conception%29
[5] http://fr.wikipedia.org/wiki/Injection_de_d%C3%A9pendance#Principe_de_l.27inversion_des_d.C3.A9pendances
[6] https://dico.developpez.com/html/302-Generalites-SOA-Service-Oriented-Architecture.php
[7] https://dico.developpez.com/html/1750-Internet-SOAP-Simple-Object-Access-Protocol.php
[8] https://dico.developpez.com/html/1824-Internet-WSDL-Web-Services-Description-Language.php
[9] http://hivemind.apache.org
[10] http://www.eclipse.org
[11] http://felix.apache.org
[12] http://www.hibernate.org
[13] http://www.osgi.org/blog/2007/02/osgi-extender-model.html
[14] http://www.eclipsezone.com/eclipse/forums/t96740.html
[15] http://felix.apache.org/site/ipojo.html
[16] http://www.springframework.org/osgi
[17] http://fr.wikipedia.org/wiki/POJO