Thématiques principales

jeudi 24 janvier 2019

OSGI : Architecture

Nous avions vu dans un précédent article ce qu'était OSGI dans ses grandes lignes et ses principaux concepts [uetteu-osgi]. Dans celui ci, je vous propose de rentrer un peu plus dans le côté technique de cette technologie en détaillant globalement les utilisations de celle ci (surtout pour rappel), d’identifier son écosystème et ensuite de passer en revue la construction des bundles, leur constitution, leur cycle de vie et la déclaration et consommation des services.

À la suite de cela, nous nous intéresserons alors aux différents implémentations du framework réalisé au fil de ces dernières années pour enfin étudier (sommairement) leur différentes intégrations au sein des serveurs d’application Java les plus connu (à notre insu)

Architecture: Vue d’ensemble

Nous avions vu dans ce dernier article [uetteu-osgi] que OSGI était un framework Java modulaire orienté service permettant la modélisation et l'exécution de composants nommés bundle.

Ils permettent à mise en oeuvre d’application dans des contextes d'équipement à ressources limitées, s’appuient sur un modèle de collaboration utilisant une registry pour le partage de services et facilitant l'accès à ces derniers pour les consommateurs.

OSGI permet aussi une gestion à chaud de ses bundles offrant ainsi la possibilité de chargement, mise à jour et déchargement de fonctionnalité dynamiquement sans interruption de service.

Enfin OSGI offre un cycle de vie fourni un cycle de vie à ces bundles et services (que nous verrons dans un des chapitres suivant) permettant la mise en oeuvre d’une gestion intelligente des fonctionnalités mis en ligne.

C’est pour ces différentes raisons que OSGI est aujourd’hui intégrée par défaut dans la grande majorité des serveurs d’applications JEE permettant l’ajout de fonctionnalité java standard de manière plus aisé et ce même dans un contexte JEE.

Environnement d'exécution

De par son côté modulaire, OSGI offre donc la possibilité de construire des applications via une logique de puzzle où de briques, constitué selon des niveaux fonctionnelles différentes. Cette approche a donc permis de constituer un catalogue riche et varié de bundles offrant une multitude de services métiers et ou technique [tuto-osgi-oscar].

On trouvera ainsi facilement sous la forme de bundle des

  • services de base permettant l’utilisation de logs
  • bundle de gestion de configuration
  • bundles de gestions de Préférences
  • Services HTTP (e.g :servlets, ou Spark [uetteu-spark]))
  • Gestion des utilisateurs
  • Parseurs XML
  • Gestion de droits,
  • Politique de sécurité
  • Monitoring
  • de l'intégration JEE [integ-jee]



Mais aussi des bundles offrant des services qui supportent le monde de l'électronique : Gestion de périphériques, connecteurs d'entrés-sorties, Gestion du plug'n play (pnp et Upnp), Gestion de l'alimentation et de l'énergie etc….

L’idée ici n’est pas d’entrer dans le détail de tous ces bundles car la bundlisation  des librairies classiques java (log4j, etc….) s’est faite discrètement mais de façon très vaste et il existe maintenant presque autant de bundle que de librairies maven dans le maven central repository. Nous verrons comment les distinguer (voir chapitre sur les bundles).

Ainsi les applications OSGI vont utiliser ces briques pour être modélisé puis assemblé. Ces éléments vont alors s'exécuter dans un environnement où chaque bundle aura son propre espace. La partie détaillant le cycle de vie d’un bundle est abordé plus loin dans cet article cependant, il faut comprendre que le framework OSGI va fournir aux bundles un contexte de chargement où les différents classloader seront séparés.  Cette approche va avoir différents avantages, car bien que contraignant en terme d’accessibilité aux classes dans les autres bundles, cela permet de rendre indépendant leur cycle de vie dans l’application.

C’est donc vous l’aurez compris grâce à ce mécanisme de séparation des classloader que OSGI va permettre le chargement et le déchargement à chaud des bundles dans l’environnement d'exécution (nommé le BundleContext).



Le schéma ci dessus illustre cette représentation et les différentes couches structurant une application OSGI. Nous avons bien évidemment la JVM et par dessus le framework OSGI laissant la possibilité au appplication d'utiliser JNI si nécessaire (nous verrons que l’on pourra à ce propos s’appuyer sur les Fragments).

Ainsi, sans rentrer dans le cycle de vie de bundles qui sera traité dans un chapitre dédié, ils sont chargés dans le BundleContext et ont chacun leur propre classloader. Ensuite le framework cherchera à faire évoluer son état selon la résolution possible de ses dépendances et de ses services.

Les bundles

Nous l’avons vu, l'élément central de OSGI c’est le bundle. Il est, d’un point de vue statique, l'équivalent du jar dans le monde java.

En fait, même, il s’agit …. d’un jar! Oui mais d’un jar un peu particulier.

Le bundle est un jar dont le manifest a été enrichi avec de nombreuses informations complémentaires qui vont permettre au framework OSGI, d’une part de le reconnaître mais aussi du coup d'être capable de le gérer comme d’un bundle à part entière.

Cette gestion comporte différents niveau:

  • statique (relation de dépendance du bundle, version de celui-ci, version des package exposé, versions des package consommé, etc..)
  • dynamique (gestion du cycle de vie, etat du bundle, point d'entrée de celui ci l’activator, etc..)


Nous aborderons plus dans le détails la partie dynamique du bundle dans la section “cycle de vie” de cet article.

Intéressons nous avant cela à sa composant statique.

Pour qu’un bundle soit un bundle, nous venons donc de dire qu’il faut déjà que celui ci soit un jar. Mais en java, être un jar n’est pas suffisant pour être chargé dans une machine virtuel, il faut qu’il existe un manifest.

Un manifest classique, classiquement ne comporte pas forcément beaucoup d’information, et celle-ci sont assez peu pertinente en dehors peut être de la déclaration de la version java de build (permettant de garantir l'exécution dans une JVM adapté) et la déclaration du main-class, etc… :


Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: CollonvilleThomas
Class-Path: ./ ./bundles/ ./bundles/tc-osgi-bundle-utils-0.3.4-assembl
 y.jar ./bundles/tc-osgi-bundle-utils-interfaces-0.1.3-assembly.jar ./
 tc-osgi-bundle-utils-0.3.4-assembly.jar ./tc-osgi-bundle-utils-interf
 aces-0.1.3-assembly.jar ./log4j-1.2.17.jar ./org.eclipse.osgi-3.11.2.
 v20161107-1947.jar ./tc-osgi-bundle-utils-0.3.4.jar ./tc-osgi-bundle-
 utils-interfaces-0.1.3.jar
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_181
Main-Class: org.tc.osgi.equinox.loader.EquinoxLoaderMain


Dans OSGI nous allons trouver beaucoup d’autres informations. Si par exemple l’application ci dessus est une application classique (donc n’est pas un bundle) celle-ci justement encapsule OSGI et va exécuter un contexte d'exécution pour des bundles. Prenons le cas du bundle basique utils que l’on voit en dépendance du classpath:


Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven Bundle Plugin
Built-By: CollonvilleThomas
Build-Jdk: 1.8.0_181
Class-Path: ./ ./tc-osgi-bundle-utils-0.3.4/ ./tc-osgi-bundle-utils-in
 terfaces-0.1.3-assembly.jar ./log4j-1.2.17.jar ./org.eclipse.osgi-3.1
 1.2.v20161107-1947.jar
Bnd-LastModified: 1547419362610
Bundle-Activator: org.tc.osgi.bundle.utils.module.activator.UtilsActiv
 ator
Bundle-Description: Un bundle pour exposer des composants et utilitaires
Bundle-ManifestVersion: 2
Bundle-Name: tc-osgi-bundle-utils-0.3.4
Bundle-SymbolicName: tc-osgi-bundle-utils
Bundle-Vendor: TC
Bundle-Version: 0.3.4
Export-Package: org.tc.osgi.bundle.utils.conf.gen;uses:="javax.xml.bin
 d,org.tc.osgi.bundle.utils.conf.jaxb";version="0.3.4",org.tc.osgi.bun
 dle.utils.conf.jaxb;uses:="javax.xml.bind.annotation,org.tc.osgi.bund
 le.utils.interf.conf.exception";version="0.3.4",org.tc.osgi.bundle.ut
 ils.conf;uses:="org.tc.osgi.bundle.utils.conf.jaxb,org.tc.osgi.bundle
 .utils.interf.conf,org.tc.osgi.bundle.utils.interf.conf.exception";ve
 rsion="0.3.4",org.tc.osgi.bundle.utils.context;uses:="org.osgi.framew
 ork,org.tc.osgi.bundle.utils.interf.context,org.tc.osgi.bundle.utils.
 interf.exception";version="0.3.4",org.tc.osgi.bundle.utils.context.ut
 ils;uses:="org.osgi.framework";version="0.3.4",org.tc.osgi.bundle.uti
 ls.logger;uses:="org.apache.log4j,org.tc.osgi.bundle.utils.interf.log
 ger";version="0.3.4",org.tc.osgi.bundle.utils.module.activator;uses:=
 "org.osgi.framework,org.tc.osgi.bundle.utils.interf.conf.exception,or
 g.tc.osgi.bundle.utils.interf.exception,org.tc.osgi.bundle.utils.inte
 rf.module.utils";version="0.3.4",org.tc.osgi.bundle.utils.module.util
 s;uses:="org.osgi.framework";version="0.3.4",org.tc.osgi.bundle.utils
 .pattern.command;uses:="org.tc.osgi.bundle.utils.interf.pattern.comma
 nd,org.tc.osgi.bundle.utils.interf.pattern.command.exception";version
 ="0.3.4",org.tc.osgi.bundle.utils.rmi.client;uses:="org.tc.osgi.bundl
 e.utils.interf.conf.exception,org.tc.osgi.bundle.utils.interf.excepti
 on,org.tc.osgi.bundle.utils.interf.rmi";version="0.3.4",org.tc.osgi.b
 undle.utils.rmi.server;uses:="org.tc.osgi.bundle.utils.interf.conf.ex
 ception,org.tc.osgi.bundle.utils.interf.rpc";version="0.3.4",org.tc.o
 sgi.bundle.utils.serial;uses:="org.tc.osgi.bundle.utils.interf.serial
 ";version="0.3.4"
Import-Package: javax.xml.bind,javax.xml.bind.annotation,org.apache.lo
 g4j;version="[1.2,2)",org.apache.log4j.xml;version="[1.2,2)",org.osgi
 .framework;version="[1.8,2)",org.tc.osgi.bundle.utils.conf,org.tc.osg
 i.bundle.utils.conf.jaxb,org.tc.osgi.bundle.utils.interf.collection;v
 ersion="[0.1,1)",org.tc.osgi.bundle.utils.interf.conf;version="[0.1,1
 )",org.tc.osgi.bundle.utils.interf.conf.exception;version="[0.1,1)",o
 rg.tc.osgi.bundle.utils.interf.context;version="[0.1,1)",org.tc.osgi.
 bundle.utils.interf.exception;version="[0.1,1)",org.tc.osgi.bundle.ut
 ils.interf.logger;version="[0.1,1)",org.tc.osgi.bundle.utils.interf.m
 odule.service;version="[0.1,1)",org.tc.osgi.bundle.utils.interf.modul
 e.utils;version="[0.1,1)",org.tc.osgi.bundle.utils.interf.pattern.com
 mand;version="[0.1,1)",org.tc.osgi.bundle.utils.interf.pattern.comman
 d.exception;version="[0.1,1)",org.tc.osgi.bundle.utils.interf.rmi;ver
 sion="[0.1,1)",org.tc.osgi.bundle.utils.interf.rpc;version="[0.1,1)",
 org.tc.osgi.bundle.utils.interf.serial;version="[0.1,1)",org.tc.osgi.
 bundle.utils.logger,org.tc.osgi.bundle.utils.module.utils,org.tc.osgi
 .bundle.utils.rmi.client,org.tc.osgi.bundle.utils.serial
JAVA_1_8_HOME: C:\Program Files\Java\jdk1.8.0_181
Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
Tool: Bnd-3.5.0.201709291849


Nous voyons que nous retrouvons les informations standard d’un jar classique (version du manifest, mainteneur, version du jdk utilisé pour builder et donc par extension pour son exécution) mais à cela on va trouver différentes informations propre à l’utilisation de ce jar en tant que Bundle OSGI.
Entre autre:

  • l’activateur: nous en avons parlé celui ci permet de faire l’interface entre le framework et le cycle de vie du bundle en gérant les démarrage et arrêt de celui ci (nous y reviendrons dans la partie cycle de vie)
  • les noms du bundle
  • sa version est est un élément important car celui ci permet de tagger les packages et services du bundle pour orienter par les autres bundles
  • la liste des packages exportés et versionné dans la version courante cette section permet aux autres bundles d'accéder aux classes présent dans ces packages, et à ce titre ces bundles devront alors déclarer ces packages comme importé (voir section suivante)
  • la liste des packages importés et versionné sur une plage de version stable.
  • Des propriétés d’environnement
  • la version du template du manifest
  • etc…


Nous verrons que ce manifest n’est pas créé à la main, heureusement qu’il existe des outils cependant il est importe de garder en tête sa structure afin d'être en mesure d’analyser les anomalies de dépendances.

À noter également que les règles de construction du versionning suivi doivent respecter quelque critère qui seront à mettre aligner selon vos critères d’evolutivités/compatibilité de vos bundles.

Ainsi sur un versionning sur 3 digit comme nous l’avons ici, (par exemple 0.3.4), généralement on considère que le dernier digit sont les corrections de bugs et ne sont pas sensé impacter les contrats d’utilisation des classes et interfaces et donc toutes les modifications allant dans ce sens sont ignoré en terme de package exposé par OSGI. C’est pour cela qu’il n’y à en import que deux digits (une précision nous amène donc à gérer les deux digit principaux comme étant des évolutions soit d’interfaces mineurs soit fonctionnelle majeur. Le premier permet de garantir la compatibilité ascendante, la seconde non (signifiant qu’une modification mineur d’interface “ajoute des éléments non impactant les anciens structures alors qu’une modification majeur rend obsolète l’utilisation d’une où toute partie du bundle dans son ancienne version).

Même si dans un premier temps, ces préoccupations sont assez peu utiles, elles deviendront rapidement un enjeu dans la gestion du projet OSGI afin d’en tirer les meilleurs bénéfices. Cette tâche de catégorisation des évolution est loin d'être  simple mais une fois mise en place par des méthodes d’analyses techniques et fonctionnelles, elles fournissent un moyen fiable et sûr de donner une compatibilité continu aux différentes version d’un même bundle et ainsi de supporter sans rupture les mise à jour à chaud du framework.

Cycle de vie

Nous avons un peu aborder la question du cycle de vie des bundles lorsque nous avons abordé l'environnement d'exécution OSGI, mais nous ne savions pas ce qu'était un bundle et donc il n'était pas aisé de décrire sont cycles de vie (effet de présentation des aspects structurante avant les aspects comportementaux).



Maintenant nous pouvons décrire ce cycle. Comme présenté par le diagramme à état précédent, un bundle pour être utilisable dans OSGI va devoir passer quelques étapes d'activations afin de garantir son intégration dans le système logiciel. Cela peut avoir un côté contraignant mais cela permet de détecter au plus tôt un certain nombre d’anomalies avant la mise en activité du bundle (comme des problèmes de résolution de dépendances par exemple).

La première étape est le chargement du bundle lui même. Cette étape est l’installation. Elle consiste à spécifier au framework la localisation physique du jar (son emplacement sur le disque).

Cette étape est la première étape de la résolution des dépendances et va mener automatiquement à l'état suivant “Resolved” si l’ensemble des dépendances d’importation du bundle sont satisfaites. Si ce n’est pas le cas alors le bundle restera dans l'état “Installed” dans l’attente que les résolutions soient finalement validées.
Bien sur il est possible de désinstaller un bundle, inutile de s'attarder sur la question, vous avez compris le principe! A noter quand même que cette action décharge le classloader donc libère la mémoire allouée nécessaire au bundle.

L'étape suivante est ensuite le démarrage du bundle amenant alors à l'état Active. Cette est étape est importante et complexe car elle implique des développements particulier. En effet si dans la phase précédente, la résolution des dépendances s'appuyait sur ce qui est déclaré dans le Manifest et consistait surtout en la résolution de l’accessibilité des package. Le démarrage du bundle est plus complexe car il consiste d’une part à initialiser les services produits et les enregistrer dans le registre de service OSGI pour les rendre accessible aux autres bundles et, bien que cela ne soit pas obligatoire à ce stade, a pre-cablee la consommation des services fourni par les autre bundles (cette phase de pré câblage est un choix de conception, si un service est consommé de façon régulière dans le bundle, il peut être intéressant d’avoir un objet ayant déjà préparer son utilisation, mais une approche paresseuse peut être envisagé)

Bien sur il est possible de revenir à l'état Resolved en stoppant le bundle. Cette étape est assez clair, elle va relâcher les services consommé et stopper les services produit (où pas selon si on veut interdire la possibilité d’avoir des interruptions de service.

Dans le concret, l’interaction avec le cycle de vie des bundles est réalisée selon deux interfaces:
La première est une interface du framework OSGI lui-même qui peut être un CLI ou une interface web et permettant de produire les événements amenant les transitions. (nous traiterons d’un exemple dans un autre article montrant cette interface)
La seconde interface est la déclaration d’une classe héritant de BundleActivator (et déclaré dans le Manifest) et permettant de réaliser les actions start et stop et facilitant la mise en oeuvre des initialisations du bundle, la consommation des services, l’exposition des services etc… (nous profiterons de l’exemple pour présenter à cet effet les outils à développer pour construire correctement cet élément et ses éléments satellites, nous en allons en parlant dans le chapitre suivant)

Les Services OSGI

On l’aura compris, les services sont les derniers éléments de la chaîne dans l’environnement OSGI. En effet, les services permettent de garantir l'indépendance des bundles entre afin de les solliciter individuellement dans leur contexte d'exécution propre.

Il existe deux cas d’utilisations d’un service:
la production du service par le bundles
la consommation du service par le bundles.



Comme énoncé dans le chapitre précédent, ces deux cas d’utilisations vont être traités lors de l’activation du bundle (ou son démarrage). Ils vont pourtant faire intervenir des mécanismes d’initialisations et d’utilisation différents et vont également impliquer la mise en oeuvre de “bundles d’interfaces” dont le rôle sera de faciliter le partage des contrats d’interfaces (en effet si l’on définit les interfaces de nos services dans le même bundle que son implémentation, alors un consommateur de ce service aura une dépendance vers cette implémentation alors qu’elle n’est pas nécessaire) favorisant au passage l'interopérabilité avec des implémentations variées.

À cela il va ensuite être nécessaire de mettre en oeuvre certaines API du framework OSGI afin de mettre en oeuvre d’un côté la mise en ligne de services au sein de la registy du framework et de l’autre la consommation d’autres services présent dans cette même registry.

Comment fait on cela?



Dans le cas de la production d’un service, une implémentation du service est injecté dans une factory qui sera ensuite enregistré via le BundleContext fourni par l’Activator du bundle.



À l’inverse pour consommer un service, il faudra passer par un proxy implémentant l’interface du service et utilisant un tracker qui aura pour rôle d’extraire le service recherché depuis le bundle context et ce de façon à initialiser le proxy sur le service et le rendre accessible au travers du bundle (au travers d’un singleton par exemple).

Grâce à ces deux mécanismes, il va donc être possible de mettre en communication les bundles via les services. Cependant on voit que cette approche peut être assez coûteuse en développement juste pour produire ou consommer un service. Nous verrons qu’il existe des extensions permettant de simplifier la mise en relation des composant entre via les services. Ces extensions s’appuie généralement sur des fichier xml comme le fait Spring avec son framework Spring DM/Virgo [SpringDM, VIRGO], nous reviendrons sur ce point car si l’approche n’est pas dénué d'intérêt, il comporte aussi des inconvénients.

Les fragments

Les fragments [vogella] sont des éléments un peu spéciaux dans le framework OSGI. Ils répondent au besoin de fournir pour un même bundle une implémentation qui peut être différente selon la situation comme par exemple pour répondre à une problématique de compatibilité avec l’OS. Pour cela, le fragments aura sa propre définition dans un manifest et déclaré comme étant une extension d’un autre bundle [fragment-ibm] et ce en partageant avec lui le même classloader (oui car comme nous l’avons vu, si le fait pour chaque bundle d’avoir son propre classloader donne la capacité d'être indépendant, cela amène aussi des contraintes pour un partage dynamique de la définition des classes).

Ainsi par exemple, et il s’agit du cas le plus évident, dans la gestion d’une interface graphique, la machine virtuelle java va être amené à utiliser des ressources du système afin de construire fenêtre et widget et cela de manière compatible avec celui ci. Pourtant, la gestion des interface graphique est loin d'être homogène selon les OS, entre le server X [XServ] (linux) et le système de fenêtre de windows [wind] (on entrera pas dans les precisions) il a donc fallu faire des passerelles.

Ainsi, le système des fragments de OSGI à pour objectif premier de répondre à cette problématique en fournissant dans le bundle une interface dans l’interface (qu’est le bundle lui même) afin de plugger dessus des détails d'implémentations propre à la plateforme [].

Les fragment OSGI permettent donc comme nous l’avons vu d'étendre un bundle avec un implémentation spécifique à la plateforme mais ils permettent également:

  • de gérer un fichier de configuration
  • d’utiliser un fichier de ressources (de langue par exemple)
  • de charger dynamiquement des classes (point de vue plugin)
  • de charger des implémentations native (point de vue compatibilité avec la plateforme)

Conclusion

Nous sommes rentré plus en détail dans le fonctionnement de OSGI que dans le précédent article mais nous n’avons encore qu’assez peu traité de cas concret pour la simple raison qu’il faut pour cela présenter les solutions, les implémentations existantes et les évolutions que celles ci apportent.

Il faut dire que ce que nous avons vu jusque la sont les concepts de base et partagés entre toutes les versions de OSGI mais, par exemple, la version 6 de OSGI propose d’utiliser des annotations pour déclarer les éléments des bundles. Nous essayerons donc d’explorer dans les futurs articles ces différentes nouveautés en passant les différentes implémentations existantes.

On notera également que nous n’avons pas aborder la question de l’outillage nécessaire à la production des bundles. Nous tâcherons aussi de voir ce qu’il est possible de faire dans un environnement classique Java/Maven.

References

[uetteu-osgi] https://un-est-tout-et-tout-est-un.blogspot.com/2018/12/osgi-concepts-generaux.html
[uetteu-spark] https://un-est-tout-et-tout-est-un.blogspot.com/2019/01/server-web-leger-spark.html
[tuto-osgi-oscar] http://lig-membres.imag.fr/donsez/cours/exemplesosgi/tutorialosgi.htm
[integ-jee] https://dzone.com/articles/osgi-gateway-micro-services
[VIRGO] https://www.eclipse.org/virgo/
[SpringDM] https://dzone.com/refcardz/getting-started-spring-dm?chapter=1
[fragment-ibm] https://www.ibm.com/support/knowledgecenter/SSRTLW_9.6.0/com.ibm.aries.osgi.doc/topics/cbundlefragment.html
[vogella] http://blog.vogella.com/2016/02/09/osgi-bundles-fragments-dependencies/
[XServ] https://doc.ubuntu-fr.org/xorg
[wind] https://fr.wikipedia.org/wiki/Gestionnaire_de_fen%C3%AAtres

Aucun commentaire:

Enregistrer un commentaire