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:
- istianziamo
una PluginFactory specificando l'url della cartella
contenente i Plugin;
- 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;
- 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.
|