MokaByte Numero 18 - Aprile 1998  

  di 
Donato Cappetta  
 
Lorenzo Bettini 
 
I  Un NetworkClassLoader 

  In questo articolo vedremo un esempio di class loader personalizzato, in particolare un class loader che carica le classi dalla rete.


La volta scorsa abbiamo visto un'introduzione sul meccanismo di personalizzazione di un class loader in Java.

Questa volta, come già accennato, vedremo un esempio più concreto class loader personalizzato: un NetworkClassLoader, cioè un class loader che scaricherà le classi dalla rete.

Questo tipo di class loader è un esempio di quello che viene detto Codice Mobile.

Codice Mobile

Ultimamente nella programmazione distribuita è sempre più utilizzato il concetto di Computazione Mobile (Mobile Computation) e Codice Mobile (Mobile Code).

L'espressione codice mobile è utilizzata con vari significati in letteratura, ad esempio:

Usualmente ci si riferisce al codice mobile come a del software in grado di viaggiare su una rete eterogenea, attraversando domini di protezione, e che può essere eseguito automaticamente all'arrivo a destinazione. I vantaggi del codice mobile sono molti, fra i quali:

Il codice mobile costituisce una specie di sistema distribuito dove i processi non locali non devono essere conosciuti in anticipo sul sito di esecuzione. Vi è comunque una sostanziale differenza fra un sistema distribuito e la mobilità di codice:

Vi sono già molti esempi di codice mobile, attualmente in uso; anche se non venivano considerati tali, alcuni di essi venivano utilizzati molto prima dell'esplosione di Internet:

L'idea del NetworkClassLoader

Il NetworkClassLoader presentato nell'articolo permette di tenere memorizzate le classi su un unico server; le varie applicazioni (client) possono richiedere il caricamento di una classe tramite il NetworkClassLoader.

La volta scorsa avevamo visto che Java permette di personalizzare il meccanismo tramite il quale vengono caricate in memoria le classi utilizzate da un'applicazione: il class loader.

Java mette a disposizione la classe ClassLoader da cui si può derivare un proprio class loader e ridefinire il metodo loadClass, utilizzato, appunto, per caricare le informazioni di una classe (membri e metodi) in memoria, durante l'esecuzione di un'applicazione:

public Class loadClass(String className) 
          throws ClassNotFoundException {...}

public synchronized Class loadClass(String className, boolean resolveIt)
          throws ClassNotFoundException {...}

Rivediamo brevemente quello che si deve fare all'interno di questo metodo:

  1. Controllare se la classe richiesta è già stata caricata, ed in tal caso restituire l'oggetto memorizzato nella tabella delle classi caricate.
  2. Cercare di caricare la classe dal file system locale, tramite il class loader primordiale.
  3. Cercare di caricare la classe dal proprio repository (una tabella, scaricando i dati dalla rete, ecc...).
  4. Richiamare defineClass coi dati binari ottenuti.
  5. Eventualmente risolvere la classe tramite il metodo resolveClass.
  6. Restituire l'oggetto classe al chiamante.

In questo caso il repository personalizzato è la rete: si caricheranno le classi da un server remoto.

Si ricordi che per quanto detto la volta scorsa, tutte le classi che sono necessarie ad una certa classe A sono caricate con lo stesso class loader con cui è stata caricata A. Quindi non ci si dovrà preoccupare di sapere in anticipo le classi necessarie per l'applicazione (classe) che intendiamo scaricare dalla rete: automaticamente il meccanismo di caricamento delle classi di Java, provvederà a caricarle tramite il NetworkClassLoader.

Ed inoltre ogni class loader utilizza un name space diverso e privato: una classe riesce ad accedere solo alle classi caricate con lo stesso class loader; quindi per ogni class loader Java mantiene un name space differente e separato. Proprio per questo motivo, e per il fatto che due classi sono considerate castable, solo se hanno un classe in comune fra le classi parente, una classe caricata col class loader personalizzato deve derivare da una classe (o implementare un'interfaccia) caricata dal file system locale.

 

Implementazione di un NetworkClassLoader

    Affinchè delle classi possano essere caricate dal network è indispensabile, oltre al NetworkClassLoader,  la presenza di un programma che si comporti da Server. Occorre un programma che si comporti da contenitore  da  distributore di  classi.

    Per l'implementazione di un NetworkClassLoader conviene procedere divedendo l'applicazione in due programmi:

  1. Il programma Client costituito dal NetworkClassLoader vero e proprio, che si occupa di richiedere  e caricare  le classi dal network.
  2. Il programma Server  -  d'ora in poi ClassServer - che si occupa di inviare le classi al NetworkClassLoader.

 

Il programma Client
 
Per definire un proprio loader delle classi occorre ereditare dalla classe java.lang.ClassLoader è definire una implementazione concreta del metodo astratto

protected abstract Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException;

I parametri del metodo loadClass() hanno il seguente significato:

  1. String name: rappresenta il nome della classe che si vuole caricare.
  2. boolean resolve: il valore di resolve deve essere  true se la classe  contiene riferimenti ad altre classi, quindi si dice che deve essere risolta. Altrimenti false.

  3. L'argomento sarà ripreso nel corso dell'articolo.
  4. throws ClassNotFoundException se la classe non viene trovata si deve generare l'eccezione  ClassNotFoundException.
  5. return Class la classe va restituita come oggetto di tipo java.lang.Class.

In linea generale il comportamento del metodo loadClass() va definto nel seguente modo:
dato il parametro name rappresentante il nome di una classe:  

 
Accanto ad ogni operazione è stato riportato anche il nome del metodo preposto per quel compito.
I metodi elencati, tranne loadClassFromServer(),  sono già definiti nella classe java.lang.ClassLoader (JDK1.1), dalla quale si va ad ereditare.

Precisamente questi metodi sono:

  1. findLoadedClass(String name): il metodo cerca la classe - per nome -  nella cache, se la trova restituisce un oggetto di tipo Class, altrimenti restituisce null. La cache è un'istanza della classe java.util.Hashtable ed è anch'essa implementata ed inizializzata nella classe ClassLoader.  
  2. findSystemClass(String name): il metodo cerca la classe nel file system locale, nelle directory specificate nella variabile d'ambiente CLASSPATH. Se la trova restituisce un oggetto di tipo Class, altrimenti genera l'eccezione   ClassNotFoundException.
    In generale l'uso di questo metodo è necessario  per due motivi:

    - Ottimizzazione: quando si definisce un  loader per una determinata classe,  tutte le altre classi che fanno riferimento ad essa vengono caricate con quel loader. E' opportuno che una classe prima di essere cercata sul network venga cercata nel file system locale soprattutto perchè  il network  è "lento" rispetto al file system.
    - Sicurezza: è preferibile che le classi provenienti da un ambiente meno protetto (il network) non vadano a sostituire quelle già presenti in locale (ambiente più protetto).  
  3. defineClass(String name, byte data[], int offset, int length)il metodo converte l'array di byte data[], rappresentante una classe,  in un oggetto di tipo Class al quale viene attribuito il nome  name.
    Se la conversione non va a buon fine (l'array di byte è corrotto,  il nome name non è valido, etc.) viene generata l'eccezione: ClassFormatError.
    Il metodo effettua anche una prima risoluzione della classe.
    Per poter costruire una classe partendo dall'array di byte occorre avere anche una rappresentazione della sua superclasse. Il tal caso il metodo sospende l'elaborazione e richiama il loader corrente  (rappresentato da loadClass()) passandogli correttamente i nuovi parametri name e resolve.
    L'operazione si ripete ricorsivamente finchè non viene ricostruita l'intera gerarchia.
    Il metodo, inoltre, si occupa di inserire la classe nella cache. In questo modo la cache contiene solo le classi provenienti dal network.  
  4. resolveClass(Class c) Una classe può far riferimento ad altre classi. Questo metodo si occupa proprio di invocare il loader (ovvero loadClass()) finchè non sono state caricate tutte le classi  referenziate.
    Se una classe referenziata fa riferimento ad altre classi il loader viene invocato in modo ricorsivo.
    Il metodo viene sempre inserito in un costrutto così fatto:
        if (resolve)  resolveClass(c);
    dove resolve diventa false nel momento in cui una classe non ha più riferimenti.
    Lo stesso metodo resolveClass(Class c) si occupa di passare correttamente il parametro resolve ogni volta che viene invocato il loader.
    In pratica la prima volta che si invoca il metodo loadClass() conviene attribuire il valore true al parametro resolve, dopodichè le successive impostazioni sono risolte automaticamente.
    Più avanti si vedrà che la classe ClassLoader è implementata in modo tale che ci si può   completamente disinteressare di questo parametro.

Rimane da analizzare il metodo loadClassFromServer() che va implementato.

Il metodo loadClassFromServer()

     Il metodo viene invocato quando una classe,  non essendo presente in locale, deve essere cercata sul network, in particolare sul programma server ClassServer.
 
    Si suppone che quando questo metodo viene invocato la connessione con il ClassServer è stata già stabilita. Cioè si immagini l'esistenza di un metodo connect()che viene invocato prima del metodo loadClassFromServer() o che  viene invocato, una volta per tutte, direttamente dal costruttore della classe NetworkClassLoader.
Il metodo connect() può essere così implementato:

     protected void connect() throws UnknownHostException, IOException { 
        try { 
          socket = new Socket(hostName, serverPort); 
          is = new DataInputStream(new 
               BufferedInputStream(socket.getInputStream())); 
          os = new DataOutputStream(new 
               BufferedOutputStream(socket.getOutputStream())); 
        } catch(UnknownHostException uhe){ 
          throw uhe; 
        } catch(IOException ioe){ 
         throw ioe; 
        } 
     } 

Dove hostName e serverPort sono degli attributi privati della classe, settati opportunamente dal costruttore.
Analogamente si può immaginare l'esistenza di un metodo disconnect() che chiude la connessione con il ClassServer:

     protected void disconnect(){
        try {
          os.close(); os = null;
          is.close(); is = null;
          socket.close(); socket = null;
        } catch(IOException ioe){
        } catch (Exception e) {}
     }

In che punto del programma invocare il metodo connect() (o il metodo disconnect()) è solo una scelta implementativa.

La logica del metodo loadClassFromServer(), si può così riassumere:
 

L'array di byte una volta restituito viene trasformato in classe e risolto.

L'implementazione completa del NetworkClassLoader è riportata nel file NetworkClassLoader.java.

Analizzando il listato si osserva che:

    Anche l'eccezione ConnectClassServerException è stata appositamente implementata per il loader.

    Ci si potrebbe chiedere del perchè di queste due nuove classi per la gestione delle eccezioni visto che la libreria di classi Java  è già fornita di una valida gerarchia.
    Il perchè  è da ricercare nel modo in cui è stato definito il metodo astratto loadClass() in java.lang.ClassLoader:

protected abstract Class loadClass(String name, boolean resolve)
    throws ClassNotFoundException;

si vede che nella clausola throws è presente solo l'eccezione ClassNotFoundException.
C'è però l'esigenza di gestire anche condizioni di eccezioni dovute a problemi di connessione con il server o ad accessi illegali al package Java.
    Ora, quando si va a ridefinire il metodo loadClass() è possibile aggiungere nella clausola throws solo eccezioni che siano sottoclassi di ClassNotFoundException.
Non è possibile aggiungere eccezioni del tipo UnknownHostException, IOException, IllegalAccessException, ecc... altrimenti il codice non viene compilato.

Le classi JavaPackageException  ConnectClassServerException  ereditano  da ClassNotFoundException.
 

Il programma Server 

    Il programma Server si occupa di inviare i file .class  al NetworkClassLoader. I file vengono inviati come array di byte.

Il programma è composto da due classi:

  1. ClassServer:  rappresenta il processo in attesa di connessioni su una determinata porta.
  2. WorkerClassServer: rappresenta il thread che comunica con il client una volta accettata la connessione.

  La classe ClassServer
 E' costituita da un ciclo infinito che attende delle connessioni su una determinata porta.
Per ogni nuova connessione crea un nuovo thread WorkerClassServer al quale viene affidata la comunicazione con il client.

Il blocco di codice principale di questa classe è il seguente:

while (true) {
  Socket clientSocket = null;
  try {
   clientSocket = serverSocket.accept();
   WorkerClassServer wcs  = new WorkerClassServer(clientSocket, classesCache);
   wcs.start();
  } catch (IOException ioe) {
   continue;
  } catch(Throwable t){
   continue;
  }
} //end while()

l'istruzione serverSocket.accept() attende la connessione di un client. Quando  viene stabilita restituisce il socket associato al client: clientSocket.
Dopodichè viene creata una nuova istanza di  WorkerClassServer alla quale viene passato il clientSocket e l'oggetto classesCache:

   WorkerClassServer wcs  = new WorkerClassServer(clientSocket, classesCache);

Il thread viene avviato: wcs.start().

L'oggetto classesCache  è un'istanza di java.util.Hashtable  destinata a  funzionare da cache per il  ClassServer.
Ogni classe richiesta dal NetworkClassLoader, viene  aggiunta nella cache  in modo da ottimizzare un successivo ricaricamento.
La cache è stata implementata in modo da essere globale al programma server,  cioè tutti i thread hanno accesso e aggiornano la stessa cache. Un'altra soluzione potrebbe essere che ogni thread gestisce una propria cache; anche in questo caso si tratta di una scelta implementativa.
 
  La classe WorkerClassServer

    La classe eredita da java.lang.Thread e si occupa di soddisfare le richieste del client. Viene istanziata dalla classe classServer, la quale gli passa il parametro  clientSocket per stabilire la connessione con il client, e classesCache il riferimento all'oggetto cache.

La parte principale della classe è composta da un ciclo infinito inserito nel metodo run().
Definito os e is come stream di input e output associati al clientSocket; la logica generale del ciclo è la seguente:  

Il metodo loadClassFromFile() è da implementare.

Il metodo loadClassFromFile()

Il metodo viene invocato per caricare una classe dal file system come array di byte.
Dato il nome di una classe nameClass, la logica del metodo loadClassFromFile() è la seguente:  

Il grosso del lavoro viene svolto dal metodo statico: getSystemResourceAsStream()  implementato in java.lang.ClassLoader.
Dato il nome di un file il metodo lo cercare nel CLASSPATH, se lo trova restituisce un input stream is  associato al file, se non lo trova restituisce null.

L'implementazione completa della classe WorkerClassServer è riportata nel file WorkerClassServer.java.
 
Per completare l'applicazione occorre definire altre due classi: RunServer e RunLoader che si occupano rispettivamente di istanziare il ClassServer e il NetworkClassLoader.

La classe RunServer contiene il metodo main() con all'interno le istruzioni che istanziano il ClassServer:

      ClassServer cs = new ClassServer(port);
      cs.start();

Il parametro port rappresenta la porta di ascolto del Server.
L'istruzione cs.start() è  presente perchè nell'implementazione, disponibile nei sorgenti,  anche la classe ClassServer eredita dalla classe Thread.
 
La classe RunLoader  contiene il metodo main() con all'interno le seguenti istruzioni principali:
 
    ClassLoader loader = new NetworkClassLoader(hostName, port);
    Class c = loader.loadClass(mainClass);
    Object main = c.newInstance();

dove:

  1. ClassLoader loader = new NetworkClassLoader(hostName, port): istanzia il loader e vengono passati al NetworkClassLoader i parametri hostName e port del ClassServer a cui connettersi.
  2. Class c = loader.loadClass(mainClass): carica la classe rappresentata da mainClass (mainClass è una Stringa).  Il loader verrà invocato ricorsivamente finchè  tutte le  classi necessarie per istanziare mainClasse non sono state caricate.
    Si osserva che non è stato invocato il metodo
    protected loadClass(String nameClass, boolean resolve) implementato il NetworkClassLoader, ma il metodo public loadClass(String nameClass)  implemantato in ClassLoader. Quest'ultimo metodo si occupa di richiamare il metodo loadClass(String nameClass, boolean resolve) passandogli nameClasse e impostando  resolve in modo automatico (true la prima volta).
  3. Object main = c.newInstance(): crea un'istanza della classe mainClass. In questo caso l'istanza creata sarà di tipo Object. E' necessario che la classe a cui si applica la newInstance() derivi da una classe (o interfaccia) comune sia al client che al server.
    La newInstance() esegue il costruttore di default della classe caricata.

 Vanno gestite opportunamente le eccezioni e le condizioni di errore.  

Istruzioni per l'uso

Per eseguire il NetworkClassLoader, occorre aver installato sulla propria macchina una Java Virtual Machine (JDK o JRE o altre) conforme alla versione 1.1 della SUN.
Dalla directory dove e' contenuto il file RunLoader digitare il comando:

java RunLoader hostName nameClass

dove hostName e' il nome della macchina server e nameClass la classe che si vuole caricare usando il NetworkClassLoader.
Si noti che il NetworkClassLoader cerca prima la classe in locale, e poi sul server. Quindi la connessione al server verra' stabilita solo se necessario.
Il nome della classe va digitato senza .class e rispettando corretamente miuscole e minuscole.

La classe RunLoader applica il metodo newInstance() sulla classe caricata, quindi viene eseguito il costruttore di default di quella classe.

Anche per eseguire il ClassServer, occorre aver installato sulla propria macchina una Java Virtual Machine conforme alla versione 1.1 della SUN.
Dalla directory dove e' contenuto il file RunServer digitare il comando:

java RunServer

Va in esecuzione la classe ClassServer, che si pone in ascolto di una connessione sulla porta 5050.
Quando il programma viene avviato prova ad individuare il nome (e l'indirizzo IP) della macchina su cui va in esecuzione. Se questo non e' possibile viene generata l'eccezione UnknownHostException .

Quando viene richiesta una classe, viene cercata nel CLASSPATH della macchina su cui il programma e' in esecuzione.

Conclusioni

    Alla base dei Network Computer c'è proprio un loader che quando necessario scarica le classi dal network.
La possibilita di "centralizzare" il software su una macchina server, e di avere dei client che si scaricano l'applicazione quando occorre,  rappresenta una soluzione per la riduzione dei tempi (e dei costi) di amministazione e manutenzione di un sistema informatico.
Una applicazione verrebbe installata (o aggiornata) una sola volta sulla  macchina server, i client automaticamente ad ogni riconnessione vedrebbero in esecuzione la nuova versione installata.
    Attualmente gli Applet tendono già a rappresentare questa filosofia, anche se con delle limitazioni, imposte per motivi di sicurezza. Per essi esiste un loader (un AppletClassLoader in genere inserito nel package sun.applet), che scarica le classi  dal network utilizzando il protocollo http e facendo le richieste ad un programma server che è il server Web.
    Implementando un NetworkClassLoader è possibile stabilire  come gestire la sicurezza, quale protocollo utilizzare,  quale tecnica utilizzare per scaricare  le classi dal network e come implementare il server in base alle proprie esigenze e senza nessuna limitazione.

 
  Donato Cappetta
Lorenzo Bettini
 



Bibliografia

[1] Adobe Systems Incorporated. Poscript Language Reference Manual. Addison-Wesley, 1985
[2] S.J. Cannan, G.A.M. Otten. SQL - The Standard Handbook. McGraw-Hill, New York, 1992 (edizione italiana: Il manuale SQL, McGraw-Hill Italia, Milano).
[3] N.S. Boreinstein. Email with a mind of its own. ftp://ftp.fv.com/pub/code/other/safe-tcl.tar.gz , 1994.
[4] The Java ™ Language Specification. ftp://ftp.javasoft.com/spec
[5] Il Class Loader. Mokabyte  n. 17 - Marzo 1998

Sorgenti

nclsrc.zip