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:
- Il termine codice mobile descrive un programma che può essere spedito senza cambiamenti ad una serie di calcolatori eterogenei ed eseguito con la stessa semantica su ognuno di essi.
- Il codice mobile è un approccio in cui i programmi sono considerati come documenti e quindi dovrebbero essere accessibili, trasmessi e mostrati (cioè eseguiti) come ogni altro documento.
- Gli agenti mobili sono oggetti contenenti del codice che possono essere trasmessi fra i partecipanti alla comunicazione in un ambiente distribuito.
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:
- Efficienza: quando sono necessarie ripetute interazioni con un sito remoto, può essere più efficiente spedire il processo direttamente sul sito remoto, in modo che vi interagisca localmente. Questo soprattutto quando la latenza della rete è alta e l'interazione consiste di molti messaggi piccoli.
- Semplicità e flessibilità: il mantenimento di una rete può essere molto più semplice quando le applicazioni sono su un server, ed i client, autonomamente, quando è necessario, le installano sul proprio sito, scaricandole dal server. L'installazione di nuovo software, o l'aggiornamento, diviene indipendente dalla natura, e dal numero dei client. In alcuni casi è possibile sapere a priori tutte le parti di codice si cui un certo sito necessita.
- Occupazione di spazio: scaricare il codice quando è necessario, invece di avere tutti i programmi duplicati su tutti i siti, permette di ridurre l'occupazione di spazio.
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:
- Un sistema distribuito implementa una piattaforma dove le varie componenti, localizzate fisicamente su siti differenti di una rete, appaiono locali. Gli utenti di tale sistema non sono consapevoli della struttura della rete sottostante, in quanto il sistema provvede a rendere trasparente la distribuzione sui vari computer.
- Le tecnologie che supportano la mobilità di codice, non nascondono la struttura della rete di computer sottostante, anzi la rendono evidente; il programmatore ne è quindi consapevole, avendo controllo, possibilmente in modo semplice, sulla distribuzione dei vari componenti.
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:
- Il linguaggio Poscript per la descrizione di documenti, sviluppato dall'Adobe Systems \[1\]. Si tratta di un vero e proprio linguaggio di programmazione, che permette di stampare un documento su una stampante Poscript; tale processo di stampa consiste nel mandare alla stampante un programma che descrive le pagine che devono essere stampate. La stampante una volta che ha ricevuto il programma lo esegue, e come risultato si ha la stampa del documento. L'utilizzo di un tale linguaggio rende la stampa di un documento indipendente dalla particolare stampante, e permette di descrivere anche complesse immagini in modo compatto. Tutto questo ha reso difatti il Poscript un linguaggio standard per molte stampanti laser.
- La Tecnologia dei Database è un'altra area dove, da molto tempo, viene utilizzato il concetto di codice mobile: invece di trasferire un enorme database dal server al client che vuole farci delle operazioni, il client spedisce delle istruzioni in linguaggio SQL \[2\] al server; questo le eseguirà e spedirà il risultato al client.
- I documenti con codice eseguibile al loro interno, trasmessi in rete sono un altro esempio di codice mobile. Ad esempio, come ormai ben sappiamo, nelle pagine HTML possono essere contenute delle applicazioni scritte in Java; oppure la possibilità di includere del codice eseguibile all'interno delle e-mail, tramite il linguaggio Safe-Tcl \[3\].
- Il problema della distribuzione del software e dell'installazione può essere risolto tramite il downloading del software dal server, da parte dei client (come già accennato).
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.
L'argomento sarà ripreso nel corso dell'articolo. 3. throws ClassNotFoundException: se la classe non viene trovata si deve generare l'eccezione ClassNotFoundException. 4. 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 namerappresentante il nome di una classe:
- Si cerca la classe in una cache di classi: findLoadedClass()
- Se non viene trovata nella cache si cerca la classe nel file system locale: findSystemClass()
- Se non viene trovata nel file system si cerca la classe sul ClassServer e, se presente, viene trasferita come array di byte: loadClassFromServer()
- Se la classe non viene trovata si genera l'eccezione: ClassNotFoundException
- Si converte l'array di byte in un oggetto di tipo e si inserisce la classe nella cache: defineClass()
- Si risolve la classe: resolveClass()
- La classe viene restituita: return Class.
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.
.
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:
- Al metodo viene passata una stringa che rappresenta il nome della classe, nameClass, da richiedere al ClassServer.
- La stringa nameClass viene inviata al ClassServer: os.writeUTF(nameClass)
- Il ClassServer risponde inviando la dimensione, in byte, della classe; viene inviato -1 se la classe non è stata trovata.
- Viene letta la dimensione della classe: size = is.readInt()
- Se size == -1la classe non è stata trovata: throw new ClassNotFoundException()
- Viene allocato l'array di byte necessario per la classe:classBytes = new byte\[size\]
- Viene letta la classe come array di byte: is.read(classBytes)
- L'array di byte viene restituito al metodo loadClass(): return classBytes
L'array di byte una volta restituito viene trasformato in classe e risolto.
Analizzando il listato si osserva che:
- Si è scelto di fare la connect() al ClassSever nel metodo loadClass().
Solo se la classe non è presente in locale, e deve essere caricata dal ClassServer, viene stabilita la connessione. E' stata introdotta una variabile booleana connected che viene settata a true, dal metodo connect(), la prima volta che si effettua la connessione.
- Sempre nel metodo loadClass() esiste un blocco di codice, prima della connect(), preposto al controllo che dal network non vengano caricate le classi che fanno parte del package standard di Java.
Il blocco di codice è il seguente:
if (nameClass.startsWith( "java.")) throw new JavaPackageException(nameClass);
Per questioni di sicurezza se si cerca di caricare una classe del package Java dal network viene generata l'eccezione JavaPackageException. Eccezione appositamente implementata per il loader ed aggiunta nella clausolathrows del metodo loadClass().
- Una'altra eccezione che può essere generata dal metodo loadClass() è:
ConnectClassServerException
essa viene lanciata quando fallisce la connessione con il ClassServer.
L'eccezione viene generata sia quando ci sono problemi di connessione iniziale con il server (UnknownHostException) sia quando ci sono problemi durante la connessione (IOExceptione SocketException).
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 e 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 classesCacheil 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:
- Attende che venga inviato il nome di una classe: nameClass = is.readUTF()
- Dato il nome nameClass sostituisce il carattere '.' con il separatore di file del sistema operativo sottostante (es. '/') per consentire il caricamento delle classi contenute nei package: nameClass.replace('.', fileSeparator.charAt(0))
- Prova ad acquisire la classe dalla cache: classesCache.get(nameClass)
- Se la classe non è presente in cache la carica dal file system locale come array di byte: classBytes=loadClassFromFile(nameClass)
- Se la classe è presente viene inviata la dimensione dell'array di byte al client: os.writeInt(classBytes.length).
- Viene inviato l'array di byte al client: os.write(classBytes, 0, classBytes.length)
- Se la classe non è presente viene inviato il valore -1 al client: os.writeInt(-1)
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:
- Aggiunge .class al nome nameClass.
- Cerca il file nel CLASSPATH del server: is = ClassLoader.getSystemResourceAsStream(nameClass)
- Se il file non esiste genera l'eccezione: throw new FileNotFoundException(nameClass)
- Calcola la dimensione del file: size = is.available()
- Alloca l'array di byte: classBytes = new byte\[size\]
- Legge il file: is.read(classBytes)
- Restituisce l'array di byte: return classBytes
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.
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 publicloadClass(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 RunLoaderhostName 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., 1994.
\[4\] The Java \™ Language Specification.
\[5\] Il "Class Loader" in [](../03/index.htm)
Sorgenti
> [](nclsrc.zip) > [](nclsrc.zip) > [](nclsrc.zip)
