Accueil > DynaResume, OSGi > Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step3]

Conception d’un client Eclipse RCP et serveur OSGI avec Spring DM [step3]


Dans le billet précédant [step2] nous avons créé le Bundle OSGi org.dynaresume.domain et préparé l’environnement OSGi (Target Platform). Dans ce billet nous allons créer les 2 Bundles OSGi Services org.dynaresume.services, et Client org.dynaresume.simpleosgiclient et gérer leur dépendances via leur fichier MANIFEST.MF.

Voici un schéma de ce que nous allons effectuer dans ce billet :

Ce schéma met en évidence plusieurs notions :

  • les dépendances entre les Bundle OSGi s’effectuent via le fichier META-INF/MANIFEST.MF et plus par le Java Build Path classique.
  • l’appel des services par le client ne se fait plus par un main Java mais par la méthode start du BundleActivator.

En fin du billet nous reprendrons les 2 problèmes souléves avec le Java build Path classique et qui seront résolus avec OSGi:

Vous pouvez télécharger les projets org.dynaresume_step3.zip et org.dynaresume_step3-commons-lang.zip (zip qui contient les Bundle OSGi qui utilisent 2 versions de la librairie Apache commans-lang*.jar et qui montre en évidence le problème de ClassLoader résolu) présentés dans ce billet.

I .Bundle org.dynaresume.services

Ici nous allons transformer le projet Java classique org.dynaresume.services en Bundle OSGi. Les sources Java de notre Bundle seront identiques à celui du projet Java classique. Créez le Bundle org.dynaresume.services avec les paramètres suivants :

  • champs ID: org.dynaresume.services
  • champs Version: 1.0.0.qualifier.
  • champs Name: DynaResume Services
  • champs Execution Environment: J2SE-1.5.
  • option generate an activator, a Java class that controls the plug-in’s life cycle cochée.
  • Activator : org.dynaresume.services.Activator

Ajoutez la dépendance (avec Require-Bundle) au Bundle org.dynaresume.domain

Sauvegardez et vous pourrez vérifier que dans le MANIFEST.MF, il y a la dépendance Require-Bundle :

Require-Bundle: org.dynaresume.domain;bundle-version="1.0.0"

Créez l’interface org.dynaresume.services.UserService comme suit :

package org.dynaresume.services; 
 
import java.util.Collection; 
import org.dynaresume.domain.User; 
 
public interface UserService { 
 
  Collection<User> findAllUsers(); 
}

Ce code ne compile pas car il n’arrive pas à résoudre la classe org.dynaresume.domain.User. Nous avons pourtant mis une dépendance sur le Bundle org.dynaresume.domain via Require-Bundle. Ceci s’explique par le fait que le Bundle org.dynaresume.domain n’expose aucune classes et que par conséquent la classe org.dynaresume.domain.User n’est pas accéssible. En effet par défaut un Bundle n’expose aucune classes. Pour indiquer les classes que l’on souhaite exposer à d’autres Bundles, ceci s’effectue en exportant les packages des classes que l’on souhaite rendre accéssibles.

I-A .Export Package

Ici nous allons rendre accéssible les classes du package org.dynaresume.domain du Bundle org.dynaresume.domain en exportant le package org.dynaresume.domain. Cette information sera contenu dans le fichier MANIFEST.MF (balise Export-Package).

Nous allons modifier le MANIFEST.MF du Bundle org.dynaresume.domain via l’onglet Runtime de l’editor PDE:

Cliquez sur le bouton Add… qui ouvre la boite de dialogue qui propose tous les packages du Bundle.

Sélectionnez le package org.dynaresume.domain, puis cliquez sur OK. Le liste Export packaged doit être renseigné avec le package org.dynaresume.domain. Sauvegardez et l’interface UserService doit compiler à nouveau :

Vous pouvez vérifiez que cette action d’export package a mis à jour le fichier MANIFEST.MF avec ce contenu :

Export-Package: org.dynaresume.domain

I-B .Protection des Activator

Maintenant que nous avons exporté le package org.dynaresume.domain dans le Bundle org.dynaresume.domain, n’importe quel Bundle peut accéder aux classes du package org.dynaresume.domain qui à ce stade sont :

  • la classe org.dynaresume.domain.User qui est le domaine Utilisateur.
  • la classe org.dynaresume.domain.Activator qui est le BundleActivator qui affiche une trace dans la console OSGi lorsque le bundle est démarré et stoppé.

Le Bundle Activator ne doit pas être accéssible via un autre Bundle. Cette classe doit juste être accéssible par le conteneur OSGi. Hors à ce stade, il est possible d’accéder à la classe org.dynaresume.domain.Activator, dans des classes d’un autre Bundle. Pour vous rendre compte de ce problème, tappez Activator dans la classe UserService et vous pourrez constater qu’il est possible d’accéder à org.dynaresume.domain.Activator :

Pour régler, ce problème il faut mettre cette classe Activator dans un package protégé. Les plugins d’Eclipse utilise souvent le package internal pour mettre les classes que le Bundle ne doit pas exposer. Nous allons procéder de la même manière et mettre la classe Activator dans le package org.dynaresume.domain.internal.

I-B-1 .Refactor ->Move

Ici nous allons effectuer un refactoring pour mettre la classe org.dynaresume.domain.Activator dans le package org.dynaresume.domain.internal. Le fait de passer par un refactoring Eclipse, permettra de mettre à jour aussi le fichier MANIFEST.MF (Bundle-Activator).

Sélectionnez la classe org.dynaresume.domain.Activator, puis cliquez sur le bouton droit de la souris pour accéder au menu contextuel. Accédez à l’item Refactor->Move… :

La fenêtre de dialogue Move s’ouvre :

Cliquez sur le bouton Create Package…, la fenêtre de création de packages s’ouvre :

Saisissez dans le champs Name, la valeur org.dynaresume.domain.internal, puis cliquez sur OK, puis sur le bouton Finish . Le refactoring de changement de package s’est effectué et le MANIFEST.MF a été modifie aussi avec la valeur suivante :

Bundle-Activator: org.dynaresume.domain.internal.Activator

I-C .Sources de org.dynaresume.services

Dans cette section nous allons nous occuper des sources Java consituant le bundle Service.

I-C-1 .MANIFEST.MF / Export-Package

Exporter le package org.dynaresume.services du Bundle org.dynaresume.services pour exposer uniquement aux autres Bundles l’interface UserService et la factory ServicesFactory. Le fichier MANIFEST.MF du bundle org.dynaresume.services doit avoir ce contenu ajouté :

Export-Package: org.dynaresume.services

I-C-2 .Activator

Modifiez le package de la classe Activator du Bundle org.dynaresume.services dans le package org.dynaresume.services.internal. Le fichier MANIFEST.MF du bundle org.dynaresume.services doit avoir ce contenu modifié :

Bundle-Activator: org.dynaresume.services.internal.Activator

Modifiez le code de cette classe comme suit pour tracer le démarrage/stoppage du Bundle :

package org.dynaresume.services.internal;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

public class Activator implements BundleActivator {

	/*
	 * (non-Javadoc)
	 * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
	 */
	public void start(BundleContext context) throws Exception {
		System.out.println("Start Bundle [" + context.getBundle().getSymbolicName() + "]");
	}

	/*
	 * (non-Javadoc)
	 * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
	 */
	public void stop(BundleContext context) throws Exception {
		System.out.println("Stop Bundle [" + context.getBundle().getSymbolicName() + "]");
	}

}

I-C-3 .Classe UserServiceImpl

Créez la classe org.dynaresume.services.impl.UserServiceImpl comme suit :

package org.dynaresume.services.impl; 
 
import java.util.ArrayList; 
import java.util.Collection; 
import java.util.List; 
 
import org.dynaresume.domain.User; 
import org.dynaresume.services.UserService; 
 
public class UserServiceImpl implements UserService { 
 
  private Collection<User> users = null; 
 
  public Collection<User> findAllUsers() { 
    if (users == null) { 
      users = createUsers(); 
    } 
    return users; 
  } 
 
  private Collection<User> createUsers() { 
    List<User> users = new ArrayList<User>(); 
    users.add(new User("angelo", "")); 
    users.add(new User("djo", "")); 
    users.add(new User("keulkeul", "")); 
    users.add(new User("pascal", "")); 
    return users; 
  } 
}

Il est important de noter que le package de la classe UserServiceImpl est org.dynaresume.services.impl qui n’est pas un package exporté. Par conséquent cette classe sera protégée et aucun autre bundle ne pourra y accéder.

I-C-4 .Classe ServicesFactory

Créez la classe org.dynaresume.services.ServicesFactory comme suit :

package org.dynaresume.services; 
 
import org.dynaresume.services.impl.UserServiceImpl; 
 
public class ServicesFactory { 
 
  private static ServicesFactory INSTANCE = new ServicesFactory(); 
  private UserService userService = null; 
 
  private ServicesFactory() { 
 
  } 
 
  public static ServicesFactory getInstance() { 
    return INSTANCE; 
  } 
 
  public UserService getUserService() { 
    if (userService == null) { 
      userService = createUserService(); 
    } 
    return userService; 
  } 
 
  private UserService createUserService() { 
    UserService userService = new UserServiceImpl(); 
    return userService; 
  } 
}

il est important de noter que le package de la classe ServicesFactory est org.dynaresume.services qui est un package exporté. Par conséquent d’autres bundles pourront utiliser cette classe.

II .Bundle org.dynaresume.simpleosgiclient

Ici nous allons transformer le projet Java classique org.dynaresume.simplemainclient en Bundle OSGi. Les sources Java de notre Bundle seront identiques à celui du projet Java classique. Créez le Bundle org.dynaresume.simpleosgiclient avec les paramètre suivants :

  • champs ID: org.dynaresume.simpleosgiclient
  • champs Version: 1.0.0.qualifier.
  • champs Name: DynaResume Simple OSGi Client
  • champs Execution Environment: J2SE-1.5.
  • option generate an activator, a Java class that controls the plug-in’s life cycle cochée .
  • Activator : org.dynaresume.simpleosgiclient.internal.Activator

Ajoutez les dépendances (avec Require-Bundle) aux Bundles org.dynaresume.domain et org.dynaresume.services.

Modifiez le code de la classe org.dynaresume.simpleosgiclient.internal.Activator comme suit pour tracer le démarrage/stoppage du Bundle :

package org.dynaresume.simpleosgiclient.internal; 
 
import java.util.Collection; 
 
import org.dynaresume.domain.User; 
import org.dynaresume.services.ServicesFactory; 
import org.dynaresume.services.UserService; 
import org.osgi.framework.BundleActivator; 
import org.osgi.framework.BundleContext; 
 
public class Activator implements BundleActivator { 
 
  public void start(BundleContext context) throws Exception { 
    System.out.println("Start Bundle [" + context.getBundle().getSymbolicName() + "]"); 
    UserService userService = ServicesFactory.getInstance().getUserService(); 
    Collection<User> users = userService.findAllUsers(); 
 
    for (User user : users) { 
      System.out.println("User [login=" + user.getLogin() + ", password=" + user.getPassword() + "]"); 
    } 
  } 
 
  public void stop(BundleContext context) throws Exception { 
    System.out.println("Stop Bundle [" + context.getBundle().getSymbolicName() + "]"); 
  } 
 
}

Avant de relancer, vérifier que les 3 bundles OSGi sont cochés dans la configuration OSGi DynaResume :

Relancez (via OSGi DynaResume) Equinox et la console OSGi doit afficher ceci :

osgi>Start Bundle [org.dynaresume.domain]
Start Bundle [org.dynaresume.services]
Start Bundle [org.dynaresume.simpleosgiclient]
User [login=angelo, password=]
User [login=djo, password=]
User [login=keulkeul, password=]
User [login=pascal, password=]

Les Traces de type « Start Bundle…. » montre que la méthode start de chaque BundleActivator a été appelé.

III .Problème résolu par OSGi

III-A .UserServiceImpl protégé

Dans la classe Activator du Bundle org.dynaresume.simpleosgiclient, si vous tappez User, la complétion ne propose plus la classe UserServiceImpl, car cette classe n’est pas dans un package qui est exporté par le Bundle :

III-B .Bundle OSGi/ClassLoader

Ici nous allons effectuer les mêmes tests que ceux effectués dans le billet [step1] avec des projets Java classiques (dépendances classiques Java Build Path). Nous allons dans un premier temps montrer que chaque Bundle OSGi a son propre ClassLoader. Pour s’en rendre compte nous allons afficher le ClassLoader dans la console Eclipse.

Modifiez les 3 classes Activator en modifiant la méthode start comme suit :

public void start(BundleContext context) throws Exception {
  System.out.println("Start Bundle [" + context.getBundle().getSymbolicName() + "], ClassLoader= "+ Activator.class.getClassLoader());
}

Relancez (via OSGi DynaResume) Equinox et vous verrez dans la console OSGi :

osgi> Start Bundle [org.dynaresume.domain], ClassLoader= org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader@b5dac4
Start Bundle [org.dynaresume.services], ClassLoader= org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader@adb1d4
Start Bundle [org.dynaresume.simpleosgiclient], ClassLoader= org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader@1484a05
...

qui montre que chaque Bundle OSGi a son propre ClassLoader.

Avec les dépendances classiques Java Build Path, nous avons vu qu’il y avait un ClassLoader unique et nous avons mis en évidence ce problème avec l’utilisation de versions différentes de la librairie Apache commons-lang*.jar. Nous allons effectuer la même chose avec nos Bundle OSGi :

Ce schéma montre que :

  • le Bundle Client org.dynaresume.simpleosgiclient pointera sur la librairie commons-lang-1.0.jar de version 1.0. Cette librairie est stockée dans le repertoire lib (qu’il faut créer) du Bundle.
  • le Bundle Services org.dynaresume.services pointera sur la librairie commons-lang-2.4.jar de version 2.4. Cette librairie est stockée dans le repertoire lib (qu’il faut créer) du Bundle.

Vous pouvez télécharger le zip org.dynaresume_step3-commons-lang.zip qui contient les Bundles avec les librairies commans-lang*.jar.

Pour rappel, la différence entre ces 2 versions de commans-lang*.jar est que la classe utilitaire org.apache.commons.lang.StringUtils possède une méthode StringUtils#isBlank(String String) dans la version 2.4 et pas dans la version 1.0. Nous souhaitons utiliser StringUtils#isBlank(String String) dans la couche Services.

III-B-1 .Runtime/Add Jar (commons-lang-2.4.jar)

Ici nous allons ajouter au Bundle org.dynaresume.services la librairie jar commons-lang-2.4.jar. Cette dépendance se retrouvera au final dans le MANIFEST.MF du Bundle (Bundle-ClassPath). Ouvrez le fichier MANIFEST.MF du Bundle et cliquez sur l’onglet Runtime :

Cliquez sur le bouton Add…, la fenêtre de sélection de JAR s’ouvre :

Sélectionnez le JAR lib/commons-lang-2.4.jar, puis cliquez sur OK, la librairie est ajoutée :

Sauvegardez et vous pourrez voir que le MANIFEST.MF a été modifié en ajoutant ce contenu :

Bundle-ClassPath: lib/commons-lang-2.4.jar,
.

III-B-2 .Utilisation commons dans la couche Services

Modifiez la méthode start de la classe org.dynaresume.services.internal.Activator, BundleActivator de la couche Services pour utiliser la méthode utilitaire StringUtils#isBlank(String str) de commons-lang-2.4.jar :

public void start(BundleContext context) throws Exception {
  StringUtils.isBlank("");
  System.out.println("commons-lang-2.4 : StringUtils#isBlank() called.");

  System.out.println("Start Bundle ["+ context.getBundle().getSymbolicName() + "], ClassLoader= "  + Activator.class.getClassLoader());
}

III-B-3 .Runtime/Add Jar (commons-lang-1.0.jar)

Ajoutez la librairie commons-lang-1.0.jar dans le Bundle org.dynaresume.simplemainclient

III-B-4 .Utilisation commons dans la couche Cliente

Modifiez la méthode start de la classe org.dynaresume.simpleosgiclient.internal.Activator, BundleActivator de la couche Cliente pour utiliser la méthode utilitaire StringUtils#isEmpty(String str) de commons-lang-1.0.jar :

public void start(BundleContext context) throws Exception {
  StringUtils.isEmpty("");
  System.out.println("commons-lang-1.0.jar : StringUtils#isEmpty() called.");

  System.out.println("Start Bundle ["+ context.getBundle().getSymbolicName() + "], ClassLoader= "  + Activator.class.getClassLoader());
}

Relancez (via OSGi DynaResume) Equinox, la console OSGi affiche ceci :

osgi> Start Bundle [org.dynaresume.domain], ClassLoader= org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader@51052d
commons-lang-2.4 : StringUtils#isBlank() called.
Start Bundle [org.dynaresume.services], ClassLoader= org.eclipse.osgi.internal.baseadaptor.DefaultClassLoader@2a6f16
commons-lang-1.0.jar : StringUtils#isEmpty() called.
...

ce qui montre qu’il est possible d’avoir une version d’une librairie par Bundle sans avoir de conflit.

IV .Conclusion

A cette étape nous avons vu que les Bundle OSGi gérent leur dépendances (avec d’autres Bundles, avec des JAR internes au Bundle) via le fichier MANIFEST.MF avec Require-Bundle ou Import-Package. Un Bundle expose ses classes qu’il souhaite rendre visible aux autres Bundles via Export-Package qui est aussi une information contenu dans le MANIFEST.MF. Un Bundle a son propre ClassLoader. Les Bundles sont gérés par un conteneur OSGi (Equinox) qui fournit une console OSGi qui permet d’arrêter/stopper à chaud des Bundles.

Vous pouvez lire le billet suivant [step4].

Catégories :DynaResume, OSGi Étiquettes :