Introduzione
I
servizi di naming sono un pilastro fondamentale di
Internet, e noi tutti li usiamo spesso e senza rendercene
conto. Chi sta leggendo ora questo articolo ha probabilmente
puntato il suo navigatore su www.mokabyte.it piuttosto
che l'indirizzo numerico 212.177.120.162, decisamente
più difficile da ricordare. Il vostro computer
ha effettuato, in modo del tutto trasparente, un'interrogazione
su uno dei servizi fondamentali di Internet, il DNS:
Domain Naming Service. La domanda era "a quale
indirizzo è associato il nome www.mokabyte.it?"
ed un server predisposto ha fornito la risposta.
Oltre
che a rendere la vita più facile per evitare
di memorizzare numeri, un servizio di naming porta
anche ad una maggior configurabilità di un
sistema, permettendo un maggiore disaccoppiamento
tra fornitore e fruitore di servizio. Infatti voi
non solo non dovete sapere che l'indirizzo IP di Mokabyte
è per l'appunto 212.177.120.162, ma non dovete
neanche preoccuparvi di operazioni amministrative
del service provider di Mokabyte, che dall'oggi al
domani potrebbe decidere di spostare il suo sito su
un indirizzo diverso, per esempio 12.184.31.32. Sarà
sufficiente che il database DNS tenga conto della
modifica e tutti i lettori di Mokabyte potranno continuare
a leggere tranquillamente la loro rivista preferita.
Un
altro esempio di naming service è quello fornitoci
dal filesystem del nostro sistema operativo: per esempio
il nome C:\temp\file.txt (mnemonico e facile da ricordare)
è associato ad un ben preciso numero di settore
fisico sull'hard disk dove sono memorizzati i dati.
Il sistema operativo è libero di riorganizzare
il modo in cui memorizza i dati (per esempio una deframmentazione
potrebbe spostare i dati su altre aree del disco),
ma il nome c:\temp\file.txt ci permette comunque di
accedere ai dati senza tenere conto di questi dettagli.
I
due esempi citati permettono di introdurre il concetto
di name syntax: ogni sistema di naming definisce una
sua propria sintassi di nomi validi. Il DNS prevede
sequenze alfanumeriche separate da punti (www.mokabyte.it),
i sistemi operativi per i file prevedono sequenze
alfanumeriche separate da sbarre (dritte o rovesciate
come in \temp\file.txt). Come vedremo in seguito,
questa suddivisione dei nomi con segni di interpunzione
è importante perchè permette di definire
strutture di nomi (tipicamente ad albero). Infatti
per il DNS sappiamo che mokabyte.it e .it stesso sono
due domini (di secondo e di primo livello) che possono
contenere altri nomi (ad esempio mail.mokabyte.it),
così come \temp individua una directory che
può contenere altri file (ad esempio \temp\pippo.doc).
Questa capacità di creare strutture può
essere ovviamente molto utile per gestire database
di nomi complessi.
Chiariti
questi primi concetti base, parliamo dell'operazione
di associazione dei nomi, che si chiama binding. Per
effettuare un binding avremo bisogno, in base a quanto
detto finora, di un oggetto da registrare e da un
nome con una sintassi valida. Parlando ad oggetti,
avremo a disposizione un metodo bind() più
o meno come il seguente:
MyObject
myObject = new MyObject(...);
String name = "my.name";
something.bind(name, myObject);
Non
è ancora chiaro, a questo punto, cos'è
something. Si tratta di un qualche intermediario che
ci permette di accedere al software che implementa
effettivamente il servizio di naming - nel prossimo
paragrafo vedremo di cosa si tratta. Ora invece vogliamo
approfondire cosa succede a myObject. Viene memorizzato
da qualche parte, nella sua versione originale o in
copia? Parrebbe di sì, perché l'operazione
complementare a bind() è chiamata lookup()
e ci permette di recuperare l'oggetto dato il nome:
MyObject
myObject = (MyObject)something.lookup("my.name");
Sembrerebbe
qualcosa di molto simile a quello che facciamo con
le hashtable:
Hashtable
table = new Hashtable();
MyObject myObject = new MyObject(...);
table.put("my.name", myObject);
...
myObject = (MyObject)table.get("my.name");
dove
effettivamente la hashtable memorizza l'oggetto stesso
che abbiamo creato e ce lo restituisce in un secondo
momento.
In
realtà i servizi di naming non funzionano proprio
in questo modo. In certi casi è troppo dispendioso
memorizzare tutto l'oggetto e l'opzione migliore è
quella di salvare solo un numero ridotto di informazioni
sufficienti per ricostruirlo (in certi casi addirittura
non vogliamo recuperare l'oggetto originale, ma un
oggetto ad esso associato; quest'ultimo caso sembra
piuttosto strano, ma chi conosce i concetti di stub
e skeleton di RMI o CORBA ha probabilmente capito
di cosa stiamo parlando - in ogni caso approfondiremo
il concetto più avanti).
In
queste circostanze si dice che non è l'oggetto
ad essere memorizzato nel sistema di naming, ma solo
un suo puntatore o una reference. La reference può
contenere un indirizzo (address), cioè informazione
utile per recuperare l'oggetto originale.
Ora
riconcentriamoci sul something di prima. È
qualcosa che può contenere parecchie diverse
associazioni nome-oggetto, e gli diamo il nome di
context. Questo perché possono esistere tanti
diversi servizi di naming completamente indipendenti
tra loro, che tuttavia possono essere usati contemporaneamente
(è del tutto normale che il nostro computer
usi il DNS contemporaneamente al filesystem durante
le sue elaborazioni). Se vogliamo costruire contesti
gerarchici, possiamo associare un nome non direttamente
ad un oggetto, ma ad un sottocontesto (subcontext).
Ad esempio, \temp non rappresenta un oggetto ma un
sottocontesto, cioè un contenitore di altri
oggetti, così come il dominio di primo livello
.it.
Per
concludere con la terminologia, definiamo un naming
system un insieme di contesti collegati tra loro e
un namespace il relativo insieme di nomi (validi).
Il Context
Dal momento che il nostro something di
prima ha acquisito la denominazione di contesto, è
logico aspettarsi che in Java(TM) esso sia rappresentato
da una classe di nome Context. Riscriviamo il nostro
esempio:
import
javax.naming.*;
...
Context context = new InitialContext();
MyObject myObject = new MyObject(...);
context.bind("my.name", myObject);
...
myObject
= (MyObject)context.lookup();
InitialContext
merita ulteriori attenzioni: che tipo di contesto
stiamo usando? DNS? Filesystem Unix? Possiamo avere
a che fare con differenti servizi, differenti software
di implementazione... come possiamo collegarci con
essi? Java(TM) ovviamente non vuole che ci preoccupiamo
dei dettagli implementativi, e ci offre un'interfaccia
uniforme per tutti queste possibilità. Così
come JDBC(TM) ci permette di eseguire SQL su un qualsiasi
database, anche JNDI ci permette di effettuare operazioni
di naming su un qualsiasi service provider: i dettagli
implementativi sono nascosti dentro una struttura
di driver. Per il nostro InitialContext() è
necessario specificare quindi il nome del driver (e
probabilmente qualche parametro per consentire la
sua inizializzazione). Queste informazioni possono
essere definite come proprietà di sistema Java(TM),
per esempio:
java.naming.factory.initial
com.sun.jndi.fscontext.RefFSContextFactory
java.naming.provider.url file:/temp/
In
questo caso stiamo usando un driver che memorizza
le informazioni di binding sul filesystem locale,
e il parametro URL permette di specificare in quale
directory esse verranno memorizzate. Teniamo presente
questo driver, perché lo useremo tra poco (è
il modo più semplice per fare conoscenza con
JNDI perché non richiede di configurare altro
software). Con altri service provider di naming che
prevedono l'esistenza di un server di rete, la seconda
proprietà serve proprio a definirne l'indirizzo
e a permetterne il collegamento.
Visto
che sono definite come proprietà di sistema,
tutti gli InitialContext che creiamo saranno configurati
allo stesso modo. Questo può essere comodo,
ed inoltre possiamo decidere di cambiare configurazione
senza toccare il codice.
java
-Djava.naming.factory.initial=
com.sun.jndi.fscontext.RefFSContextFactory
-Djava.naming.provider.url=file:/temp/
myPackage.MyClass
Se
vogliamo inizializzare esplicitamente in modo diverso
ogni InitialContext, possiamo passare come parametro
del costruttore un Hashtable con dentro le proprietà
richieste:
Hashtable
props = new Hashtable();
props.put("java.naming.factory.initial",
"com.sun.jndi.fscontext.RefFSContextFactory");
props.put("java.naming.provider.url", "file:/temp/");
Context context = new InitialContext(props);
Oltre
alle citate operazioni di bind() e lookup() altri
metodi ci permettono di investigare sui contenuti
di un contesto. listBindings() restituisce un'enumerazione
di oggetti Binding, una classe aggregato che contiene
il nome dell'oggetto, il nome della sua classe e l'oggetto
stesso:
for
(NamingEnumeration e = context.listBindings(...);
e.hasMore(); ){
Binding binding = (Binding)e.next();
System.out.println("Name:
" + binding.getName() + " class: "
+
binding.getClassName()
+
"
object: " + binding.getObject());
}
Notare che non abbiamo specificato il parametro di
listBindings(); in generale sarà un nome valido
nel namespace, che però deve essere associato
ad un sottocontesto o contenere dei caratteri jolly.
Un'operazione
simile è list(), che però restituisce
un'enumerazione di NameClassPair. Quest'ultima classe
è una superclasse di Binding e non contiene
il metodo getObject(), ma solo le informazioni sul
nome e sul nome della classe. Non recuperando l'oggetto,
list() è in generale meno dispendiosa di listBindings().
Quando
si usano i contesti, dobbiamo ricordarci che essi
non sono thread-safe: solo un thread per volta può
usare una singola istanza di Context (non ci sono
problemi se si usano due differenti istanze che si
riferiscono allo stesso service provider). Con "uso"
si intende anche l'uso di NamingEnumeration e oggetti
da essi estratti: finchè essi non sono stati
"chiusi", il contesto in questione non può
essere usato da thread differenti.
Il
primo esempio
Mettendo insieme quello che abbiamo imparato finora
siamo in grado di scrivere il seguente esempio:
import
javax.naming.*;
import java.util.Hashtable;
public
class Example1
{
public static void main(String[] args)
{
String name = args[0];
try
{
Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory");
env.put(Context.PROVIDER_URL, "file:/temp");
Context context = new InitialContext(env);
Object obj = context.lookup(name);
System.out.println(name + " is bound to: "
+ obj +
"(" + obj.getClass() + ")");
for (NamingEnumeration e = context.listBindings(name);
e.hasMore(); )
{
Binding binding = (Binding)e.next();
System.out.println("Name: " + binding.getName()
+
" class: " + binding.getClassName() +
" object: " + binding.getObject());
}
context.close();
}
catch (NamingException e)
{
System.err.println("Problem looking up "
+ name + ": " + e);
e.printStackTrace();
}
}
}
Nota: con il JDK1.3, le classi di JNDI sono incluse
nel classpath, ma è necessario procurarsi il
driver per il service provider. Per procurarvelo,
andate all'indirizzo http://java.sun.com/products/jndi
e cercate il FSContext Service Provider (per motivi
di copyright non possiamo renderlo disponibile su
questo sito). Il software contiene un paio di .jar
che vanno semplicemente installati nel classpath.
Supponendo
che la directory c:\temp\jndi contenga i seguenti
file:
Directory
di c:\temp\jndi
12/12/2001
00.16 <DIR> .
12/12/2001 00.16 <DIR> ..
11/12/2001 23.53 <DIR> doc
11/12/2001 23.53 <DIR> lib
27/06/2000 00.24 3.305 COPYRIGHT
12/12/2001 00.12 22 c.bat
12/12/2001 00.15 65 r.bat
12/12/2001 00.11 176 run.bat
12/12/2001 00.15 1.845 Example1.class
12/12/2001 00.15 1.099 Example1.java
27/06/2000 00.24 1.969 README-FS.txt
11/12/2001 23.53 99.925 fscontext1_2beta3.zip
ecco
cosa produce il nostro applicativo:
C:\temp\jndi>java
-cp .;lib\providerutil.jar;lib\fscontext.jar Example1
jndi
jndi is bound to: com.sun.jndi.fscontext.RefFSContext@712c4e(class
com.sun.jndi.fscontext.RefFSContext)
Name: c.bat class: java.io.File object: C:\temp\jndi\c.bat
Name: COPYRIGHT class: java.io.File object: C:\temp\jndi\COPYRIGHT
Name: doc class: com.sun.jndi.fscontext.RefFSContext
object: com.sun.jndi.fscontext.RefFSContext@360be0
Name: Example1.class class: java.io.File object: C:\temp\jndi\Example1.class
Name: Example1.java class: java.io.File object: C:\temp\jndi\Example1.java
Name: fscontext1_2beta3.zip class: java.io.File object:
C:\temp\jndi\fscontext1_2beta3.zip
Name: lib class: com.sun.jndi.fscontext.RefFSContext
object: com.sun.jndi.fscontext.RefFSContext@45a877
Name: r.bat class: java.io.File object: C:\temp\jndi\r.bat
Name: README-FS.txt class: java.io.File object: C:\temp\jndi\README-FS.txt
Name: run.bat class: java.io.File object: C:\temp\jndi\run.bat
Come
si può vedere, la lookup() di "jndi"
(l'argomento passato sulla riga di comando) produce
un RefFSContext, un oggetto interno al driver che
rappresenta un contesto, e i bindings contengono tutti
i file presenti nella directory (ad ogni nome è
stato mappato un oggetto di tipo java.io.File).
Registrazione
di oggetti
Concludiamo questa prima puntata vedendo quale strategia
va seguita per registrare nel contesto un nostro oggetto
custom. Prima di tutto vediamo com'e' fatto il nostro
oggetto MyObject:
import
javax.naming.*;
public
class MyObject implements Referenceable
{
String value;
public MyObject (String v)
{
value = v;
}
public Reference getReference()
throws NamingException
{
return new Reference(MyObject.class.getName(),
new StringRefAddr("value", value),
MyObjectFactory.class.getName(),
null); // factory location
}
public String toString()
{
return value;
}
}
L'idea è quella di costruire un oggetto con
un singolo attributo, una stringa 'value'. Per poter
registrare istanze di questa classe con bind(), dobbiamo
implementare l'interfaccia Referenceable: il nostro
oggetto deve prevedere l'esistenza di una reference
che punti ad esso. Referenceable ci obbliga a definire
il metodo getReference(), che deve costruire la Reference
richiesta. Questa richiede quattro parametri:
- il
nome della classe dell'oggetto;
- un
indirizzo della reference, nel quale mettiamo il
valore degli attributi di MyObject;
- l
nome di una classe di factory per il nostro oggetto
(dobbiamo ancora vedere com'e' fatta)
- la
URL dove è reperibile la factory (nel nostro
caso null, la factory sarà reperibile in
locale)
- La
factory verrà richiamata dal JNDI per ricostruire
l'oggetto originale a partire dalle informazioni
memorizzate nella reference:
import
javax.naming.*;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
public
class MyObjectFactory implements ObjectFactory
{
public MyObjectFactory()
{
}
public Object getObjectInstance (Object obj, Name
name, Context ctx, Hashtable env)
throws Exception
{
if (obj instanceof Reference)
{
Reference ref = (Reference)obj;
if (ref.getClassName().equals(MyObject.class.getName()))
{
RefAddr addr = ref.get("value");
if (addr != null)
return new MyObject((String)addr.getContent());
}
}
return null;
}
}
Il
metodo getObjectInstance() riceve come parametro la
reference precedentemente ottenuta, dalla quale è
possibile ottenere l'address precedentemente memorizzato.
A questo punto abbiamo tutte le informazioni necessarie
per ricostruire un'istanza uguale all'oggetto originario
(notate che non stiamo restituendo l''istanza effettivamente
registrata, ma ne creiamo una nuova).
Ecco
l'output del nostro programma:
C:\temp\jndi>java
-cp .;lib\providerutil.jar;lib\fscontext.jar
Example2 my info
Come
vedete viene correttamente stampato un oggetto con
lo stesso valore di quello originario.
Vale
la pena di dare un'occhiata ad un file che è
stato creato nella directory c:\temp:
C:\temp\jndi>type
c:\temp\.bindings
#This file is used by the JNDI FSContext.
#Wed Dec 12 00:52:09 GMT+01:00 2001
myname/RefAddr/0/Encoding=String
myname/FactoryName=MyObjectFactory
myname/RefAddr/0/Content=my info
myname/RefAddr/0/Type=value
myname/ClassName=MyObject
Questo
file contiene le informazioni persistenti relative
alla mappatura dell'oggetto (possiamo ritrovare i
dati inseriti dal nostro programma). Va sottolineato
come l'oggetto rimanga mappato sul sistema anche DOPO
la terminazione del programma. Se infatti rilanciamo
l'eseguibile:
C:\temp\jndi>java
-cp .;lib\providerutil.jar;lib\fscontext.jar Example2
Operation failed: javax.naming.NameAlreadyBoundException:
myname
otteniamo
un'eccezione che conferma che il nome è già
legato ad un oggetto. A questo punto dovremmo eliminarlo
con context.unbind("myname"); un altra prova
che vale la pena effettuare è commentare la
bind() e verificare che il programma è in grado
di recuperare l'oggetto direttamente con una lookup()
Conclusione
Per
questa puntata abbiamo concluso: siamo stati in grado
di costruire i nostri primi esempi. Per semplicità
abbiamo considerato un service provider che usa il
filesystem locale per memorizzare le informazioni
sugli oggetti registrati: nei prossimi articoli vedremo
come service provider più sofisticate permettono
di effettuare la stessa operazione in rete: su un
singolo nodo si potrà registrare un oggetto
e su tutti gli altri nodi sarà possibile recuperarlo.
E più avanti esploreremo i servizi di directory,
che aggiungeranno nuove potenzialità.
Esempi
Scarica gli esempi descritti
in questo articolo
Fabrizio
Giudici si occupa di Java dal 1995, anno in cui
questa tecnologia e' stata lanciata da Sun Microsystems.
Ha collaborato fino al 1997 con l'Universita' di Genova,
presso la quale ha conseguito il Dottorato di Ricerca
in Ingegneria Elettronica ed Informatica. Ha iniziato
a collaborare con Mokabyte nel Settembre 1996; fino
all'estate del 2001 e' stato partner di un'azienda
operante nel campo dell'integrazione di sistemi; attualmente
opera come freelance. In questi anni si e' occupato
di sviluppo, progettazione e definizione di architetture
in diversi campi, dai sistemi informativi bancari
alle telecomunicazioni. Fabrizio Giudici e' istruttore
Java per conto di Sun Microsystems Italia sin dal
1998, e le sue competenze coprono praticamente tutto
lo spettro delle tecnologie Java, da Swing agli EJB,
passando per la recente Java 2 Micro Edition. Attualmente
sta preparando un nuovo sito, www.tidalwave.it, che
sara' dedicato alla distribuzione di risorse, documenti,
progetti di pubblico dominio in Java. TidalWave sara'
pronto presumibilmente per l'inizio del mese di marzo
2002. Puo' essere contattato all'indirizzo Fabrizio.Giudici@tidalwave.it.
|