MokaByte Numero 20 - Giugno 1998  


di
Lorenzo Bettini
  Agenti mobili in Java
(II parte)

 

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

la 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):

+
|--Agent
|--test

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:

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:

Questo non vuole essere un articolo su queste API, quindi vedremo solo gli aspetti che ci permetteranno di ottenere le informazioni che cerchiamo per poter spedire l'agente con tutti i dati necessari.

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:

Queste classi sono dotate di metodi per avere le informazioni sull'elemento, come ad esempio

Una volta ottenute le classi utilizzate dall'agente, basterà chiamare la funzione getClassBytes per ottenere i byte di queste classi. Questi saranno spediti insieme all'agente.

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() {
    return new AgentPacketEx( this, getClassBytes(), getUsedClasses() ) ;
  }

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):

  synchronized protected Hashtable getUsedClasses() {
    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 ) ;
      }
    }
  }

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.

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 classe

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

Questo algoritmo è implementato da due metodi in ricorsione mutua:

  // tramite le reflection API si ottengono le classi dei vari membri
  // 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 ) ;
  }

In 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).

Di seguito viene mostrato l'altro metodo in ricorsione mutua col precedente:

  protected void getUsedClassesNamesOf( Class classVar, Vector result ) {
    String className = filter( classVar.getName() ) ;
    if ( isUsefulClass( className ) && ! result.contains( className ) ) {
      // e' la prima volta
      result.addElement( className ) ;
      // chiamata ricorsiva
      getUsedClassesNames( classVar, 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.

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 )
      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 )) ) ;
    }
  }

Il metodo reconstructAgent semplicemente, tramite la tabella hash delle classi contenuta nel pacchetto ricevuto dalla rete, aggiorna la tabella del class loader.

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)

Questo paragrafo è stato modificato nell'Agosto 2001, in modo da fare funzionare il tutto anche con Java 1.2 e successive. Ringrazio tutti i lettori che mi hanno fatto notare i problemi riscontrati con le versioni della JVM 1.2 e mi scuso per il ritardo con cui ho provveduto a correggere il tutto.

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 ) {
    AgentHandler Ahandler ;
    Ahandler = new AgentHandlerEx( server, socket ) ;
    Ahandler.start() ;
  }

La classe AgentHandlerEx deriva da AgentHandler, vista l'altra volta. La classe derivata ridefinisce il metodo startAgentLoader:

  protected void startAgentLoader() {
    String agentLoaderClassName = new String ("Agent.AgentLoaderEx");
    String agentLoaderSuperClassName = 
      new String ("Agent.DefaultAgentLoader");

    try {
      AgentClassLoader loader = new AgentClassLoader() ;
      loader.setMessages( true ) ;
      loader.addClassBytes( agentLoaderClassName,
        ClassBytesLoader.loadClassBytes( agentLoaderClassName ) ) ;
      loader.addClassBytes( agentLoaderSuperClassName,
        ClassBytesLoader.loadClassBytes( agentLoaderSuperClassName ) ) ;
      loader.forceLoadClass( agentLoaderSuperClassName ) ;
      Class agentLoaderClass = loader.forceLoadClass( agentLoaderClassName ) ;
      AgentLoader agentLoader = (AgentLoader)agentLoaderClass.newInstance() ;
      agentLoader.setServer( server ) ;
      agentLoader.setInputStream( socket.getInputStream() ) ;
      agentLoader.setOutputStream( socket.getOutputStream() ) ;
      agentLoader.start() ;
    } catch ( Exception e ) {
      e.printStackTrace() ;
    }
  }

Notare che in questa implementazione non si sono seguiti i canoni della buona programmazione ad oggetti (infatti c'è molta duplicazione di codice); mi premeva di più fare notare che una sostanziale modifica è quella di fare caricare al class loader non solo il nuovo AgentLoader, cioè AgentLoaderEx, ma anche la sua classe base, cioè DefaultAgentLoader. In questo modo siamo sicuri che queste due classi saranno caricate col nostro class loader. Questo è fondamentale perché il caricamento delle classi dell'agente funzioni come prima.

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

 

A presto :-)

Lorenzo Bettini

 

Sorgenti (modificati in Agosto 2001)

ag2src.zip



Bibliografia

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