MokaByte 100 - 8bre 2005
 
MokaByte 100 - 8bre 2005 Prima pagina Cerca Home Page

 

 

 

Mjpf (micro Java Plugin Framework)

mjpf è un framework OpenSource che fornisce un insieme minimale di strumenti per la realizzazione di applicazioni Java estensibili attraverso un'architettura basata su plugin.

Introduzione
Il valore aggiunto che risiede nella possibilità di caricare dinamicamente del codice e, in questo modo, di estendere le funzionalità di un sistema a tempo di esecuzione, penso sia ben evidente a tutti quelli che hanno utilizzato applicazioni basate su plugin. I benefici più significativi, dal lato di chi le sviluppa, vanno dall'ovvia possibilità di aggiungere funzionalità ad un sistema esistente senza dover intervenire sul codice già scritto, alla possibilità di suddividere i compiti tra i componenti di un team di sviluppo in modo più semplice ed intuitivo. L' esempio per eccellenza viene sicuramente da Eclipse[1] un progetto OpenSource molto noto che ha lo scopo di fornire una piattaforma estensibile per la realizzazione di software. Allo stesso modo di Eclipse, potrebbe essere interessante pensare di realizzare le proprie applicazioni in modo tale che siano facilmente estensibili in un secondo momento. Ma come fare a realizzare autonomamente un'applicazione che sia basata su un simile meccanismo?

 

mjpf
mjpf [mjpf] è la risposta che Io insieme ad Aucom [Aucom] abbiamo cercato di dare a questa domanda.
mjpf è un framework che consente di realizzare applicazioni che, a tempo di esecuzione, caricano ed eseguono del codice in modo trasparente ad un utente. Ad essere sinceri dobbiamo dire che esistono altri framework che hanno lo stesso obiettivo, uno dei più noti è JPF (Java Plugin Framework)[2]. Tuttavia, per la nostra esperienza e a nostro parere, questi framework presentano architetture forse troppo rigide e complesse che sono si ricche di funzionalità ma che, allo stesso tempo, possono risultare ostiche da comprendere ed utilizzare.
mjpf nasce con l'idea di definire un insieme di entità e funzioni primitive utili all'estensione dinamica di un'applicazione Java (il prefisso micro ne evidenzia non a caso l'essenzialità). Nell'insieme mjpf è veramente molto leggero, allo stato attuale è composto unicamente da 2 classi e un'interfaccia, ma tanto basta per realizzare quello che serve. Provare mjpf e leggerne il codice (che è molto semplice ed accessibile) può essere interessante sia per chi decidesse di rendere la propria applicazione basata su plugin, sia per chi volesse imparare delle tecniche di programmazione non sempre comuni come l'uso di ClassLoader personalizzati.
Per necessità di spazio dobbiamo assumere che il lettore conosca, anche se solo superficialmente, le problematiche e le tecniche relative al caricamento e all'esecuzione dinamica di codice. Per chi ritenesse di dover approfondire simili conoscenze consigliamo di andarsi a rileggere gli articoli [3]

 

Per prima cosa una panoramica generale
Definiamo "applicazione di base" il cuore dell'applicazione che si intende sviluppare (o estendere) utilizzando mjpf. Questa è tipicamente costituita da un insieme di classi/oggetti che definiamo "oggetti fondamentali" e le cui funzionalità sono, eventualmente, accessibili attraverso una GUI.
Utilizzando mjpf un'applicazione di base può estendere le sue funzionalità ricercando ed eseguendo, a runtime, dei Plugin contenuti in un path specificato dopo averli inizializzati con i suoi oggetti fondamentali.


Figura 1 - Possibile architettura di un'applicazione basata su mjpf

In mjpf, un plugin è un archivio Java (un file jar) in cui sono presenti alcune classi che implementano l'interfaccia PluginEntry. Questa interfaccia è molto semplice e presenta i seguenti metodi:

public void initPluginEntry(Object param)
public void startPluginEntry()
public void pausePluginEntry()
public void stopPluginEntry()

Le classi che implementano l'interfaccia PluginEntry costituiscono quelli che vengono definiti gli entry-point del Plugin; ovvero degli oggetti attraverso i quali è possibile interagire con gli altri oggetti interni al plugin, al fine di ottenere i servizi offerti.

L'idea, presa (non a caso) dalle Applet (o dalle Xlet, Midlet...), è che i metodi forniti dall'interfaccia PluginEntry dovrebbero essere sufficienti alla gestione di un componente software caricato a tempo di esecuzione.
Il problema nasce dal fatto che il codice relativo ai plugin non è noto a priori ma è caricato a runtime per mezzo di un ClassLoader personalizzato [3]. Per questo motivo è necessario fare riferimento ad un'interfaccia, questa si nota a priori, per l'esecuzione di pochi fondamentali metodi che consentano di inizializzare ed eseguire il codice caricato.
All'interno di ogni Plugin deve essere presente un file xml in cui sono descritti i suoi entry-point.
Un oggetto del package mjpf chiamato PluginFactory permetterà quindi di caricare ed ottenere a runtime dei riferimenti ai vari entry-point dei Plugin contenuti in una directory specificata.

 

Anatomia di un Plugin
Andiamo più in dettaglio a vedere come creare plugin adatti ad essere caricati attraverso mjpf. A costo di risultare fastidiosamente ripetitivi ribadiamo che, in questo contesto, intendiamo con Plugin un jar contenente diverse classi ed almeno un entry-point insieme ad un descrittore xml dei suoi entry-point.
Visto che un entry-point è una classe che implementa l'interfaccia PluginEntry, deve necessariamente (e del resto noi contiamo su questo..) implementare tutti i suoi metodi: initPluginEntry, startPluginEntry, pausePluginEntry e stopPluginEntry.


Figura 2 - L'interfaccia PluginEntry

initPluginEntry(Object param)
Questo metodo deve essere utilizzato per implementare l'inizializzazione del plugin entry-point. Viene invocato dall'applicazione di base per passare al plugin i suoi (o alcuni dei suoi) oggetti fondamentali. Tipicamente è consigliabile (o almeno questa è la nostra esperienza) passare come parametro un hashmap contenente tutti gli "oggetti fondamentali" dell'applicazione di base. In questo modo l'entry-point caricato potrà scegliere di volta in volta quale oggetto utilizzare estraendolo dall'hashmap per mezzo del suo indice. Come viene mostrato in figura 1, gli "oggetti fondamentali" passati ai plugin costituiscono l'unico mezzo di interazione e comunicazione tra plugin e applicazione base. E' quindi opportuno progettare l' "applicazione base" e i suoi plugin in modo tale che esistano degli oggetti fondamentali atti alla comunicazione e all'interazione tra queste due entità.

StartPluginEntry()
questo metodo deve essere utilizzato per implementare la sequenza di eventi che attiva l'esecuzione del particolare entry-point. Ad esempio se l'entry-point è un JFrame l'implementazione del suo metodo startPluginEntry potrebbe contenere il codice this.setVisible(true) mentre, nel caso in cui fosse un Thread l'implementazione del suo metodo startPluginEntry potrebbe contenere il codice "this.Start()"

stopPluginEntry()
questo metodo deve essere utilizzato per implementare la sequenza di eventi che termina l'esecuzione del particolare entry-point. Ad esempio se l'entry-point è un Thread l'implementazione del suo metodo stopPluginEntry potrebbe contenere il codice "this.Stop()"

pausePluginEntry()
questo metodo deve essere utilizzato per implementare la sequenza di eventi che mette in pausa l'esecuzione del particolare entry-point. Ad esempio se l'entry-point è un JFrame l'implementazione del suo metodo pausePluginEntry potrebbe contenere il codice this.setVisible(false.)

Quello che deve essere chiaro quindi è che un entry-point può essere un'estensione di qualsiasi classe purchè implementi l'interfaccia PluginEntry. Una delle possibilità (è un approccio che consigliamo ma non è l'unico) è che gli entry-point realizzino delle JFrame contenenti l'interfaccia grafica che permette di accedere ad alcune (o a tutte) le funzionalità del Plugin cui appartengono. In questo modo i metodi dell'interfaccia PluginEntry sono sufficienti all'interazione con il Plugin. Infatti è possibile utilizzare initPluginEntry per inizializzare il Plugin con gli oggetti dell'applicazione con cui dovrà interagire (gli "oggetti fondamentali") ed il metodo startPluginEntry per visualizzare una sua interfaccia (quella del particolare entry-point). E' attraverso questa interfaccia grafica che l'utente sarà in grado di attivare i servizi che il Plugin mette a disposizione.

Come detto precedentemente, per descrivere le caratteristiche degli entry-point di un plugin viene inserito nella sua root un file xml avente nome "descriptor.xml".

Di seguito riportiamo la sintassi di questo file:


Figura 3 - Struttura del file descriptor.xml

I tag mostrati sono quelli che al momento sono stati pensati ed implementati. Va detto che mjpf è un progetto appena nato e non sono da escludere evoluzioni e cambiamenti.
Per ora possiamo dire che i tag Type, Name e Main sono sicuramente i tag più importanti per la descrizione di un entry-point. Con il tag Type infatti è possibile organizzare gli entry-point in famiglie (cosa utile se si vuole associare gli entry-point dei plugin alle voci della barra dei menu dell'applicazione di base); con il tag Name è possibile associare una nome simbolico ad un entry-point, mentre con il tag Main è possibile (nonché necessario) indicare qual è la classe del plugin che realizza l'entry-point descritto.
I tag Icon e Tips sono attualmente opzionali e sono stati inseriti nel caso il programmatore volesse associare un immagine agli entry-point o dei suggerimenti di tipo testuale.

 

Un piccolo esempio
Come esempio consideriamo il plugin SimplePlugin.jar che è possibile scaricare dal sito di mjpf. Questo è costituito da un' unica classe, SimpleFrame, che costituisce anche l'unico entry-point del plugin. il codice seguente mostra l'implementazione di SimpleFrame:

package simplePlugin
import mjpf.*;
import java.util.*;

public class SimpleFrame extends javax.swing.JFrame implements mjpf.PluginEntry {

  public SimpleFrame() {
    initComponents();
  }

  private void initComponents() {
    jPanel1 = new javax.swing.JPanel();
    jLabel1 = new javax.swing.JLabel();
    setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
    jPanel1.add(jLabel1);
    getContentPane().add(jPanel1, java.awt.BorderLayout.CENTER);
    java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
    setBounds((screenSize.width-399)/2, (screenSize.height-162)/2, 399, 162);
  }

  private javax.swing.JLabel jLabel1;
  private javax.swing.JPanel jPanel1;

  public void initPluginEntry(Object param){
    //inizializzazione del plugin con gli
    //oggetti passati dall'applicazione principale
    //vedere esempio in "caricare Plugin"
    Hashtable ht = (Hashtable)param;
    jLabel1.setText((String)ht.get("hello"));
  }

  public void startPluginEntry(){
    this.setVisible(true);
  }

  public void pausePluginEntry(){
    this.setVisible(false);
  }

  public void stopPluginEntry(){}

}

Com' è possibile notare (le righe in grassetto evidenziano le parti di maggior interesse..) SimpleFrame non è altro che una specializzazione della classe JFrame che implementa l'interfaccia PluginEntry. In particolare, questo entry-point, si aspetta come parametro di inizializzazione (initPluginEntry(Object elem)) un hashmap che contenga almeno un oggetto di tipo String corrispondente all'indice "hello". Questo valore verrà quindi assegnato come testo ad una label che verrà poi mostrata in un pannello del JFrame.

Al momento dell'esecuzione del pluginEntry (coincidente con l'invocazione del suo metodo startPluginEntry()) viene invocato il metodo setVisible(true) della classe JFrame che la visualizza mostrandone, al centro, il valore della stringa passatagli come parametro di inizializzazione dall'applicazione principale.
Questo esempio mostra, anche se in piccolo, che gli entry-point di un Plugin, nonostante siano caricati a runtime, sono in grado di gestire oggetti dell'applicazione di base che ha caricato i Plugin. Essendo questi oggetti passati per riferimento, una loro modifica all'interno del Plugin comporta una modifica dell'oggetto dal punto di vista dell' "applicazione di base" che ha caricato il plugin. Questo è possibile perchè entrambi effettivamente stanno gestendo lo stesso oggetto che diventa così facendo un mezzo di comunicazione tra l' applicazione di base e il plugin.
La classe SimpleFrame dovrà essere compilata nel package SimplePlugin e contenuta in un file jar (ad esempio SimplePlugin.jar).
Fatto questo, dovrà essere inserito, nella root del file jar, un file di testo chiamato "descriptor.xml" con contenuto simile a quello mostrato di seguito:

<plugin>
<entry>
<type>SimplePlugins</type>
<name>SimplePlugin</name>
<main>simplePlugin.SimpleFrame</main>
<icon>icon.gif</icon>
<tips>a simple frame </tips>
</entry>
</plugin>

 


Figura 4
- Struttura di un plugin

In questo viene espresso il fatto che il jar è un plugin che contiene un entry-point. In particolare questo entry-point appartiene alla famiglia di entry-point chiamata "SimplePlugins", ha nome "SimplePlugin" ed è realizzato dalla classe simplePlugin.SimpleFrame.

 

Caricare ed eseguire plugin
Finora abbiamo descritto il modo in cui realizzare plugin con mjpf.
Sperando che il tutto sia risultato chiaro vediamo ora come caricare ed eseguire i plugin realizzati dal lato dell'applicazione di base.
Immaginiamo di avere una cartella (directory è forse il termine più appropriato) in cui abbiamo inserito tutti i file jar relativi ai plugin realizzati.
Per ottenere un riferimento agli entry-point dei Plugin presenti in una certa directory, nell'ambito di un'applicazione che abbiamo definito di base, è possibile utilizzare la classe PluginFactory di mjpf.
La classe PluginFactory permette di caricare tutti gli entry-point dei plugin presenti in un URL che viene specificata come parametro di costruttore.


Figura 5 - 
La classe PluginFactory

All'interno della vostra applicazione di base dovete istanziare quindi un oggetto di tipo PluginFactory passandogli come parametro di costruttore l'url della directory in cui risiedono i plugin che intendete caricare. Fatto questo, la PluginFactory caricherà in modo automatico tutti i file jar contenuti nella cartella specificata (i vostri plugin.. appunto) effettuando il parsing dei file "descriptor.xml" in essi contenuti. In funzione delle informazioni contenute in questi descrittori la classe PluginFactory costruisce per ogni Plugin entry-point un oggetto di tipo EntryDescriptor. Com' è possibile notare nella figura in basso, un oggetto di tipo EntryDescriptor contiene tutte le informazioni che è possibile associare ad un EntryPoint mediante il file descriptor.xml insieme ad un id univoco che gli viene assegnato automaticamente dalla PluginFactory al momento del caricamento.


Figura 6
- La classe EntryDescriptor


Una volta ottenuto un oggetto PluginFactory è possibile ottenere una Collection degli EntryDescriptor caricati utilizzando il metodo getAllEntryDescriptor(). A questo punto è possibile determinare quali entry-point istanziare e caricare in funzione dei relativi EntryDescriptor.


Quindi, ricapitolando:

  1. istianziamo una PluginFactory specificando l'url della cartella contenente i Plugin;
  2. la PluginFactory carica tutti gli entry-point contenuti nei Jar presenti nella cartella indicata creando per ognuno un oggetto EntryDescriptor che racchiude le informazioni (per quell'entry point) presenti all'interno del file descriptor.xml del jar cui l'entry-point appartiene;
  3. E' possibile ottenere una Collection di tutti gli EntryDescriptor caricati attraverso il metodo getAllEntryDescriptor() di PluginFactory

Quando si desidera istanziare un entry-point si deve utilizzare il metodo getPluginEntry(Integer id) di EntryDescriptor, che prende come parametro l'id dell'entry-point che si intende caricare (ottenibile dall' EntryDescriptor per mezzo del metodo getId()). Il metodo getPluginEntry() restituisce un'istanza del PluginEntry richiesto che deve essere opportunamente castato su un oggetto di tipo PluginEntry.
A questo punto è possibile utilizzare il metodo initPluginEntry(Object param) per inizializzare l'entry-point ottenuto e gli altri metodi (startPluginEntry, pausePluginEntry, stopPluginEntry) per gestirne l'esecuzione nel modo opportuno.
Lo so... è un po articolato a descriversi ma un'occhiata al codice seguente chiarirà sicuramente il tutto.

...
String plugins_path = "...../plugin/";

//istanzio una PluginFactory
PluginFactory pf = new PluginFactory(plugins_path);

//ottengo gli EntryDescriptor caricati
Collection plugcol = pf.getAllEntryDescriptor();
Iterator plugiter = plugcol.iterator();

//ricerco il Plugin "SimplePlugin"
while(plugiter.hasNext()){
  EntryDescriptor pd = (EntryDescriptor)plugiter.next();
  If(pd.getName().equals("SimplePlugin")){
    PluginEntry pl = (PluginEntry)pf.getPluginEntry(pd.geId());
    String stringa = "hello world from a Plugin!";
    HashMap param = new HashMap();
    param.add("hello",stringa);
    //inizializzazione con oggetti fondamentali
    //ora l'applicazione base e il plugin hanno il riferimento alla
    //String stringa
    Pl.initPluginEntry(param);
    Pl.startPluginEntry();
  }
}
...

Nell'esempio mostrato viene scelto di eseguire un particolare PluginEntry in funzione del nome (nel caso specifico "SimplePlugin"), ottenuto il suo riferimento viene inizializzato con un hashmap che contiene uno oggetto String avente indice "hello" e a questo punto viene eseguito attraverso il metodo startPluginEntry.
Chiaramente potete utilizzare i criteri che di volta in volta ritenete maggiormente opportuni per selezionare quali entry-point eseguire e quali no.
In alternativa a questo metodo è possibile ottenere gli oggetti EntryDescriptor relativamente agli entry-point cui è stato associato un certo nome utilizzando direttamente il metodo getEntryDescriptorsByName(String) della PluginFactory istanziata. Poiché è possibile che esistano più entry-point con lo stesso nome, il metodo ritorna un Vector degli EntryDescriptor Object relativi.

 

Mjpf e Aucom
Come accennato precedentemente Aucom ha fatto e fa uso di mjpf specialmente nell'ambito della realizzazione di un'applicazione Client per un sistema di gestione di flotte di veicoli chiamato GEF. In figura 7 viene mostrata uno screen-shot di un caso d'uso del client di GEF. Questo è composto da una finestra principale in cui viene mostrata la mappa della zona relativa ai veicoli in osservazione (nel caso in questione la zona EUR di Roma) con diversi menù da cui è possibile accedere ai diversi servizi offerti dal sistema. In particolare la barra dei menu ha alcuni menu fissi (come "file" e "impostazioni") ed altri che vengono creati dinamicamente a tempo di esecuzione in funzione degli entry-point dei plugin individuati da mjpf nella cartella "plugin" dell'applicazione Client. In particolare viene creato una voce nella barra dei menu per ogni elemento type individuato nei descrittori xml dei plugin ed ogni riferimento agli entry-point con quel type viene inserito, per mezzo del suo nome, all'interno di quel menu.
Quando l'utente seleziona un oggetto da un menu, ad esempio il pannello di controllo della mappa (telecomando), l'applicazione Client di GEF ottiene un riferimento all' EntryDescriptor relativo per mezzo dell'istanza della PluginFactory precedentemente utilizzata, ne effettua l'inizializzazione e ne esegue il metodo startPluginEntry() rendendone visibile la GUI . Come scelta progettuale si è deciso di utilizzare come parametro di inizializzazione dei plugin dell'applicazione un hashmap contentente gli oggetti fondamentali come ad esempio la mappa da visualizzare e il collegamento al Server contenente i dati dei veicoli. Nella figura in basso viene mostrato l'uso del plugin "Telecomando" che permette di agire sulla mappa visualizzata effettuando operazioni di spostamento e zoom. Il modo in cui è stata realizzata quest'applicazione ci permette di estenderla in modo semplice e senza mai intervenire sul codice già scritto (i menu vengono aggiornati dinamicamente appena viene inserito un nuovo jar valido all'interno della cartella plugin). Per quanto riguarda delle possibili manutenzioni evolutive future, un'architettura simile consente di effettuare cambiamenti ai componenti in modo indipendente dagli altri. Prendiamo proprio l'esempio del "Telecomando"; se un giorno dovessimo avere bisogno di modificare questo strumento per aggiungere funzionalità di rotazione della mappa sarà sufficiente modificare il codice relativo al plugin ed inserirlo nuovamente nella cartella Plugin per vedere immediatamente in atto le modifiche apportate senza alcuna modifica del codice degli altri componenti.



Figura 7 - L' Applicazione GEF


Conclusioni
mjpf è una libreria costituita da un insieme essenziale di entità e di primitive utili alla realizzazione di applicazioni Java basate su plugin. L' esperienza ci ha dimostrato che una opportuna progettazione dell'applicazione rende possibile utilizzare mjpf in molteplici contesti. L'organizzazione degli aspetti funzionali di un'applicazione in plugin presenta molti vantaggi. Tra questi:

  • facilita considerevolmente la suddivisione dei task all'interno di un team di sviluppo;
  • permette di apportare modifiche evolutive attraverso minime (se non nulle) modifiche del codice già scritto;
  • consente di rendere un'applicazione personalizzabile e aggiornabile in modo immediato

mjpf è un progetto molto giovane e pertanto soggetto a continue evoluzioni e cambiamenti. Se avete provato mjpf vi invito a scrivermi per farmi sapere cosa ne pensate e se lo avete trovato utile. Scrivetemi anche nel caso in cui vogliate collaborare proponendo delle modifiche o, perché no, aiutandoci ad implementarle.

 

Riferimenti
[1] Eclipse: http://www.eclipse.org/
[2] JPF: http://jpf.sourceforge.net/
[3] Lorenzo Bettini - "Il ClassLoader", http://www.mokabyte.it/1998/03/clasload.htm
[4] http://mjpf.sourceforge.net

Andrea Sindico è laureato in Ingegneria Informatica Specialistica con indirizzo Sistemi ed Applicazioni Informatiche.
Aucom è una società con sede in Roma che ha intrapreso lo sviluppo di un dispositivo hardware a basso costo che integrando un ricevitore GPS con un modulo GSM/GPRS misura la posizione di un mezzo mobile e la riporta su un qualsiasi Web-server in modo da fornire un sistema di tracciamento basato integralmente su Internet a costi operativi estremamente competitivi.