OSGi, Java e la programmazione 'a bundle'

III parte: Un esempio concretodi

Nei precedenti articoli della serie sono state dapprima presentate le caratteristiche e le politiche di funzionamento delle specifiche OSGi per poi analizzare le presenti implementazioni open-source di alcuni framework su di esse basati. In questo articolo verranno presentati gli strumenti per la programmazione a bundle e sarà implementato il classico esempio HelloWorld, seguito da interessanti evoluzioni dello stesso per presentare le caratteristiche dei framework OSGi.

Strumenti di sviluppo

Nel panorama Java, l'ambiente di sviluppo più considerato e utilizzato dagli sviluppatori è sicuramente Eclipse [1]. Tra tutti i plug-in che esso offre, non mancano le integrazioni per la programmazione modulare offerta da OSGi: in questo caso ancora di più essendo Eclipse un perfetto esempio di programmazione in stile OSGi in quanto i cosiddetti plug-in non sono altro che bundle in stile OSGi. Eclipse offre a tal proposito degli strumenti adatti allo sviluppo di bundle, primo tra tutti il Plug-in Development Environment chiamato anche PDE. In questa sede però non verrà utilizzato in modo da non legare lo sviluppatore all'ambiente di sviluppo. Al suo posto verrà utilizzato un altro strumento molto utile: Bnd [2]. Sviluppato da Peter Kriens, Bnd è uno strumento a linea di comando adatto alla generazione automatica di bundle. Negli ultimi tempi è stato anche integrato in Eclipse stesso ed è disponibile come estensione di Maven [3].

Come framework è stata scelta l'implementazione di Apache: Felix [4]. La scelta è stata dettata dalla sua compattezza (poche centinaia di kb) e dal fatto che l'implementazione è molto fedele alle specifiche: in questo caso lo sviluppatore può stare sicuro di poter installare i bundle sviluppati per Apache Felix anche in qualsiasi altro framework OSGi.

Installazione del framework e preparazione dell'ambiente

 

L'installazione del framework è molto semplice: è sufficiente scompattare il file reperibile facilmente dal sito di Apache Foudation. Dopo averlo scaricato e scompattato, basta posizionarsi all'interno della directory in cui si è decompresso il file scaricato precedentemente e lanciare il comando Java:

java -jar bin/felix.jar

In questo modo si avvia il framework e si entra all'interno della sua semplice console:

Welcome to Felix
================
->

A questo punto il framework è pronto per ricevere i primi comandi.

Avendo scelto Eclipse, è possibile integrare il framework all'interno di un progetto Java nell'ambiente Eclipse stesso.

Per fare ciò, ed essere quindi più veloci nello sviluppo, basta creare un progetto Java all'interno di Eclipse e includere i file nel classpath di progetto.

Il framework è così integrato nell'ambiente e per utilizzarlo, basta creare un'esecuzione del progetto, includendo la classe main del framework come eseguibile.

Come detto sopra, anche bnd è integrabile all'interno di Eclipse.

Per far ciò, si deve scaricare il file corrispondente, copiarlo nella directory plugin di Eclipse e riavviare l'ambiente.

Tutto è pronto per sviluppare il primo bundle OSGi.

HelloWorld

Ormai da anni, il modo più semplice per capire come un nuovo artefatto informatico funzioni, è quello di partire dall'ormai mitico esempio HelloWorld.

In questo caso bisogna creare un bundle OSGi, cioè una classe che implementi l'interfaccia BundleActivator:

package it.imolinfo.osgi.helloworld;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class HelloWorldActivator implements BundleActivator {       
public void start(BundleContext context) throws Exception {
             System.out.println(" Hello , World ! ");
       }       
       public void stop(BundleContext context) throws Exception {
             System.out.println(" Goodbye , World ! ");
       }
}

Questa sarà la classe Java da compilare. Come visto negli articoli precedenti, un bundle OSGi ha bisogno di un file manifest che presenti tutte le informazioni per far sì che il bundle funzioni correttamente. Il file è facilmente configurabile a mano ma in questa sede si utilizzerà il tool Bnd presentato prima, il quale espleta questa funzione egregiamente. Bnd non solo genera automaticamente il file manifest, ma impacchetta già il bundle pronto per essere distribuito in un qualsiasi framework OSGi.

Bnd ha bisogno di un file di configurazione nel quale si specificano informazioni generali riguardanti il bundle da creare. In questo caso bisognerà specificare il package di riferimento e la classe activator del bundle stesso:

#HelloWorld.bnd
Private-Package: it.imolinfo.osgi.helloworld
Bundle-Activator: it.imolinfo.osgi.helloworld.HelloWorldActivator

Se Bnd è stato installato correttamente in Eclipse, basterà cliccare con il tasto destro sul file Bnd e cliccare successivamente sulla voce di menù Make Bundle.

A questo punto il file HelloWorld.jar generato sarà il bundle da installare successivamente sul framework.

Se non si vuole usare l'integrazione con Eclipse, basta prima compilare la classe Java e successivamente eseguire il comando:

java −jar bnd.jar build −classpath classes HelloWorld.bnd

dove classes sarà il path delle classi compilate.

Volendo esplorare il file HelloWorld.jar si può notare come Bnd abbia generato correttamente il file MANIFEST.MF sotto la cartella META-INF e di come questo sia completo di tutte le informazioni necessarie per il giusto funzionamento del bundle:

Manifest-Version: 1.0
Private-Package: it.imolinfo.osgi.helloworld
Bundle-Version: 0
Tool: Bnd-0.0.384
Bnd-LastModified: 1277934552377
Bundle-Name: HelloWorld
Bundle-ManifestVersion: 2
Created-By: 1.6.0_20 (Sun Microsystems Inc.)
Bundle-Activator: it.imolinfo.osgi.helloworld.HelloWorldActivator
Import-Package: org.osgi.framework;version="1.5"
Bundle-SymbolicName: HelloWorld

Il file presenta delle informazioni di vitale importanza per il corretto funzionamento del bundle.

Tra queste si può individuare l'esplicitazione della classe Activator con il quale il bundle farà il suo start-up, l'importazione dei package necessari, il nome del bundle e la sua versione.

Il bundle è pronto per essere installato. Dopo aver avviato il framework si avrà la situazione seguente:

Welcome to Felix
================
->

Digitando il comando:

-> install file:HelloWorld.jar

il bundle è stato correttamente installato nel framework. Per vedere la sua reale presenza all'interno di Felix bisogna digitare:

> ps
START LEVEL 1
   ID   State         Level Name
[   0] [Active     ] [    0] System Bundle (2.0.1)
[   1] [Active     ] [    1] Apache Felix Bundle Repository (1.4.2)
[   2] [Active     ] [    1] Apache Felix Shell Service (1.4.1)
[   3] [Active     ] [    1] Apache Felix Shell TUI (1.4.1)
[   4] [Installed ] [    1] HelloWorld (0)
>

Si nota come il bundle è stato installato ma non è stato ancora attivato. Per farlo, basta digitare:

-> start 4
 Hello , World !

Il risultato sarà sicuramente quello atteso e cioè la scritta "Hello , World !" sulla console del framework.

Volendo disattivare il bundle basta digitare:

-> stop 4
  Goodbye , World !

e il bunlde sarà disattivato con la scritta "Goodbye , World !"

Da notare come i comandi start e stop reagiscono al numero ID del bundle dopo che è stato installato.

Oltre che avviati (start) e fermati (stop), i bundle possono essere aggiornati. Si presuppone di cambiare l'implementazione della classe HelloWorldActivator cambiando il messaggio di benvenuto:

package it.imolinfo.osgi.helloworld;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class HelloWorldActivator implements BundleActivator {
       public void start(BundleContext context) throws Exception {
             System.out.println("Ciao, Mondo!");
       }
       public void stop(BundleContext context) throws Exception {
             System.out.println("Arrivederci, Mondo!");
       }
}

Dopo aver modificato l'implementazione, bisogna ovviamente ricompilare e re-impacchettare il bundle. Non è più però necessario arrestare il bundle per poi riavviarlo. Si può, infatti, sfruttare una delle proprietà peculiari dei framework OSGi, e cioè quella di aggiornare i bundle on-the-fly quindi senza riavviare il framework stesso.

Per farlo bisogna, dapprima conoscere l'ID del bundle da aggiornare:

> ps
START LEVEL 1   ID   State         Level Name
[   0] [Active     ] [    0] System Bundle (2.0.1)
[   1] [Active     ] [    1] Apache Felix Bundle Repository (1.4.2)
[   2] [Active     ] [    1] Apache Felix Shell Service (1.4.1)
[   3] [Active     ] [    1] Apache Felix Shell TUI (1.4.1)
[   4] [Active     ] [    1] HelloWorld (0)
>

e poi lanciare il comando:

>update 4
Goodbye , World !
Ciao, Mondo!

Come si nota, il bundle è stato automaticamente aggiornato senza alcun intervento a livello framework.

Utilizzo dei servizi

La piattaforma OSGi offre la possibilità di pensare all'implementazione di una funzionalità come servizio. I servizi in OSGi sono delle semplici classi Java che implementano una o più interfacce come qualsiasi sviluppatore Java è abituato a fare. Il livello che aggiunge OSGi è quello di registrare queste funzionalità, espresse da classi Java, come servizi e offrirli al framework stesso e a tutti i bundle che vogliono utilizzarlo.

Per vedere in pratica come funziona il meccanismo, bisogna sporcarsi di più le mani e implementare un bundle HelloWorld un pò più evoluto.

Si pensi alla possibilità della formattazione del messaggio di benvenuto, espressa come servizio.

Per far ciò basta definire un'interfaccia:

package it.imolinfo.osgi.services;
public interface Printer {
       public String print();
}

Come si può notare, è una semplice interfaccia che definisce il metodo print().

Si procede anche all'implementazione dell'interfaccia stessa:

package it.imolinfo.osgi.services;
public class CiaoPrinterService implements Printer {
       @Override
       public String print() {
       return "Ciao Mondo";
       }
}

L'implementazione dell'interfaccia è un metodo che restituisce una semplice stringa.

Si procede adesso introducendo il concetto di servizio nella terminologia OSGi. Per far sì che un servizio sia usufruibile dal framework, lo si deve registrare. Sarà un bundle a occuparsi della registrazione:

package it.imolinfo.osgi.services;
import java.util.Hashtable;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;
public class CiaoActivator implements BundleActivator {
       public static BundleContext bc = null;
       @Override
       public void start(BundleContext bc) throws Exception {
             System.out.println(bc.getBundle().getHeaders().get(
                           Constants.BUNDLE_NAME)
                           + " starting...");
             CiaoActivator.bc = bc;
             Printer service = new CiaoPrinterService();
             ServiceRegistration registration = bc.registerService(
                           CiaoPrinterService.class.getName(),
                           service, new Hashtable());
             System.out.println("Service registered: "
                                  + registration.getReference());
       }
       @Override
       public void stop(BundleContext bc) throws Exception {
             System.out.println(bc.getBundle().getHeaders().get(Constants.BUNDLE_NAME)
                           + " stopping...");
             CiaoActivator.bc = null;
       }
}

Il bundle, attraverso il metodo registerService, si preoccuperà di registrare il servizio, corredato di informazioni opzionali contenute in una hashtable (nel nostro caso vuota). Il tutto viene implementato nel metodo start del bundle. Il metodo stop non farà altro che assegnare a null la variabile globale corrispondente al BundleContext. Questo è sufficiente alla de-registrazione del servizio.

Fin qua il tutto risulta abbastanza semplice e lineare.

Per far vedere il funzionamento a runtime della gestione dei servizi, si è pensato a complicare l'esempio HelloWorld mettendo in gioco un Thread Java che non fa altro che stampare, a intervalli regolari, la scritta fornita dal servizio appena registrato:

package it.imolinfo.osgi.services;
class ServiceUser extends Thread {
       private Printer service = null;
       private boolean running = true;
       public ServiceUser(Printer service) {
             this.service = service;
       }
       public void run() {
             String saluta = null;
             while (this.running) {
                    try {
                           saluta = this.service.print();
                    } catch (RuntimeException e) {
                           System.out.println("RuntimeException: " + e);
                    }
                    System.out.println("Messaggio: " + saluta);
                    try {
                           Thread.sleep(5000);
                    } catch (InterruptedException e) {
                           System.out.println("ERROR: " + e);
                    }
             }
       }
       public void stopThread() {
             this.running = false;
       }
}

Questa classe sarà usata direttamente da un ServiceTrackerCustomizer all'interno del framework OSGi:

package it.imolinfo.osgi.services;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
public class PrinterServiceTrackerCustomizer implements
             ServiceTrackerCustomizer {
       private ServiceUser thread = null;
       private BundleContext bc;
       public PrinterServiceTrackerCustomizer(BundleContext bc) {
             this.bc = bc;
       }
       public Object addingService(ServiceReference reference) {
             Printer service = (Printer) bc.getService(reference);
             if (this.thread == null) {
                    this.thread = new ServiceUser(service);
                    this.thread.start();
                    return service;
             } else
                    return service;
       }
       public void modifiedService(ServiceReference reference, Object serviceObject) {
             this.thread.stopThread();
             try {
                    this.thread.join();
             } catch (InterruptedException e) {
                    e.printStackTrace();
             }
             Printer service = (Printer) bc.getService(reference);
             this.thread = new ServiceUser(service);
             this.thread.start();
       }
       public void removedService(ServiceReference reference, Object serviceObject) {
             this.thread.stopThread();
             try {
                    this.thread.join();
             } catch (InterruptedException e) {
                    e.printStackTrace();
             }
             this.thread = null;
       }
}

Quest'ultima classe avrà la responsabilità di reagire agli eventi scatenati dalla registrazione/de-registrazione del servizio. Come si nota, per ogni evento facente parte del ciclo di vita di un servizio all'interno del framework, il ServiceTrackerCustomizer reagisce di conseguenza.

Infine, ci sarà un bundle utilizzatore del servizio stesso:

package it.imolinfo.osgi;
import it.imolinfo.osgi.services.CiaoActivator;
import it.imolinfo.osgi.services.CiaoPrinterService;
import it.imolinfo.osgi.services.PrinterServiceTrackerCustomizer;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
public class HelloWorldActivator2 implements BundleActivator {
       boolean avviato = false;
       ServiceTracker tracker;
       public void start(BundleContext bc) throws Exception {
             System.out.println(bc.getBundle().getHeaders().get(
                           Constants.BUNDLE_NAME)
                           + " starting...");
             CiaoActivator.bc = bc;
             ServiceTrackerCustomizer customizer
                    = new PrinterServiceTrackerCustomizer(bc);
             tracker = new ServiceTracker(bc, CiaoPrinterService.class.getName(),
                           customizer);
             tracker.open();
       }
       public void stop(BundleContext bc) throws Exception {
             System.out.println(bc.getBundle().getHeaders().get(
                           Constants.BUNDLE_NAME)
                           + " stopping...");
             bc = null;
             tracker.close();
       }
}

Attraverso il metodo start(), il bundle si mette in ascolto, mediante il PrinterServiceTrackerCustomizer, della presenza del servizio CiaoPrinterService nel sistema. A questo punto il framework si occuperà del resto affinche' il bundle utilizzatore possa far uso del servizio nella maniera opportuna.

Per vedere il tutto all'opera bisognerà anche creare i file Bnd per la generazione dei bundle CiaoActivator responsabile della registrazione del servizio e HelloActivator2 utilizzatore del servizio:

#CiaoActivator.bnd
Private-Package: it.imolinfo.osgi.services
Bundle-Activator: it.imolinfo.osgi.services.CiaoActivator
Export-Package: it.imolinfo.osgi.services
#HalloActivator.bnd
Private-Package: it.imolinfo.osgi
Bundle-Activator: it.imolinfo.osgi.HelloWorldActivator2
Export-Package: it.imolinfo.osgi

Creati i due bundle, bastarà installarli nel framework ed attivarli:

-> install file:HelloActivator2.jar
Bundle ID: 4
-> install file:CiaoActivator.jar
Bundle ID: 5

Ai due comandi, seguirà la comunicazione da parte del framework degli identificativi (ID) dei due bundle appena installati.

Non rimane che attivare il bundle utilizzatore

-> start 4
HelloActivator2 starting...

Il bundle è attivato, ma non si vede traccia dei messaggi stampati a video del servizio CiaoPrinterService utilizzato dal bundle. Questo perche' il servizio non è ancora disponibile, in quanto non è stato ancora attivato il bundle responsabile della registrazione del servizio.

Quello che ci si deve aspettare lanciando il comando di avvio del bundle CiaoActivator è appunto il fatto che il Thread ServiceUser venga attivato e che quindi verrà stampato a video ciclicamente il messaggio fornito dal servizio registrato e cioè Ciao Mondo:

-> start 5
CiaoActivator starting...
Service registered: [it.imolinfo.osgi.services.CiaoPrinterService]
-> ServiceUserThread: Messaggio: Ciao Mondo
ServiceUserThread: Messaggio: Ciao Mondo

Il servizio viene registrato, il PrinterServiceTrackerCustomizer si prenderà carico di comunicare al bundle HelloActivator2 l'avvenuta registrazione e di attivare il Thread.

Adesso, deregistrando il servizio, e quindi arrestando il bundle CiaoActivator, si vedrà come le stampe fornite dal servizio non saranno più disponibili a video:

>stop 5
CiaoActivator stopping...

Ecco come il cerchio è stato chiuso, riguardo la registrazione e all'utilizzo di un semplice servizio in un framework OSGi.

Il tutto è estremamente dinamico, e non si è agito mai direttamente sul framework, ma sui bundle che si sono implementati e installati su di esso (la differenza non è trascurabile).

La potenzialità di tutto ciò sta nel fatto che un servizio, nella realtà, può andare e venire inibendo così anche il funzionamento di altre funzionalità di un programma. Il framework si prende carico della gestione di tutto ciò che concerne il ciclo di vita dei servizi, lasciando allo sviluppatore la libertà di preoccuparsi solo ed esclusivamente dello sviluppo delle specifiche a lui assegnate.

Conclusioni

In questo articolo conclusivo si è esplorata da vicino la programmazione a bundle suggerita dai framework OSGi. Attraverso un semplice esempio si è introdotto il concetto di bundle insieme all'utilizzo di strumenti, come Bnd, che facilitano lo sviluppo in ambito OSGi. L'esempio è poi stato portato avanti introducendo il concetto di servizio e di suo utilizzo, mostrando la potenzialità del framework sotto questo aspetto e dimostrando come il framework stesso si presti alla programmazione orientata ai servizi. Saranno le specifiche OSGi infatti a preoccuparsi della registrazione e manutenzione dei servizi all'interno del framework evitando allo sviluppatore la gestione del ciclo di vita degli stessi in relazione ai bundle che li utilizzano.

Riferimenti

[1] Eclipse

[2] Bnd

[3] Apache Maven

[4] Apache Felix

 

 

Condividi

Pubblicato nel numero
153 luglio 2010
Emanuele Barrano si laurea all'Alma Mater Studiorum di Bologna con una tesi sul Cloud Computing presso l'IBM Innovation Center di Dublino. Dal 2009 lavora per Imola Informatica intraprendendo un percorso che lo ha avvicinato alle Architetture IT, argomento sul quale oggi fornisce le sue competenze in attività di consulenza.
Articoli nella stessa serie
Ti potrebbe interessare anche