MokaByte
Numero 20 - Giugno 1998
|
|||
|
(II parte) |
||
Lorenzo Bettini |
|||
Riprendiamo il nostro discorso sull'implementazione degli agenti mobili in Java, ed espandiamo il package visto l'altra volta.
Introduzione
L'altra volta
[1] avevamo iniziato a vedere l'implementazione
di un package per la gestione degli agenti mobili [2,3]
in Java. Del resto si era anche detto che non si aveva la pretesa di realizzare
un vero e proprio framework, come quello degli Aglets [4],
comunque il package che deriverà alla fine, sarà già
utilizzabile per realizzare applicazioni con agenti mobili, e potrà
essere esteso (sempre secondo la filosofia della programmazione ad oggetti)
in modo semplice per ottenere maggiori funzionalità e caratteristiche
più avanzate.
Durante tutto l'articolo si farà continuamente riferimento al precedente articolo [1] e quindi si consiglia vivamente di tenere alla mano una copia stampata, o almeno di ridargli una veloce lettura.
L'altra volta ci eravamo lasciati con un quesito: Cosa c'è che non va con l'attuale implementazione del framework. L'esempio fornito a corredo funzionava a dovere. Del resto per essere sicuri che il class loader (utilizzato dall'AgentServer) non ricavasse le informazioni sulla classe dal file system locale, basta mettere il file di test in una directory a cui il server non può accedere; ovviamente il package dovrà essere raggiungibile tramite il CLASSPATH.
Si consiglia quindi di posizionarsi sulla directory dove è contenuta la directory Agent e qui lanciare
java Agent.AgentServerla directory del test deve essere messa in una directory non raggiungibile dal server. Ad esempio nei sorgenti dell'altra volta la struttura era la seguente (la stessa struttura viene mantenuta anche nei sorgenti di questo articolo):
+In questo modo posizionandosi su test basterà aggiornare il CLASSPATH con la directory precedente (..) in modo che il programma test riesca ad accedere al package Agent, ma il server il server Agent non riesca ad accedere al directory test; ad esempio sotto Win95 basterebbe eseguire il comando:
|--Agent
|--test
set CLASSPATH=%CLASSPATH%;..mentre sotto Unix (con la bash shell):
export CLASSPATH=$CLASSPATH:..Il problema del package dell'altra volta è che quando un agente viene spedito, vengono spedite solo le informazioni che riguardano la sua classe. Questo può andare bene per classi semplici, come quella vista l'altra volta, perché anche se non spediamo le classi String, Integer, queste saranno trovate sul file system locale del server, trattandosi di classi standard della libreria Java. Anzi è bene che queste classi non vengano spedite, per problemi di sicurezza (da cui il test nel class loader if( ! className.startsWith( "java.") ). Provate infatti a modificare la classe TestAgent della volta scorsa in modo che utilizzi una classe propria (anche questa ad esempio contenuta nel file TestAgent.java) avrete la spiacevole sorpresa che il server non riuscirà a trovare (giustamente) questa classe, in quanto non è stata spedita insieme all'agente. Un esempio è nel file TestAgentErr.java, che appunto provocherà l'errore suddetto sul server. Ovviamente il server è pensato bene per reagire a questi errori: l'agente non verrà eseguito: tutto qui. Questo è possibile grazie al fatto che il server non si occupa direttamente della gestione degli agenti, ma delega questo compito ad altri thread (AgentHandler).
In questo articolo vedremo come estendere il package dell'altra volta per risolvere questo problema. L'idea è quella di estendere le classi dello scorso articolo per creare nuove classi (tutte col suffisso Ex, per extended), in modo da sfruttare quello che è già corretto, sempre secondo la filosofia della programmazione object oriented. Questa filosofia vorrebbe che le classi base non venissero toccate; questo richiede un'attenta progettazione delle classi base. Purtroppo, a causa di alcune sviste di design, ho dovuto modificare alcuni particolari delle classi base. Si tratta però di piccole modifiche, quindi in effetti nei sorgenti di questo mese sono presenti le classi base (quelle della scorsa volta) leggermente modificate.
Introspection e Reflection
Per ovviare
al problema suddetto si ha la necessità di ricavare la struttura
non solo della classe dell'agente che vogliamo spedire, ma anche di tutte
le classi che l'agente utilizza (come già detto, evitando le classi
che fanno parte della libreria standard di Java). Il meccanismo di andare
ad analizzare a run time la struttura di una classe viene detto Introspection.
Per ottenere queste informazioni "basterebbe" andare a leggere direttamente in binario del file .class, e, conoscendo la struttura di questo tipo di file (tra l'altro resa nota), ricavare le classi utilizzate [5].
Esiste però un metodo più semplice, leggibile e a livello più alto (non dipendente da eventuali modifiche alla struttura dei file .class); questo è reso possibile dalle Reflection API, presenti dal jdk 1.1 [6]. Tramite queste API, contenute nel package java.lang.reflect è possibile ottenere la struttura di una classe a run time: questo comprende:
Le informazioni sugli elementi suddetti (che rappresentano la struttura di un classe) sono contenute in classi (presenti nel package java.lang.reflect) con un nome abbastanza espressivo, come ad esempio:
NOTA: con le reflection API attuali non è possibile ottenere informazioni sulle variabili locali dei vari metodi. Questo vuol dire che se una classe utilizzata da un agente viene utilizzata solo come tipo di una variabile locale di un metodo, tale classe non sarà scoperta dalla nostra ispezione, ed anche in questo caso l'agente non potrà essere eseguito correttamente sul server. Per utilizzare il package si deve quindi seguire la seguente:
Regola 1: se un agente utilizza una classe non presente fra quelle standard di Java, tale classe dovrà costituire il tipo di un campo della classe, di un parametro di un metodo, o di un tipo di ritorno di un metodo.
Del resto questa convenzione non è molto restrittiva. Iniziamo a vedere adesso le modifiche che dobbiamo apportare al nostro package: vediamo le classi derivate e la ridefinizione di alcuni metodi.
L'AgentPacketEx e l'AgentEx
L'AgentPacket,
che rappresenta quello che effettivamente viene spedito in rete, viene
esteso in modo da avere, oltre alla classe dell'agente, e all'agente stesso
(memorizzato in forma binaria), anche una hash table in cui le chiavi sono
i nomi delle classi utilizzate dall'agente, ed i valori sono i byte che
rappresentano queste classi:
public class AgentPacketEx extends AgentPacket {
public
Hashtable usedClasses ; // ( name, byte[] )
Quando l'agente
deve essere spedito, in seguito alla chiamata del metodo migrate,
viene chiamato il metodo sendAgent, che tramite la serializzazione
spedisce in rete l'AgentPacket. Tale pacchetto viene costruito tramite
la chiamata del metodo createAgentPacket (questa è stata
una modifica delle modifiche di cui sopra, apportata alla classe base Agent;
nella precedente versione il packet veniva creato all'interno di questo
metodo; sarebbe stato necessario riscrivere l'intero metodo, mentre in
questo modo basta ridefinire il metodo createAgentPacket; questa
è stata una svista nel design delle classi base). Basterà
quindi ridefinire questo metodo, in modo che venga creato un AgentPacketEx,
invece di un AgentPacket, e grazie al polimorfismo tutto il resto
continuerà a funzionare come prima:
synchronized protected AgentPacket createAgentPacket() {Il metodo getUsedClasses si occupa del recupero delle informazioni delle varie classi utilizzare dall'agente, sfruttando le API Reflection. Le classi vengono memorizzate in un membro di AgentEx, una hash table (usedClasses):return new AgentPacketEx( this, getClassBytes(), getUsedClasses() ) ;
}
synchronized protected Hashtable getUsedClasses() {Il metodo createUsedClassesTable provvede al riempimento della tabella hash: vengono prima ottenuti i nomi dell classi utilizzate dall'agente (a questo punto solo le classi effettivamente importanti sono state memorizzate nel vettore UsedClassesNames), e poi viene riempita la tabella hash, ottenendo i byte delle varie classi tramite la chiamata di getClassBytes, che avevamo visto la volta scorsa.if ( usedClasses == null )
createUsedClassesTable() ;
return usedClasses ;
}
synchronized protected void createUsedClassesTable() {
if ( usedClasses != null )
return ;
usedClasses = new Hashtable() ;
Vector UsedClassesNames = new Vector() ;
// otteniamo "tutte" le classi utilizzate da questa classe
getUsedClassesNames( getClass(), UsedClassesNames ) ;
byte[] classBytes ;
String className ;
Enumeration en = UsedClassesNames.elements() ;
while ( en.hasMoreElements() ) {
className = (String)en.nextElement() ;
classBytes = getClassBytes( className ) ;
if ( classBytes != null ) {
usedClasses.put( className, classBytes ) ;
PrintMessage( "Registrata "" + className ) ;
}
}
}
Si sarà notato che il vettore contenente i nomi di classe non viene restituito, ma passato come parametro. Infatti per il recupero dei nomi delle classi si adotta un algoritmo ricorsivo, e quindi torna più comodo passare il vettore come parametro, e lasciare che ogni chiamata aggiorni tale vettore.
L'algoritmo suddetto è il seguente:
input: una classe, un vettore di nomi di classeQuesto algoritmo è implementato da due metodi in ricorsione mutua:
begin
per ogni classe utilizzata dalla classe in input come:
tipo di un membro
tipo di ritorno di un metodo
tipo di parametro di un metodo (o costruttore)
esegui
aggiungi il nome della classe al vettore (se non c'è già)
richiama ricorsivamente questa procedura su questa classe
fine esegui
fine per ogni
richiama questa procedura sulla classe base
end
// tramite le reflection API si ottengono le classi dei vari membriIn questo metodo si vedono le Reflection API in funzione. Come si può notare l'ispezione dei vari elementi della classe è semplice. Tornerebbe molto comodo poter ottenere anche le varie ed eventuali inner class, dichiarate all'interno di una classe. A questo scopo servirebbe getDeclaredClasses. Tuttavia questo metodo non funziona. Dopo avere perso non poco tempo, mi sono deciso ad andare a vedere nei sorgenti di Java, ed ho scoperto che tale metodo non è ancora implementato. Anche in questo caso quindi non si potrà recuperare le classi dichiarate all'interno (e come prima si dovranno dichiarare per ognuna di essa dei membri nella classe esterna).// e dei parametri e dei valori di ritorno dei vari metodi della
// classe specificata
protected void getUsedClassesNames( Class c, Vector result ) {
Field[] fields = c.getDeclaredFields() ;
Constructor[] constructors = c.getDeclaredConstructors() ;
Method[] methods = c.getDeclaredMethods() ;
Class[] declClasses = c.getDeclaredClasses() ;
Class[] classes ;
int i, j ;
for( i = 0 ; i < fields.length ; i++ ) {
getUsedClassesNamesOf( fields[i].getType(), result ) ;
}
for ( i = 0; i < constructors.length; i++ ) {
classes = constructors[i].getParameterTypes() ;
if ( classes.length > 0 ) {
for ( j = 0; j < classes.length; j++ )
getUsedClassesNamesOf( classes[j], result ) ;
}
}
for ( i = 0; i < methods.length; i++ ) {
getUsedClassesNamesOf( methods[i].getReturnType(), result ) ;
classes = methods[i].getParameterTypes() ;
if ( classes.length > 0 ) {
for ( j = 0; j < classes.length; j++ )
getUsedClassesNamesOf( classes[j], result ) ;
}
}
// purtroppo getDeclaredClasses non è ancora implementata nel jdk
// quindi questi è in attesa che venga implementata, per adesso
// e' inutile (il vettore e' vuoto) :-(
for ( i = 0; i < declClasses.length; i++ ) {
getUsedClassesNamesOf( declClasses[i], result ) ;
}
getUsedClassesNamesOf( c.getSuperclass(), result ) ;
}
Di seguito viene mostrato l'altro metodo in ricorsione mutua col precedente:
protected void getUsedClassesNamesOf( Class classVar, Vector result ) {In questo modo si riesce ad ottenere i nomi delle classi utilizzate dall'agente; i dati binari di queste classi adesso potranno essere spediti insieme all'agente.String className = filter( classVar.getName() ) ;
if ( isUsefulClass( className ) && ! result.contains( className ) ) {
// e' la prima volta
result.addElement( className ) ;
// chiamata ricorsiva
getUsedClassesNames( classVar, result ) ;
}
}
Regola 2: Tutte le classi utilizzate da un agente devono implementare l'interfaccia java.io.Serializable.
Il caricamento di un agente remoto (l'AgentLoaderEx)
Ovviamente,
dovrà essere ridefinita anche la classe che riceve l'agente e lo
manda in esecuzione. Si sta parlando della classe DefaultAgentLoader,
estesa, per l'occasione, dalla classe AgentLoaderEx. Anche in questo
caso, grazie all'ereditarietà ed il polimorfismo, le modifiche da
effettuare sono poche. Basterà ridefinire il metodo startAgent,
in modo che prima che venga mandato in esecuzione (ricostruito) l'agente,
si aggiorni la tabella del class loader con le classi utilizzate dall'agente
(nella tabella del class loader è già presente, a questo
punto, la classe dell'agente: questo è stato fatto nel metodo start):
protected void startAgent( AgentPacket pack )Il metodo reconstructAgent semplicemente, tramite la tabella hash delle classi contenuta nel pacchetto ricevuto dalla rete, aggiorna la tabella del class loader.throws ClassNotFoundException, IOException, AgentException {
try {
if ( pack instanceof AgentPacketEx ) {
AgentPacketEx Pack = (AgentPacketEx)pack ;
reconstructAgent( Pack ) ;
}
super.startAgent( pack ) ;
} catch ( ClassCastException cce ) {
cce.printStackTrace() ;
}
}
protected void reconstructAgent( AgentPacketEx pack ) {
AgentClassLoader cl = (AgentClassLoader)(getClass().getClassLoader()) ;
Enumeration en = pack.usedClasses.keys() ;
String className ;
while ( en.hasMoreElements() ) {
className = (String)en.nextElement() ;
cl.addClassBytes( className, (byte[])(pack.usedClasses.get( className )) ) ;
}
}
A questo punto il class loader troverà tutte le informazioni di cui avrà bisogno nella propria tabella, e quindi l'agente potrà essere eseguito tranquillamente. E' da notare che il class loader non è minimamente coinvolto nell'estensione del package, in quanto l'interfaccia col mondo esterno è costituita dalla sua tabella.
Il nuovo server (AgentServerEx)
Ovviamente è
necessario modificare anche il server, in modo che utilizzi il nuovo loader
degli agenti (non si confonda questo loader, col class loader vero e proprio).
anche in questo caso le modifiche da fare sono minime: basta modificare
il metodo newAgentHandler:
protected void newAgentHandler( AgentServer server, Socket socket ) {Il nome agentLoaderName viene passato al costruttore; il default è ovviamente AgentLoaderEx.AgentHandler Ahandler ;
Ahandler = new AgentHandler( server, socket, agentLoaderName ) ;
Ahandler.start() ;
}
Anche la classe AgentHandler è stata modificata, rispetto a quella del precedente articolo, in modo che prenda come parametro il nome della classe che implementa l'AgentLoader. Anche in questo caso si è trattato di una svista in fase di design.
Un esempio
A questo punto
possiamo spedire agenti che utilizzano altre classi, non presenti nella
libreria di Java, essendo sicuri (pur di seguire le indicazioni suddette)
che l'agente si porterà dietro tutto il necessario. Una soluzione
sarebbe potuta essere quella in cui il server richieda via via le classi
necessarie all'agente, al momento in cui ne ha bisogno. Tuttavia risulta
più efficiente spedire tutto quello di cui ha bisogno l'agente in
una sola volta. Questo rientra nella filosofia di agente mobile, che viaggia
sempre con la sua valigia [3], e comunque
è anche la filosofia adottata nel memorizzare tutte le classi di
un'applet in un unico file JAR.
Ad esempio, seguendo le istruzioni all'inizio di questo articolo, lanciando come server l'AgentServerEx, e lanciando l'applicazione TestAgentEx presente nella directory test, si potrà vedere la migrazione di un agente che utilizza altre classi. In particolare la classe utilizzata utilizza a sua volta un'altra classe e deriva da un'altra classe. Tramite l'algoritmo presentato, tutte queste classi saranno recuperate, e quindi l'agente potrà eseguire sul server senza problemi: quando avrà bisogno di una classe il class loader saprà dove trovarla. Ma non è finita qui, l'agente dopo la prima migrazione effettuerà un'altra migrazione (stavolta l'ultima) sullo stesso sito, ma su un numero di porta uguale al precedente più uno (in un esempio reale l'agente migrerebbe effettivamente su un altro sito, ma per testare l'agente torna comodo utilizzare l'indirizzo di loopback 127.0.0.1). Quindi per testarlo si dovrà lanciare su un altro terminale un altro AgentServerEx, specificando come porta la 10000 (a meno che non abbiate specificato un numero di porta diverso da quello di default per il primo AgentServerEx). E' da notare che un agente si porta con sé anche l'hash table usedClasses, quindi non avrà problemi, quando dal primo server agent tenterà di migrare sul secondo e avrà bisogno di memorizzare nel pacchetto le proprie classi (sarebbe inutile cercare le classi sul file system locale del server!). Nella figura seguente è illustrato l'output dell'esempio
Sia all'AgentServer, che agli agenti è possibile, tramite il metodo setMessages (o tramite opzione passata a riga di comando) attivare la visualizzazione dei vari messaggi (le operazioni interne compiute, come il caricamento delle varie classi, ecc...). Questi sono utili in fase di debug, ma possono essere istruttivi anche per capire cosa avviene "dietro le quinte" del sistema ad agenti presentato.
Conclusioni
Il package presentato
non ha la pretesa di essere immediatamente utilizzabile per scopi professionali,
ma può essere personalizzato per ottenere un framework effettivamente
utilizzabile, e che magari si può avvicinare notevolmente a quello
proposto dagli Aglets. Fondamentalmente il package è carente
di meccanismi di sicurezza (che comunque possono essere facilmente aggiunti,
come probabilmente vedremo in un prossimo articolo).
Il package è costituito da una gerarchia di classi; effettivamente è stato scelto l'approccio di avere una classe base ed una derivata per scopi didattici. Probabilmente in un package reale non ci sarebbe stato bisogno di una specializzazione, in quanto la reflection sarebbe stata utilizzata subito. Tuttavia, se uno è consapevole del fatto che il proprio agente non utilizza ulteriori classi al di fuori di quelle standard (e di quelle del pacchetto Agent) potrebbe decidere di derivare da Agent invece che da AgentEx, in modo da avere un agente più snello. Del resto un AgentServerEx è in grado di gestire anche un semplice Agent.
Il materiale
qui presentato del resto si basa su una parte del framework realizzato
in [8].
Sorgenti
[1] Lorenzo Bettini,
Agenti mobili in Java (I parte), MokaByte Maggio 1998
[2] Fabrizio
Giudici, Agenti mobili, MokaByte Gennaio 1997 e Febbraio 1997
[3] General
Magic, Telescript, http://www.genmagic.com/Telescript
.
[4] IBM Aglets
Workbench http://www.trl.ibm.co.jp/aglets
.
[5] C. MacManis,
Take a look inside Java Classes, JavaWorld ( http://www.javaworld.com
) Agosto 1997.
[6] C. MacManis,
Take an in-depth look at the Java Reflection API, JavaWorld ( http://www.javaworld.com
) Settembre 1997.
[6] Lorenzo
Bettini, Il class loader, MokaByte Marzo 1998
[7] Lorenzo
Bettini, La programmazione distribuita in Java, MokaByte Novembre
1997
[8] Lorenzo
Bettini, Progetto e realizzazione di un linguaggio di programmazione
per codice mobile, tesi di Laurea in Scienze dell'Informazione, Aprile
1998, http://rap.dsi.unifi.it .
MokaByte Web 1998 - www.mokabyte.it MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it |