MokaByte 59 - Gennaio 2002 
foto dell'autore non disponibile
Corso JNDI
I parte: introduzione a JNDI
di
Fabrizio
Giudici
JNDI, la API Java(TM) per i servizi di Naming e Directory, è forse una delle più sottovalutate. Citata soprattutto nell'ambito di Java 2 Enterprise Edition (J2EE(TM)), dove gioca un ruolo fondamentale per la gestione di risorse e degli EJB, spesso viene ridotta alla citazione delle due operazioni più ricorrenti, bind() e lookup(). Con questo articolo iniziamo a parlare dei fondamentali di JNDI, che approfondiremo nei suoi usi più avanzati nelle prossime puntate.

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:

  1. il nome della classe dell'oggetto;
  2. un indirizzo della reference, nel quale mettiamo il valore degli attributi di MyObject;
  3. l nome di una classe di factory per il nostro oggetto (dobbiamo ancora vedere com'e' fatta)
  4. la URL dove è reperibile la factory (nel nostro caso null, la factory sarà reperibile in locale)
  5. 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.

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it