Thématiques principales

dimanche 3 février 2019

OSGI : Felix

Nous revoilà avec OSGI mais cette fois ci pour aller dans le concret, plus de blabla, maintenant on fait chauffer la JVM.

Le problème du choix

Bien tout d’abord il nous faut choisir un framework, c’est à dire une implémentation. Nous en avons vu plusieurs dans l’article précédent [osgi-fwk] et il nous faudra faire un choix. À mon sens il y avait plusieurs possibilité:

  • Knoplerfish [knoplerfish] est trop compliqué bien qu’à jour dans l'implémentation des versions OSGI
  • Karaf [karaf] est un peu trop lourd pour ce que nous voulons montrer ici (mais nous ferons un article dessus)
  • Equinox [equinox] est sympas je l’aurais choisi si je ne prévoyais pas de présenter mon framework equinox-loader [eq-loader] qui l’utilise
  • Concierge [concierge] est hors propos, on ne va pas faire de Iot
  • Donc en toute logique, on va essayer l’utilisation de OSGI avec Felix [felix]

Tout d’abord il faut télécharger la version courante: c’est ici [felix-down]

Dockerisation

Pour simplifier son utilisation, nous allons préalablement le dockeriser pour faciliter son exploitation, on construit donc un fichier Dockerfile dans un répertoire (moi je l’ai nommé felix-framework) contenant le processus de construction suivant:

FROM openjdk:8-jre-alpine

RUN wget http://mirror.ibcp.fr/pub/apache//felix/org.apache.felix.main.distribution-6.0.1.tar.gz \
&& tar zxvpf org.apache.felix.main.distribution-6.0.1.tar.gz && rm org.apache.felix.main.distribution-6.0.1.tar.gz

COPY . /felix-framework-6.0.1/
CMD cd /felix-framework-6.0.1; java -jar bin/felix.jar


Ensuite pour construire notre conteneur, un appel à la commande :

$ docker build -t felix-framework . 

Exploration

Maintenant que l’on à une image docker on va pouvoir l’utiliser à volonté. Explorons dans un premier temps les fonctionnalités du framework. Pour cela on  lance l’image docker en mode interactif

$ docker run -it --name felix --rm felix-framework

Et du coup on obtient le shell du framework OSGI de felix:

____________________________
Welcome to Apache Felix Gogo

g!

Si on exécute la commande bundles (permettant d'avoir la liste des bundles installé), on obtient


g! bundles                                                                                                                                    
    0|Active     |    0|org.apache.felix.framework (6.0.1)
    1|Active     |    1|org.fusesource.jansi (1.17.1)
    2|Active     |    1|org.jline (3.7.0)
    3|Active     |    1|org.apache.felix.bundlerepository (2.0.10)
    4|Active     |    1|org.apache.felix.gogo.command (1.0.2)
    5|Active     |    1|org.apache.felix.gogo.jline (1.1.0)
    6|Active     |    1|org.apache.felix.gogo.runtime (1.1.0)

Voilà, à ce stade que voyons nous? Une liste de 6 bundles formant ce que l’on pourrait appeler le minimum vital à une première utilisation de OSGI. Tous les bundles sont actifs cela signifie que leurs dépendances ont été résolue au chargement et que l’ensemble des services nécessaire chacun des démarrages ont été correctement servis.

Ainsi avec ce packaging, nous avons d’une part l'implémentation felix du framework OSGI fourni par le bundle org.apache.felix.framework et d’autre part un jeu de commande de base fourni par les bundles org.apache.felix.gogo. Ce sont ces derniers qui nous permettent d’avoir une interface CLI, mais sans eux, le framework fonctionnerait malgré tout (juste que l’on ne pourrait interagir avec sa configuration)

Felix est proposé avec d’autre bundles que l’on peut trouver sur la page [bundle-subproject]. Certains sont intéressant d’autres moins… en fait, il n’est pas forcément pertinent de s’attarder sur ceux ci puisque que nous le verrons, il en existe bien d’autres et qu’il est possible au pire de bundleliser toutes librairies compilées dans un format classique java.

Un exemple

Du coup le mieux est de faire simple: on va se construire un exemple Helloworld… (le grand classique) au travers de deux bundles l’un écrira sur la sortie standard la concaténation du mot “Hello” avec le résultat produit par un service OSGI du second lui fournissant “World”.

Le premier Bundle

Avant cela on va déjà s'intéresser à comment construire un bundle.  On va s’appuyer sur maven et en particulier le plugin maven org.apache.felix:maven-bundle-plugin :


<plugin>
 <groupId>org.apache.felix</groupId>
 <artifactId>maven-bundle-plugin</artifactId>
 <version>3.5.0</version>
 <extensions>true</extensions>
 <configuration>
  <instructions>
   <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
   <Bundle-Name>${project.name}</Bundle-Name>
   <Bundle-Version>${project.version}</Bundle-Version>
   <Bundle-Vendor>TC</Bundle-Vendor>
  </instructions>
 </configuration>
 <executions>
  <execution>
   <id>bundle-manifest</id>
   <phase>process-classes</phase>
   <goals>
    <goal>manifest</goal>
   </goals>
  </execution>
 </executions>
</plugin>

Ce plug in va permettre la construction d’un projet maven déclaré comme bundle (nous verrons le prototype).  Ainsi, dans l'idéal on va placer cet élément dans un pom parent dans une section plugin management.  De cette façon, on pourra produire autant de bundle que souhaité. Pour info ce bundle est ici [osgi-parent]

Maintenant du coup on va créer un pom de type bundle héritant du pom parent cité ci-dessus:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>org.tc.osgi.bundle.hello.world</groupId>
 <artifactId>tc-osgi-bundle-hello-world-consumer</artifactId>
 <name>${project.artifactId}-${project.version}</name>
 <packaging>bundle</packaging>
 <version>${build}${snapshot}</version>

 <properties>
  <build>0.2.0</build>
  <hw.producer.version>0.2.0</hw.producer.version>
  <snapshot>-SNAPSHOT</snapshot>
 </properties>

 <parent>
  <groupId>org.tc.parent</groupId>
  <artifactId>tc-osgi-parent</artifactId>
  <version>0.8.1</version>
 </parent>

 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
     <archive>
      <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
     </archive>
    </configuration>
   </plugin>
   <plugin>
    <groupId>org.apache.felix</groupId>
    <artifactId>maven-bundle-plugin</artifactId>
    <configuration>
     <instructions>
      <Bundle-Activator>org.tc.osgi.bundle.hello.world.consumer.module.activator.HelloWorldConsumerActivator</Bundle-Activator>
     </instructions>
    </configuration>
   </plugin>
  </plugins>
 </build>

 <dependencies>
  <dependency>
   <groupId>org.apache.felix</groupId>
   <artifactId>org.apache.felix.framework</artifactId>
   <version>6.0.1</version>
  </dependency>

 </dependencies>

</project>

On retrouve les éléments classiques d’un projet java classique à part donc le type de l'artefact, ici un bundle, et l’utilisation du plugin felix.

À cela, on va ajouter en dépendance felix lui même afin d’utiliser les API OSGI et définir notre premier activator (que l’on aura précisé dans le plugin)

Très bien maintenant avant d'espérer jouer avec des services il faut vérifier que l’on sait correctement démarrer notre bundle.


public class HelloWorldConsumerActivator implements BundleActivator {

    public HelloWorldConsumerActivator() { }

    @Override
    public void start(final BundleContext context) throws Exception {
       System.out.println("Hello world");
    }
 
    @Override
    public void stop(final BundleContext context) throws Exception {
    }
}

Bien sur cela va nous fournir un jar qui va falloir ajouter à notre image docker… donc on va donc créer un nouveau docker file dédier à la construction d’une nouvelle image basé sur la précédente mais incluant en plus notre bundle:


FROM collonvtom/felix-framework:latest

COPY . /felix-framework-6.0.1/bundle/
CMD cd /felix-framework-6.0.1; java -jar bin/felix.jar

On met le jar dans le répertoire avec le docker file et on deploi et on exécute ensuite notre nouvelle image:


$ docker build -t hello-world-felix . 
$ docker run -it --rm --name hw-felix hello-world-felix
Hello world
____________________________
Welcome to Apache Felix Gogo

g! bundles                                                                                                                                             
    0|Active     |    0|org.apache.felix.framework (6.0.1)
    1|Active     |    1|org.fusesource.jansi (1.17.1)
    2|Active     |    1|org.jline (3.7.0)
    3|Active     |    1|org.apache.felix.bundlerepository (2.0.10)
    4|Active     |    1|org.apache.felix.gogo.command (1.0.2)
    5|Active     |    1|org.apache.felix.gogo.jline (1.1.0)
    6|Active     |    1|org.apache.felix.gogo.runtime (1.1.0)
    7|Active     |    1|tc-osgi-bundle-hello-world-consumer (0.2.0.SNAPSHOT)

Constatations

Au démarrage felix installe et lance tous les bundles présent dans son répertoire “bundle”. C’est bien pour démarre avec les bundles indispensables mais c’est pas forcement top si l’on veut faire une application qui exploite le cycle de vie des bundles, on a peut être pas forcement envie de lancer tous les bundles et de faire une monté en charge inutile. Il faudra donc penser à déposer les bundles dans un autre répertoire et prévoir un script de chargement indépendant, surtout qu’ici malheureusement cela à le travers de nous masquer le fonctionnement du framework et d’explorer le cycle de vie des bundles.

On voit donc que notre bundle avec id:7 est lancé et on constate que sur la console, on à bien un message “Hello world”. Mais comme on aime bien vérifier et que le but est de comprendre le cycle de vie des bundles on va désinstaller et réinstaller le bundle manuellement (comme si on devait le mettre à jour) et vérifier que l’on est capable de le re exécuter:


g! uninstall  7 
g! install file:///felix-framework-6.0.1/bundle/tc-osgi-bundle-hello-world-consumer-0.2.0-SNAPSHOT-assembly.jar                               
Bundle ID: 8
g! bundles                                                                                                                                    
    0|Active     |    0|org.apache.felix.framework (6.0.1)
    1|Active     |    1|org.fusesource.jansi (1.17.1)
    2|Active     |    1|org.jline (3.7.0)
    3|Active     |    1|org.apache.felix.bundlerepository (2.0.10)
    4|Active     |    1|org.apache.felix.gogo.command (1.0.2)
    5|Active     |    1|org.apache.felix.gogo.jline (1.1.0)
    6|Active     |    1|org.apache.felix.gogo.runtime (1.1.0)
    8|Installed  |    1|tc-osgi-bundle-hello-world-consumer (0.2.0.SNAPSHOT)

g! start 8                                                                                                                                    
Hello world
g! stop 8                                                                                                                                     
g! bundles                                                                                                                                    
    0|Active     |    0|org.apache.felix.framework (6.0.1)
    1|Active     |    1|org.fusesource.jansi (1.17.1)
    2|Active     |    1|org.jline (3.7.0)
    3|Active     |    1|org.apache.felix.bundlerepository (2.0.10)
    4|Active     |    1|org.apache.felix.gogo.command (1.0.2)
    5|Active     |    1|org.apache.felix.gogo.jline (1.1.0)
    6|Active     |    1|org.apache.felix.gogo.runtime (1.1.0)
    8|Resolved   |    1|tc-osgi-bundle-hello-world-consumer (0.2.0.SNAPSHOT)

Ok on on voit bien que la désinstallation/réinstallation à provoqué l'incrémentation de l’ID du bundle  et celui-ci est dans l'état installé. La commande start permet de produire le message et le bundle passe dans l'état Activé. Enfin, un stop ramène le bundle dans l'état resolved.

Tout ca c’est bien mais bon on voulait avoir deux bundles, l’un disant Hello et allant chercher via un service le message d’un second disant World!

Second Bundle

Donc on va faire un second bundle dans lequel nous allons définir cette interface et son implémentation.


public interface IWorldService {

 public String world();
}

public class WorldServiceImpl implements IWorldService
{
 @Override
 public String world() {
  return "World";
 }
}

Pour exposer un service, il faut ensuite déclarer ce service via un ServiceFactory qui va consolider le service et l’instance de son implémentation:


public class WorldServiceFactory implements ServiceFactory{

 private IWorldService instance;

 public WorldServiceFactory(IWorldService instance) {
  super();
  this.instance = instance;
 }
 
 @Override
 public Object getService(Bundle bundle, ServiceRegistration registration) {
  System.out.println("Obtention du service WorldServiceFactory");
  return instance;
 }

 @Override
 public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {
  System.out.println("Relachement du service WorldServiceFactory");
  
 }
}

Il reste alors à implémenter l’activator de ce nouveau bundle en enregistrant la factory lors de son démarrage:


public class HelloWorldProviderActivator implements BundleActivator {

 private ServiceRegistration serviceReg;
 
 public HelloWorldProviderActivator() {   }
    
    @Override
    public void start(final BundleContext context) throws Exception {
     WorldServiceFactory factory=new WorldServiceFactory(new WorldServiceImpl());
     this.serviceReg=context.registerService(IWorldService.class.getName(),factory, null);
    }

    @Override
    public void stop(final BundleContext context) throws Exception {
     serviceReg.unregister();
    }
}

Du coup on a un bundle capable de fournir un service et de l’enregistrer dans le registre OSGI. Reste maintenant à modifier notre premier bundle de façon à ce qu’il consomme ce service.

Pour cela nous allons d’abord construire ce que l’on appelle un Tracker. Cela permet d’aller dans le registre OSGI et d’obtenir une instance de l’objet service que l’on souhaite utiliser:


public class WorldServiceTracker extends ServiceTracker {

    public WorldServiceTracker(final BundleContext context) throws InvalidSyntaxException, BundleException {
        super(context, IWorldService.class.getName(), null);
    }

    @Override
    public Object addingService(final ServiceReference reference) {
        System.out.println("Inside WorldServiceTracker.addingService " + reference.getBundle());
        return super.addingService(reference);
    }

    public IWorldService getUtilsService() {
        return (IWorldService) super.getService();
    }

    @Override
    public void removedService(final ServiceReference reference, final Object service) {
        System.out.println("Inside WorldServiceTracker.removedService " + reference.getBundle());
        super.removedService(reference, service);
    }
}

Du coup il ne reste qu’à utiliser ce tracker dans la méthode start de notre activator :


public void start(final BundleContext context) throws Exception {
 WorldServiceTracker tracker=new WorldServiceTracker(context);
 tracker.open();
 IWorldService service=tracker.getWorldService();
 System.out.println("Hello "+ service.world());
}

On build (mvn clean install) et on construit notre image docker et on lance le conteneur:


$ docker run -it --rm --name hw-felix hello-world-felix
Demarrage du bundle Consumer
ERROR: Bundle tc-osgi-bundle-hello-world-consumer [7] Error starting file:/felix-framework-6.0.1/bundle/tc-osgi-bundle-hello-world-consumer-0.2.0-SNAPSHOT-assembly.jar (org.osgi.framework.BundleException: Activator start error in bundle tc-osgi-bundle-hello-world-consumer [7].)
java.lang.NullPointerException
        at org.tc.osgi.bundle.hello.world.consumer.module.activator.HelloWorldConsumerActivator.start(HelloWorldConsumerActivator.java:22)
        at org.apache.felix.framework.util.SecureAction.startActivator(SecureAction.java:697)
        at org.apache.felix.framework.Felix.activateBundle(Felix.java:2398)
        at org.apache.felix.framework.Felix.startBundle(Felix.java:2304)
        at org.apache.felix.framework.Felix.setActiveStartLevel(Felix.java:1535)
        at org.apache.felix.framework.FrameworkStartLevelImpl.run(FrameworkStartLevelImpl.java:308)
        at java.lang.Thread.run(Thread.java:748)
Demarrage du bundle Provider
Enregistrement du service World
____________________________
Welcome to Apache Felix Gogo

g!

Quoi? une NPE? qu’est ce qui s’est passé? C’est simple on en parlait au début de l’article mais Felix démarre par défaut tous les bundles, sauf que s’il est capable de résoudre les dépendances statiques, concernant les services, il est incapable de savoir qui consomme qui.

Et la on constate que le consumer est démarré avant le provider… donc en toute logique, le service n’est pas encore disponible. Essayons alors de démarrer après coup le consumer (maintenant que le provider a été démarré):


g! bundles                                                                                                                                    
    0|Active     |    0|org.apache.felix.framework (6.0.1)
    1|Active     |    1|org.fusesource.jansi (1.17.1)
    2|Active     |    1|org.jline (3.7.0)
    3|Active     |    1|org.apache.felix.bundlerepository (2.0.10)
    4|Active     |    1|org.apache.felix.gogo.command (1.0.2)
    5|Active     |    1|org.apache.felix.gogo.jline (1.1.0)
    6|Active     |    1|org.apache.felix.gogo.runtime (1.1.0)
    7|Resolved   |    1|tc-osgi-bundle-hello-world-consumer (0.2.0.SNAPSHOT)
    8|Active     |    1|tc-osgi-bundle-hello-world-provider (0.2.0.SNAPSHOT)

g! start 7                                                                                                                                    Demarrage du bundle Consumer
Inside WorldServiceTracker.addingService tc-osgi-bundle-hello-world-provider [8]
Obtention du service WorldServiceFactory
Hello World
g!

Cool! Ça fonctionne! le bundle est allé consommé le service est a récupéré la partie World du message.

Conclusions

Bien on a fait deux bundles, l’un consommant le service de l’autre seulement différents points peuvent être améliorés:

  • La construction des services et leur consommation via les Factory et les Tracker, c’est un peu lourd … mais on verra qu’il existe des solutions (dans d’autres articles).
  • Avec l’approche que nous avons mise en place pour consommer le service, nous avons réalisé une consommation ponctuel de celui-ci, seulement, nous ne l’avons pas vraiment officielle relâché. D’autre part il est parfois utile de le garder sous la main, il est alors préférable d’utiliser un pattern proxy couplé à un singleton de façon à rendre facilement accessible le service au sein de l’ensemble du bundle (nous verrons des exemples également).
  • Nous avons également créer une dépendance forte entre les deux bundles, juste parce que l’un utilise le service offert par l’autre. Il aurait fallu créer un troisième bundle ne contenant que les interfaces et permettant de découpler les contrats de services des implémentations et donc faciliter la  mise à jour des implémentations associées à ces interfaces.

Voila si vous souhaitez retrouver le code de cet article, il se trouve ici [osgi-hello-world]

Références


  • [osgi-fwk] https://un-est-tout-et-tout-est-un.blogspot.com/2019/01/osgi-les-frameworks.html
  • [knoplerfish]  https://www.knopflerfish.org/
  • [karaf] http://karaf.apache.org/manual/latest/#_overview
  • [equinox] http://www.eclipse.org/equinox
  • [eq-loader] https://github.com/collonville-tom/tc-equinox-loader
  • [concierge]  https://www.eclipse.org/concierge/
  • [felix] http://felix.apache.org/
  • [felix-down] http://felix.apache.org/downloads.cgi
  • [bundle-subproject] http://felix.apache.org/documentation/subprojects.html
  • [osgi-parent] https://github.com/collonville-tom/tc-parent/tree/master/tc-osgi-parent
  • [osgi-hello-world] https://github.com/collonville-tom/tc-hello-world


Aucun commentaire:

Enregistrer un commentaire