Tutoriel pour tester ses commandes OSGi avec Karaf et PaxExam

Sébastien Foubert

Cet article a pour but de présenter OSGi et ses concepts dans un premier temps. Ensuite, de fournir un exemple basique de développement de composants OSGi à l'aide d'Apache Karaf 4. Enfin montrer comment on peut écrire des tests d'intégration à l'aide de Pax Exam, afin de tester ses commandes.

N'hésitez pas à commenter cet article sur le forum : Commentez

- Titre : Tutoriel pour tester ses commandes OSGi avec Karaf et PaxExam

- Auteur : Sébastien Foubert

- Parution : 1 octobre 2017

- Licence : Copyright ® 2016 Sébastien Foubert. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Définition de OSGi

- II. Présentation de Karaf

- III. Projet OSGi exemple

- III-A. Service JNDI pour la Datasource

- III-B. Services et entités JPA

- III-C. Développement de commandes Karaf

- III-D. Packaging kar

- III-E. Tests d'intégration des commandes avec PaxExam

- IV. Conclusion

- V. Remerciements

I - Définition de OSGi

OSGi (Open Service Gateway Initiative) est une spécification permettant de concevoir une application basée sur une architecture modulaire. Chaque module, appelé bundle. Il possède une version et peut dépendre d'autres modules. OSGi gère différents classloader par bundle. Ceci permet une gestion dynamique des bundles, donc de déployer plusieurs versions d'un même composant. Le redéploiement partiel et à chaud de composants est donc ainsi possible, ce qui permet une mise à jour de versions applicatives sans coupure de service.

II - Présentation de Karaf

Karaf est un framework open source de la fondation Apache, basé sur OSGi. La dernière version (4.0.3) est téléchargeable sur le site ici. Il faut ensuite se positionner dans le répertoire bin/ et lancer la commande : karaf. La console shell de Karaf présente alors un prompt qui permet de lancer des commandes :

Karaf s'appuie sur Apache Aries pour fournir un mécanisme d'injection de dépendances pour OSGi. Un bundle est constitué d'un fichier Blueprint XML qui permet d'instancier des composants, d'injecter des dépendances entre ces composants, d'exposer des services à travers le Service Registry, d'injecter des références vers d'autres services d'autres bundles.

1. <?xml version="1.0" encoding="UTF-8"?>

2. <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">

3. ...

4. </blueprint>

Le plugin maven maven-bundle-plugin va permettre de packager le bundle et de générer le fichier MANIFEST.MF définissant un nom Bundle-SymbolicName, une version Bundle-Version, les packages exportés Export-Package, et importés Import-Package, ou encore les packages contenant les commandes Karaf-Commands.

Un bundle peut être installé manuellement. Par exemple :

> bundle:install -s mvn:com.h2database/h2/1.4.190

Mais, il peut être également installé par l'intermédiaire d'une feature. Une feature est constituée d'un fichier description XML qui liste les bundles que l'on souhaite installer au démarrage.

1. <features name="${project.artifactId}-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0">

2. <feature name='${project.artifactId}' description='${project.name}' version='${project.version}'>

6. <details>${project.description}</details>

10. <feature version="4.0.3">jdbc</feature>

11. <feature version="4.3.6.Final">hibernate</feature>

12. <feature version="2.2.0">jpa</feature>

13. ...

14. <bundle start-level="70">mvn:com.h2database/h2/1.4.190</bundle>

15. ...

16. </feature>

17. </features>

Karaf vient avec un ensemble de repositories de features préconfigurées. Les commandes, ci-dessous, permettent respectivement de lister les repositories de features, lister les features, installées ou à disposition pour installation, installer une feature.

1. > feature:repo-list

2. > feature:list

3. > feature:install <nom_feature>

Les bundles sont récupérés depuis Internet et stockés dans le repository local. Les commandes, ci-dessous, permettent respectivement de démarrer/stopper un bundle, lister les bundles installés, de redéployer automatiquement un bundle compilé et installé dans le repository local.

1. > bundle:start

2. > bundle:stop

3. > bundle:list

4. > bundle:watch <bundle_id>

III - Projet OSGi exemple

Le projet exemple ippon-osgi-sample montre comment développer des commandes OSGi. Il s'agit d'une application de type CRUD qui fournit la possibilité de lister les salariés de la société, d'ajouter, de mettre à jour et de supprimer un salarié d'une base de données. Cet exemple s'appuie sur un socle JPA/Hibernate et une base H2. Il est organisé en projet multimodules maven :

1. +ippon-osgi-sample

2. |-ippon-osgi-sample-ds

3. |-ippon-osgi-sample-services

4. |-ippon-osgi-sample-command

5. |-ippon-osgi-sample-kar

6. |-ippon-osgi-sample-ittests

- ippon-osgi-sample-ds contient la datasource vers la base H2, packagée sous forme de bundle pour être déployé simplement.

- ippon-osgi-sample-services contient les services et entités JPA. Ces services sont exposés en tant que services OSGi.

- ippon-osgi-sample-command contient les commandes OSGi qui pourront être invoquées depuis le shell de Karaf.

- ippon-osgi-sample-kar contient principalement le fichier features.xml qui permet de faire du provisionning et de packager notre application avec les bundles précédents pour déploiement dans Karaf.

- ippon-osgi-sample-ittests contient les tests d'intégration d'exécution de nos commandes.

Pour initialiser le projet, on peut utiliser différents archetypes maven. Le premier est un archetype maven destiné à créer des commandes. Le second permet d'initialiser un projet avec Blueprint XML. Le troisième est un archetype maven pour la création de feature, pour le provisionning et le packaging sous forme de fichier kar.

1. $ mvn archetype:generate   -DarchetypeGroupId=org.apache.karaf.archetypes   -DarchetypeArtifactId=karaf-command-archetype   -DarchetypeVersion=4.0.0   -DgroupId=fr.ippon.osgi.sample   -DartifactId=ippon-osgi-sample-command   -Dversion=1.0-SNAPSHOT   -Dpackage=fr.ippon.osgi.sample.command

3. $ mvn archetype:generate -DarchetypeGroupId=org.apache.karaf.archetypes -DarchetypeArtifactId=karaf-blueprint-archetype -DarchetypeVersion=4.0.0  -DgroupId=fr.ippon.osgi.sample   -DartifactId=ippon-osgi-sample-services -Dpackage=fr.ippon.osgi.sample.services -Dversion=1.0-SNAPSHOT

5. $ mvn archetype:generate     -DarchetypeGroupId=org.apache.karaf.archetypes     -DarchetypeArtifactId=karaf-feature-archetype     -DarchetypeVersion=4.0.0     -DgroupId=fr.ippon.osgi.sample     -DartifactId=ippon-osgi-sample-feature     -Dversion=1.0-SNAPSHOT     -Dpackage=fr.ippon.osgi.sample.feature

Une feature définit les différentes ressources via URL (instances, bundles, fichiers de configuration). features.xml recense d'autres features que l'on souhaite installer et activer par défaut au démarrage du serveur. Voici les principaux dans notre cas : jdbc, hibernate, transaction, jpa, etc. Dans ce même fichier peuvent être ajoutés nos propres bundles, par exemple, l'ajout du driver H2 ou des bibliothèques Apache Commons. La commande feature : info hibernate liste l'ensemble des bundles dépendant de cette feature. Karaf peut demander le téléchargement d'artefacts à partir de dépôts distants présents dans sa liste de repositories.

Un kar est donc un package sous forme d'archive zip, qui contient toutes les ressources décrites dans le fichier features XML. Il peut être déployé sans connexion Internet. il est constitué d'un répertoire repository contenant une liste de features XML et l'ensemble des artefacts Maven. Voici les commandes pour installer / désinstaller un kar :

1. karaf@root()> kar:uninstall ippon-osgi-sample-kar-1.0-SNAPSHOT

2. karaf@root()> kar:install file:/D:/Java/workspace-nb/ippon-osgi-sample/ippon-osgi-sample-kar/target/ippon-osgi-sample-kar-1.0-SNAPSHOT.kar

III-A - Service JNDI pour la Datasource

Le projet ippon-osgi-sample-ds produit à la compilation un bundle OSGi qui va enregistrer dans le registre de services notre datasource vers la base H2. Ce service est accessible à partir de son nom JNDI (jdbc/ippon-osgi)

1. <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">

3. <bean id="dataSource" class="org.h2.jdbcx.JdbcDataSource">

4. <property name="URL" value="jdbc:h2:tcp://localhost/~/ippon-osgi;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS ippon"/>

5. <property name="user" value="ippon"/>

6. <property name="password" value="ippon"/>

7. </bean>

9. <service interface="javax.sql.DataSource" ref="dataSource">

10. <service-properties>

11. <entry key="osgi.jndi.service.name" value="jdbc/ippon-osgi"/>

12. </service-properties>

13. </service>

15. </blueprint>

III-B - Services et entités JPA

Le bundle ippon-osgi-sample-services contient les entités JPA et les services d'accès à la base de données. On définit le fichier persistence.xml en précisant bien l'URL vers le service qui expose la datasource.

1. <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

2. <persistence-unit name="ippon-pu" transaction-type="JTA">

3. <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

4. <class>fr.ippon.osgi.sample.model.Employee</class>

5. <exclude-unlisted-classes>true</exclude-unlisted-classes>

6. <jta-data-source>osgi:service/javax.sql.DataSource/(osgi.jndi.service.name=jdbc/ippon-osgi)</jta-data-source>

7. <properties>

8. <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

9. ...

10. </properties>

11. </persistence-unit>

12. </persistence>

Dans le fichier Blueprint XML, JPA est activé. Les beans de services sont déclarés et exposés comme services OSGi, afin de les utiliser dans d'autres bundles.

1. <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://aries.apache.org/xmlns/jpa/v2.0.0" xmlns:tx="http://aries.apache.org/xmlns/transactions/v1.2.0" default-activation="lazy">

3. <jpa:enable />

5. <bean id="employeeBean" class="fr.ippon.osgi.sample.services.EmployeeServiceImpl">

6. <tx:transaction method="*"/>

7. </bean>

8. <service ref="employeeBean" interface="fr.ippon.osgi.sample.services.EmployeeService"/>

10. </blueprint>

Il ne faut pas oublier aussi de préciser au niveau du plugin maven-bundle-plugin le chemin vers le fichier de configuration JPA <Meta-Persistence>META-INF/persistence.xml</Meta-Persistence> et d'exporter les packages contenant les services <Export-Package>fr.ippon.osgi.sample.services*</Export-Package>.

La configuration JPA est classique. Une entityManager est récupérée grâce à l'annotation @PersistenceContext(unitName = "ippon-pu") et l'API Criteria JPA est utilisée pour construire les requêtes. (cf. EmployeeServiceImpl.java.)

III-C - Développement de commandes Karaf

Karaf offre la possibilité d'étendre ses commandes shell de base. Nous allons créer nos propres commandes faisant référence à nos services Blueprint développés précédemment dans le bundle ippon-osgi-sample-services. Une commande est définie par un scope et un nom. Elle peut avoir en paramètre des options ou des arguments ; la complétion peut être activée.

Voici un exemple de commande faisant appel au service qui liste les salariés de la société.

1. @Command(scope = "ippon", name = "list-employees", description = "Liste les employees de la societe")

2. @Service

3. public class ListEmployees implements Action {

5. @Option(name = "-j", aliases = {"--job"}, description = "Liste de jobs", required = false, multiValued = false)

6. @Completion(JobCompleter.class)

7. private String jobsParam;

9. @Reference

10. private EmployeeService employeeService;

12. @Override

13. public Object execute() throws Exception {

14. System.out.println("Liste des employes :");

15. employeeService.getAllEmployees();

16. ...

17. }

18. }

Les annotations @Command @Service servent à déclarer cette classe comme commande Karaf. Cette commande est exposée comme un service OSGi. Il faut également dans le maven-bundle-plugin penser à définir les packages contenant les commandes Karaf <Karaf-Commands>fr.ippon.osgi.sample.command*</Karaf-Commands> dans les instructions.

1. <plugin>

2. <groupId>org.apache.felix</groupId>

3. <artifactId>maven-bundle-plugin</artifactId>

4. <version>2.5.4</version>

5. <extensions>true</extensions>

6. <configuration>

7. <instructions>

8. <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>

9. <Export-Package>

10. fr.ippon.osgi.sample.command*;-noimport:=true

11. </Export-Package>

12. <Karaf-Commands>fr.ippon.osgi.sample.command*</Karaf-Commands>

13. </instructions>

14. </configuration>

15. </plugin>

L'annotation @Option permet de définir des paramètres pour la commande. @Argument permet de définir les arguments. @Completion permet de faire de la complétion et peut être associée à une option ou un argument de commande.

L'annotation @Reference permet de récupérer les services OSGi exposés par le projet ippon-osgi-services. L'usage de ces annotations simplifie grandement la configuration, réalisée auparavant en XML dans les versions antérieures de Karaf (dans le fichier Blueprint XML du bundle). La méthode execute() contient le cœur d'exécution de la commande qui renvoie la liste des salariés.

Retourner tous les salariés de la société : ippon:list-employees

Retourner uniquement les architectes de la société : ippon:list-employees -j ARCHITECT

Retourner uniquement les architectes de la société dont le nom contient 'Employee 3': ippon:list-employees -j ARCHITECT -n 'Employee 3'

D'autres commandes d'ajout et de suppression de salarié sont disponibles sur GitHub ici.

1. > ippon:add-employee DEV 'New Employee' 'New Employee' '01-01-1990'

2. > ippon:remove-employee 2

III-D - Packaging kar

Voici le feature du projet, il contient les features Karaf à activer, les bundles de bibliothèques tierces et les bundles du projet (ippon-osgi-sample-*). Le plugin maven karaf-maven-plugin va générer le kar (cf. pom.xml).

1. <features name="${project.artifactId}-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0">

2. <feature name='${project.artifactId}' description='${project.name}' version='${project.version}'>

6. <details>${project.description}</details>

10. <feature version="4.0.3">jdbc</feature>

11. <feature version="4.3.6.Final">hibernate</feature>

12. <feature version="2.2.0">jpa</feature>

13. <feature version="1.3.0">transaction</feature>

14. <feature version="4.0.3">jndi</feature>

16. <bundle start-level="70">mvn:com.h2database/h2/1.4.190</bundle>

17. <bundle start-level="80">mvn:commons-lang/commons-lang/2.6</bundle>

18. <bundle start-level="80">mvn:commons-logging/commons-logging/1.2</bundle>

19. <bundle start-level="80">mvn:commons-io/commons-io/2.4</bundle>

21. <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-ds/1.0-SNAPSHOT</bundle>

22. <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-services/1.0-SNAPSHOT</bundle>

23. <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-command/1.0-SNAPSHOT</bundle>

24. </feature>

25. </features>

III-E - Tests d'intégration des commandes avec PaxExam

PaxExam est un framework pouvant réaliser des tests d'intégration dans le cadre d'un environnement OSGi. Il est capable de démarrer un Karaf, de déployer des features, des bundles, de surcharger les propriétés de configuration et de lancer de commandes. Différentes stratégies permettent d'utiliser ou non la même configuration pour chaque test unitaire.

Pour l'intégrer au projet Maven ippon-osgi-sample-ittests, il faut ajouter les dépendances suivantes :

1. <dependency>

2. <groupId>org.ops4j.pax.exam</groupId>

3. <artifactId>pax-exam-container-karaf</artifactId>

4. <version>${pax-exam.version}</version>

5. <scope>test</scope>

6. </dependency>

7. <dependency>

8. <groupId>org.ops4j.pax.exam</groupId>

9. <artifactId>pax-exam-junit4</artifactId>

10. <version>${pax-exam.version}</version>

11. <scope>test</scope>

12. </dependency>

13. <dependency>

14. <groupId>org.ops4j.pax.exam</groupId>

15. <artifactId>pax-exam-inject</artifactId>

16. <version>${pax-exam.version}</version>

17. <scope>test</scope>

18. </dependency>

19. <dependency>

20. <groupId>org.apache.geronimo.specs</groupId>

21. <artifactId>geronimo-atinject_1.0_spec</artifactId>

22. <version>1.0</version>

23. <scope>test</scope>

24. </dependency>

25. <!-- Karaf -->

26. <dependency>

27. <groupId>org.apache.karaf</groupId>

28. <artifactId>apache-karaf</artifactId>

29. <version>4.0.3</version>

30. <type>tar.gz</type>

31. <scope>test</scope>

32. <exclusions>

33. <exclusion>

34. <groupId>org.apache.karaf</groupId>

35. <artifactId>org.apache.karaf.client</artifactId>

36. </exclusion>

37. </exclusions>

38. </dependency>

Puis ajouter le plugin suivant, pour activer les fonctionnalités de versionAsInProject() sur les features (voir plus loin).

1. <plugin>

2. <groupId>org.apache.servicemix.tooling</groupId>

3. <artifactId>depends-maven-plugin</artifactId>

4. <version>1.2</version>

5. <executions>

6. <execution>

7. <id>generate-depends-file</id>

8. <goals>

9. <goal>generate-depends-file</goal>

10. </goals>

11. </execution>

12. </executions>

13. </plugin>

La classe de tests d'intégration des commandes Karaf doit posséder les annotations suivantes : @RunWith(PaxExam.class) pour activer PaxExam et @ExamReactorStrategy(PerClass.class) pour activer la configuration une et une seule fois pour tous les tests de cette même classe. La classe étendue KarafTestSupport fournit des fonctions permettant d'exécuter des commandes, vérifier l'installation d'un bundle ou d'une feature. Elle est fortement inspirée de celle des tests d'intégration du projet Karaf lui-même. https://github.com/apache/karaf/blob/master/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java

1. @RunWith(PaxExam.class)

2. @ExamReactorStrategy(PerClass.class)

3. public class IpponOSGIPaxExamTest extends KarafTestSupport {

5. @ProbeBuilder

6. public TestProbeBuilder probeConfiguration(TestProbeBuilder probe) {

7. return probe.setHeader(Constants.DYNAMICIMPORT_PACKAGE, "*,org.apache.felix.service.*;status=provisional");

8. }

10. @Configuration

11. public static Option[] configure() throws Exception {

12. return new Option[]{

13. karafDistributionConfiguration()

14. .frameworkUrl("mvn:org.apache.karaf/apache-karaf/4.0.3/tar.gz")

15. .karafVersion("4.0.3")

16. .useDeployFolder(false)

17. .unpackDirectory(new File("target/paxexam/unpack")),

18. logLevel(LogLevelOption.LogLevel.WARN),

20. // install features

21. features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jdbc"),

22. features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "hibernate"),

23. features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jpa"),

24. features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "transaction"),

25. features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jndi"),

26. features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "pax-jdbc-pool-dbcp2"),

27. features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "aries-annotation"),

29. // Change ssh port

30. editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiRegistryPort", RMI_REG_PORT),

31. editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiServerPort", RMI_SERVER_PORT),

33. keepRuntimeFolder(),

35. // install bundles

36. mavenBundle().groupId("com.h2database").artifactId("h2").version("1.4.190"),

37. mavenBundle().groupId("commons-lang").artifactId("commons-lang").version("2.6"),

38. mavenBundle().groupId("commons-logging").artifactId("commons-logging").version("1.2"),

39. mavenBundle().groupId("commons-io").artifactId("commons-io").version("2.4"),

41. // install bundle datasource h2 for test

42. streamBundle(bundle().add("OSGI-INF/blueprint/datasource-h2-test.xml",

43. new File("src/test/resources/OSGI-INF/blueprint/datasource-h2-test.xml").toURL())

44. .set(Constants.BUNDLE_NAME, "Apache Karaf :: Ippon OSGI Datasource Test")

45. .set(Constants.BUNDLE_SYMBOLICNAME, "ippon-osgi-sample-ds")

46. .set("Bundle-ManifestVersion", "2")

47. .set(Constants.DYNAMICIMPORT_PACKAGE, "*").build()).start(),

49. // install ippon bundles

50. mavenBundle().groupId("fr.ippon.osgi.sample").artifactId("ippon-osgi-sample-services").version("1.0-SNAPSHOT"),

51. mavenBundle().groupId("fr.ippon.osgi.sample").artifactId("ippon-osgi-sample-command").version("1.0-SNAPSHOT"),

53. };

54. }

56. ...

57. }

La configuration dans la méthode probeConfiguration() autorise l'import dynamique de tous les packages de type provisional dans le features.xml du projet.

La configuration de la méthode configure() définit la distribution Karaf karafDistributionConfiguration() à déployer lors des lancements des tests, les features features() et bundles mavenBundle() à installer. streamBundle() permet de créer un bundle à la volée à partir d'une autre datasource de test datasource-h2-test.xml. editConfigurationFilePut() permet de surcharger les fichiers de configuration du serveur.

Il est alors possible de tester l'installation des features et bundles.

1. @Test

2. public void testProvisioning() throws Exception {

3. // Check that the features are installed

4. assertFeatureInstalled("jdbc", "4.0.3");

5. assertFeatureInstalled("hibernate", "4.3.6.Final");

6. assertFeatureInstalled("jpa", "2.2.0");

8. // Check that the bundles are installed

9. assertBundleInstalled("ippon-osgi-sample-services");

10. assertBundleInstalled("ippon-osgi-sample-command");

11. }

De tester l'exécution de nos commandes Karaf. Exemple :

1. @Test

2. public void testListEmployeesWithOptionsCommand() {

3. Assert.assertNotNull(bundleContext);

5. String result = executeCommand("ippon:list-employees -j ARCHITECT -n 'Employee 3'");

6. System.out.println("result : " + result);

7. Assert.assertNotNull(result);

8. }

IV - Conclusion

Nous avons vu comment écrire nos propres commandes Karaf, comment les tester avec PaxExam. J'espère que ce tutoriel peut constituer une première approche pour qui souhaite débuter avec les concepts d'OSGi.

Le code source de l'application est disponible sur GitHub : https://github.com/sfoubert/ippon-osgi-sample

V - Remerciements

Cet article a été publié avec l'aimable autorisation de Ippon technologies.

Nous tenons à remercier Claude Leloup pour sa relecture orthographique et Mikcael Baron pour la mise au gabarit.