Tester ses commandes OSGi avec Karaf et PaxExam

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

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 rend possible une mise à jour de versions applicatives sans coupure de service.

Présentation de Karaf

karaf-logoKaraf 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-prompt

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.

<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
    ...
</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.

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



        <feature version="4.0.3">jdbc</feature>
        <feature version="4.3.6.Final">hibernate</feature>
        <feature version="2.2.0">jpa</feature>
        ...
        <bundle start-level="70">mvn:com.h2database/h2/1.4.190</bundle>
        ...
    </feature>
</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.

> feature:repo-list
> feature:list  
> 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.

> bundle:start 
> bundle:stop 
> bundle:list
> bundle:watch <bundle_id>

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 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 multi-modules maven :

+ippon-osgi-sample
 |-ippon-osgi-sample-ds
 |-ippon-osgi-sample-services
 |-ippon-osgi-sample-command
 |-ippon-osgi-sample-kar
 |-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écedents pour déploiement dans Karaf.
  • ippon-osgi-sample-ittests contient les tests d’intégration d’exécution de nos commandes.

MindmapPour 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.

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

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

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 URLs (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 ajouter nos propres bundles, comme par exemple, l’ajout du driver H2 ou des librairies 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’artefact à partir de dépôts distants présent 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 :

karaf@root()> kar:uninstall ippon-osgi-sample-kar-1.0-SNAPSHOT
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

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)

<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
 
  <bean id="dataSource" class="org.h2.jdbcx.JdbcDataSource">
    <property name="URL" value="jdbc:h2:tcp://localhost/~/ippon-osgi;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS ippon"/>
    <property name="user" value="ippon"/>
    <property name="password" value="ippon"/>
  </bean>

  <service interface="javax.sql.DataSource" ref="dataSource">
    <service-properties>
       <entry key="osgi.jndi.service.name" value="jdbc/ippon-osgi"/>
    </service-properties>
  </service>

</blueprint>

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.

<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">
  <persistence-unit name="ippon-pu" transaction-type="JTA">
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <class>fr.ippon.osgi.sample.model.Employee</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
    <jta-data-source>osgi:service/javax.sql.DataSource/(osgi.jndi.service.name=jdbc/ippon-osgi)</jta-data-source>
    <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
      ...
    </properties>
  </persistence-unit>
</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.

<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">

    <jpa:enable />
    
    <bean id="employeeBean" class="fr.ippon.osgi.sample.services.EmployeeServiceImpl">
        <tx:transaction method="*"/>
    </bean>
    <service ref="employeeBean" interface="fr.ippon.osgi.sample.services.EmployeeService"/>

</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-INF/persistence.xml et d’exporter les packages contenant les services fr.ippon.osgi.sample.services.*

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)

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é.

@Command(scope = "ippon", name = "list-employees", description = "Liste les employees de la societe")
@Service
public class ListEmployees implements Action {

    @Option(name = "-j", aliases = {"--job"}, description = "Liste de jobs", required = false, multiValued = false)
    @Completion(JobCompleter.class)
    private String jobsParam;

    @Reference
    private EmployeeService employeeService;

    @Override
    public Object execute() throws Exception {
            System.out.println("Liste des employes :");
            employeeService.getAllEmployees();
                ...
            }
}

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 fr.ippon.osgi.sample.command* dans les instructions.

<plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.5.4</version>
                <extensions>true</extensions>
                <configuration>
                    <instructions>
                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
                        <Export-Package>
                            fr.ippon.osgi.sample.command*;-noimport:=true
                        </Export-Package>
                        <Karaf-Commands>fr.ippon.osgi.sample.command*</Karaf-Commands>
                    </instructions>
                </configuration>
            </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

list-employees-1

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

list-employees-2

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

list-employees-3

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

> ippon:add-employee DEV 'New Employee' 'New Employee' '01-01-1990'
> ippon:remove-employee 2

Packaging kar

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

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

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



        <feature version="4.0.3">jdbc</feature>
        <feature version="4.3.6.Final">hibernate</feature>
        <feature version="2.2.0">jpa</feature>
        <feature version="1.3.0">transaction</feature>
        <feature version="4.0.3">jndi</feature>
        
        <bundle start-level="70">mvn:com.h2database/h2/1.4.190</bundle>
        <bundle start-level="80">mvn:commons-lang/commons-lang/2.6</bundle>
        <bundle start-level="80">mvn:commons-logging/commons-logging/1.2</bundle>
        <bundle start-level="80">mvn:commons-io/commons-io/2.4</bundle>
        
        <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-ds/1.0-SNAPSHOT</bundle>
        <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-services/1.0-SNAPSHOT</bundle>
        <bundle start-level="80">mvn:fr.ippon.osgi.sample/ippon-osgi-sample-command/1.0-SNAPSHOT</bundle>
    </feature>
</features>

Tests d’intégration des commandes avec PaxExam

paxexam-logoPaxExam 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 tests unitaires.

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

<dependency>
            <groupId>org.ops4j.pax.exam</groupId>
            <artifactId>pax-exam-container-karaf</artifactId>
            <version>${pax-exam.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.ops4j.pax.exam</groupId>
            <artifactId>pax-exam-junit4</artifactId>
            <version>${pax-exam.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.ops4j.pax.exam</groupId>
            <artifactId>pax-exam-inject</artifactId>
            <version>${pax-exam.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.geronimo.specs</groupId>
            <artifactId>geronimo-atinject_1.0_spec</artifactId>
            <version>1.0</version>
            <scope>test</scope>
        </dependency>
        <!-- Karaf -->
        <dependency>
            <groupId>org.apache.karaf</groupId>
            <artifactId>apache-karaf</artifactId>
            <version>4.0.3</version>
            <type>tar.gz</type>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.karaf</groupId>
                    <artifactId>org.apache.karaf.client</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

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

<plugin>
                <groupId>org.apache.servicemix.tooling</groupId>
                <artifactId>depends-maven-plugin</artifactId>
                <version>1.2</version>
                <executions>
                    <execution>
                        <id>generate-depends-file</id>
                        <goals>
                            <goal>generate-depends-file</goal>
                        </goals>
                    </execution>
                </executions>
            </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

@RunWith(PaxExam.class)
@ExamReactorStrategy(PerClass.class)
public class IpponOSGIPaxExamTest extends KarafTestSupport {

    @ProbeBuilder
    public TestProbeBuilder probeConfiguration(TestProbeBuilder probe) {
        return probe.setHeader(Constants.DYNAMICIMPORT_PACKAGE, "*,org.apache.felix.service.*;status=provisional");
    }

    @Configuration
    public static Option[] configure() throws Exception {
        return new Option[]{
            karafDistributionConfiguration()
            .frameworkUrl("mvn:org.apache.karaf/apache-karaf/4.0.3/tar.gz")
            .karafVersion("4.0.3")
            .useDeployFolder(false)
            .unpackDirectory(new File("target/paxexam/unpack")),
            logLevel(LogLevelOption.LogLevel.WARN),

            // install features

            features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jdbc"),
            features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "hibernate"),
            features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jpa"),
            features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "transaction"),
            features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "jndi"),
            features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "pax-jdbc-pool-dbcp2"),
            features(maven().groupId("org.apache.karaf.features").artifactId("standard").type("xml").classifier("features").versionAsInProject(), "aries-annotation"),

            // Change ssh port

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

            keepRuntimeFolder(),

            // install bundles

            mavenBundle().groupId("com.h2database").artifactId("h2").version("1.4.190"),
            mavenBundle().groupId("commons-lang").artifactId("commons-lang").version("2.6"),
            mavenBundle().groupId("commons-logging").artifactId("commons-logging").version("1.2"),
            mavenBundle().groupId("commons-io").artifactId("commons-io").version("2.4"),

            // install bundle datasource h2 for test

            streamBundle(bundle().add("OSGI-INF/blueprint/datasource-h2-test.xml",
                    new File("src/test/resources/OSGI-INF/blueprint/datasource-h2-test.xml").toURL())
                    .set(Constants.BUNDLE_NAME, "Apache Karaf :: Ippon OSGI Datasource Test")
                    .set(Constants.BUNDLE_SYMBOLICNAME, "ippon-osgi-sample-ds")
                    .set("Bundle-ManifestVersion", "2")
                    .set(Constants.DYNAMICIMPORT_PACKAGE, "*").build()).start(),

            // install ippon bundles

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

        };
    }
	
	...
}

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.

@Test
    public void testProvisioning() throws Exception {
        // Check that the features are installed

        assertFeatureInstalled("jdbc", "4.0.3");
        assertFeatureInstalled("hibernate", "4.3.6.Final");
        assertFeatureInstalled("jpa", "2.2.0");

        // Check that the bundles are installed

        assertBundleInstalled("ippon-osgi-sample-services");
        assertBundleInstalled("ippon-osgi-sample-command");
    }

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

@Test
    public void testListEmployeesWithOptionsCommand() {
        Assert.assertNotNull(bundleContext);

        String result = executeCommand("ippon:list-employees -j ARCHITECT -n 'Employee 3'");
        System.out.println("result : " + result);
        Assert.assertNotNull(result);
    }

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

Références

https://www.osgi.org/developer/specifications/

https://ops4j1.jira.com/wiki/display/PAXEXAM4 https://ops4j1.jira.com/wiki/display/PAXEXAM4/Karaf+Test+Container+Reference

https://karaf.apache.org/manual/latest/developers-guide/extending.html
http://iocanel.blogspot.fr/2012/01/advanced-integration-testing-with-pax.html

https://www.packtpub.com/application-development/apache-karaf-cookbook https://github.com/jgoodyear/ApacheKarafCookbook