Advanced Java
Advanced Java
0 Novembre 2012
Sommario
Sommario.........................................................................................................................................................1
Introduzione.....................................................................................................................................................8
Ambiente software e tool.............................................................................................................................8
Analisi delle proprietà di sistema e variabili di ambiente.................................................................................9
Introduzione alla Java Reflection......................................................................................................................9
Uso delle annotazioni e reflection..............................................................................................................14
C’è un baco in URLClassLoader ?............................................................................................................15
Uso della reflection nella classe ServerSistema..........................................................................................16
Caricamento delle classi e uso dell’opzione –Xbootclasspath........................................................................18
Il caricamento delle classi in Java...............................................................................................................18
Il caricamento delle classi per istanziazione...............................................................................................19
Uso dell’opzione non standard –Xbootclasspath........................................................................................21
Accesso programmatico al JAR manifest........................................................................................................24
Scrittura del manifest.................................................................................................................................24
Lettura del manifest...................................................................................................................................25
Uso combinato della lettura del manifest e dell’opzione –Xbootclasspath/p.........................................27
Manifest di MyCorbaWorld e modifiche all’oggetto ServerSistema.......................................................28
Serializzazione di oggetti Java.........................................................................................................................29
Approccio programmatico alla serializzazione: l’applicazione Serial..........................................................30
Subtipi della classe OutputStream e codice di serializzazione................................................................31
Ereditarietà della serializzabilità di una classe....................................................................................31
Dump degli oggetti serializzati e scrittura permanente......................................................................32
Approccio programmatico alla deserializzazione: l’applicazione Deserial..................................................33
Subtipi della classe InputStream e codice di deserializzazione...............................................................34
Limitazioni alla deserializzabilità.............................................................................................................34
Il protocollo di serializzazione.....................................................................................................................35
Dettagli sulla serializzazione dell’istanza di una classe...........................................................................35
Descrizione della classe......................................................................................................................37
Descrizione del supertipo...............................................................................................................37
Descrizione dei campi di una classe................................................................................................38
Descrizione dello stato dell’istanza serializzata..................................................................................38
Serializzazione di tipi primitivi................................................................................................................39
Serializzazione nativa di un tipo primitivo..........................................................................................39
pag. 1
Advanced Java rev. 1.0 Novembre 2012
pag. 2
Advanced Java rev. 1.0 Novembre 2012
pag. 3
Advanced Java rev. 1.0 Novembre 2012
pag. 4
Advanced Java rev. 1.0 Novembre 2012
pag. 5
Advanced Java rev. 1.0 Novembre 2012
pag. 6
Advanced Java rev. 1.0 Novembre 2012
pag. 7
Advanced Java rev. 1.0 Novembre 2012
Introduzione.
Questo documento descrive un’odissea attraverso il mondo Java; il viaggio procede per tematiche: alcune
sono intrinseche al mondo Java, fanno parte del cuore del framework, come la reflection; altre tematiche
riguardano campi applicativi, che però hanno grande importanza nel mondo Java.
Il linguaggio utilizzato è Java, versione 1.7.0_07 JavaEE (Enterprise Edition); l’ho installato tramite il
pacchetto di installazione java_ee_sdk-[Link], usando le scelte default: così facendo, Java
risulta installato nella directory C:\glassfish3\jdk7.
Come strumento indipendente, non compreso in altri pacchetti (cioè not embedded), ho installato Apache
ant, versione 1.8.4. Ho definito la variabile d’ambiente, di sistema, ANT_HOME = C:\apache-ant-1.8.4, e ho
aggiunto <ANT_HOME>/bin alla variabile d’ambiente path.
Ho usato Eclipse, nella versione Eclipse Java EE IDE for Web Developers, versione Juno SR1. Per
installarlo, bisogna scaricare il pacchetto [Link], quindi unzippare sotto C:\, così
viene creata la directory C:\eclipse, con alcune sottodirectory.
... ...
--[Link] --[Link]
256m 256m
--[Link] -vm
C:/glassfish3/jdk7/bin/[Link]
openFile openFile
-vmargs -vmargs
-[Link]=1.5 -[Link]=1.7
-Xms40m -Xms40m
-Xmx384m -Xmx512m
Una volta installato Eclipse, lo lancio e scelgo Help Install new software e controllo la voce: What is
already installed ?
pag. 8
Advanced Java rev. 1.0 Novembre 2012
Trovo che l’unico plugin installato è Eclipse IDE for Java EE Developers.
Volendo creare applicazioni grafiche, Swing e SWT, bisogna installare i plugin appositi. Per farlo, scelgo
HelpInstall new software, come prima, Work with = All available sites, e dalla lista che si presenta scelgo
General purpose tools, di qui seleziono Swing Designer e SWT Designer, e li installo: con ciò, Eclipse Juno è
pronto per sviluppare applicazioni grafiche, Swing o SWT che siano.
Come web application server, viene utilizzato Apache Tomcat 7.0, corredato di axis2 come libreria di
implementazione delle web service.
Ho installato Tomcat usando il pacchetto [Link]; ho seguito tutti i default, tranne che per
la porta principale, al posto del default 8080 ho assegnato 8081.
Per installare Apache axis2, ho scaricato il pacchetto [Link] (è la standard binary distribution,
che non necessita compilazione), e ho unzippato sotto C:\, ottenendo così la directory C:\axis2-1.6.1. Ho
definito la variabile d’ambiente, di sistema, AXIS2_HOME = C:\axis2-1.6.1.
Per costruire [Link], apro un command prompt e mi posiziono in <AXIS2_HOME>/webapp, ove si trova
[Link], e do il comando ant [Link]. Così facendo, viene creato [Link] entro la directory
<AXIS2_HOME>/dist.
Copio il file [Link] entro la directory webapps di Tomcat, con Tomcat running: automaticamente Tomcat
crea la directory axis2 entro webapps.
Come database, viene utilizzato Oracle Express 10g. Dopo l’installazione, il TNS name è XE.
Sono presenti tre utenti/database: Libreria, DBPeople e il sample database predefinito HR.
Ho creato la directory Drivers entro oraclexe, contenente [Link], usato per l’accesso JDBC dal linguaggio
Java, JDK 1.6 o superiore.
Come software di implementazione di Corba, viene usato Orbacus, versione 4.3.4 per Java; ogni dettaglio
inerente Orbacus è fornito in un apposito documento.
Questo è ciò che viene fatto in maniera generalizzata nell’applicazione AppReflex; non studierò
l’applicazione in toto, ma studierò la sua gestione della reflection.
Il sistema di caricamento di una classe della quale si conosce il nome cambia, a seconda che la classe sia
locale ossia già presente nell’insieme di classi (JAR) acceduto di default dalla JVM, oppure la classe sia
esterna ossia si trovi in un JAR esterno, non acceduto di default dalla JVM 1. L’applicazione AppReflex
permette il caricamento in entrambi i casi.
1
Non bisogna confondere tra ricerca e caricamento: la JVM carica solamente le classi che servono, quando
servono, e le cerca entro i JAR che si trovano in un certo default path; finchè non c’è necessità, la JVM non
carica una classe che pure si trova in un JAR del default path.
pag. 9
Advanced Java rev. 1.0 Novembre 2012
Il codice che permette il caricamento di una classe locale, partendo dal suo nome, è il seguente:
try
{
Class cls = [Link](nomeClasse);
}
catch (ClassNotFoundException e)
{
<segnalazione dell’errore>
}
ove nomeClasse è una variabile di tipo String; la classe Class appartiene al package [Link].
Posso caricare una classe appartenente a [Link], come Empl che appartiene al package rem,
assegnando nomeClasse = “[Link]”, ma posso caricare anche una classe non appartenente a
[Link] ma comunque situata (in un JAR) sul default path della JVM, ad esempio la classe NTSystem,
appartenente a [Link]: nomeClasse = “[Link]”.
Il default path in cui la JVM cerca le classi comprende sicuramente il classpath, rappresentato dalla proprietà
[Link], ma comprende anche altri percorsi; i dettagli variano tra le diverse installazioni.
Programmaticamente, il default path comprende sia il classpath, sia la lista dei JAR elencati dalla proprietà
[Link] (tra essi, c’è sempre [Link]), sia i JAR presenti nelle directory elencate nella proprietà
[Link]. L’argomento sarà approfondito e chiarito nel seguito, trattando i class loader.
Osservazione importante: quando l’oggetto della JVM è un JAR e non una singola classe, il classpath è
posto automaticamente uguale al JAR oggetto; così, dato il comando java –jar [Link], il valore della proprietà,
quindi il classpath, è il seguente: [Link] = [Link], qualsiasi diversa assegnazione del classpath prima
del lancio viene ignorata.
Adesso considero una classe esterna, ad esempio la classe Complex, appartenente al package num,
contenuta in LibJar/[Link]. Per caricarla, devo prima di tutto indirizzare correttamente il JAR file; nel caso in
cui si trovi sul disco locale, come Uti, posso scrivere String urlJAR =
"file:/C:/Libjar/[Link]".
Una volta che una classe è stata caricata, è possibile studiarne i costruttori, i metodi e gli altri membri; come
esempio, uso ancora la classe [Link] della libreria [Link]. Ecco il codice della classe [Link]:
public class Complex
{
private double real,img;
public Complex() { real = img = 0; }
public Complex(double r,double i) { real = r; img = i; }
public double getReal() { return real; }
public double getImg() { return img; }
public void setReal(double r) { real = r; }
public void setImg(double i) { img = i; }
private String format(double x) { ... }
pag. 10
Advanced Java rev. 1.0 Novembre 2012
Posso conoscere i costruttori della classe caricata, e i tipi dei parametri di ciascuno di essi, mediante il
codice seguente, in cui Constructor appartiene al package [Link]:
Constructor[] ctors = [Link]();
[Link]("Trovati " + [Link] +" costruttori");
for (Constructor ctor : ctors) [Link]([Link]());
È possibile recuperare i tipi dei parametri in modo programmatico (cioè, non solo visualizzarli): dato un
costruttore ctor, il codice Type[] ts = [Link]() recupera l’array dei tipi
dei parametri del costruttore, nell’ordine di apparizione.
Se il costruttore che si intende usare è senza parametri, scrivo Object ist = [Link](),
ovviamente non posso definire ist usando il suo tipo, Complex, quindi lo definisco genericamente come
Object (se sapessi già del tipo Complex, non seguirei questa strada). Alternativamente, se ctor rappresenta il
costruttore senza parametri, posso scrivere Object ist = [Link](); le due strade non
sono esattamente coincidenti, tuttavia sono entrambe percorribili.
I valori da passare devono essere del tipo giusto; ad esempio, usando come ctor il costruttore
Complex(double,double), posso scrivere quanto segue:
Object[] vals = new Object[2];
vals[0] = -2.3;
vals[1] = 3.15;
Object c = [Link](vals);
[Link](c);
Anche vals[0] = -Math.E e vals[1] = [Link] costituiscono una valida parametrizzazione, oppure
vals[0] = 1 e vals[1] = 2 perché essendo interi possono fungere da double; invece scrivendo
vals[0] = 1 e vals[1] = true oppure vals[0] = "1" e vals[1] = 2 ho eccezioni runtime.
Per conoscere i metodi della classe caricata, uso come esempio il codice seguente:
Method[] ms = [Link]();
[Link]("Trovati " + [Link] + " metodi");
for (Method m : ms) [Link]([Link]());
pag. 11
Advanced Java rev. 1.0 Novembre 2012
È possibile recuperare i tipi dei parametri in modo programmatico (cioè, non solo visualizzarli): dato un
metodo m, il codice Type[] ts = [Link]() recupera l’array dei tipi dei
parametri del metodo, nell’ordine di apparizione.
Vedo adesso come possono essere invocati i metodi; c’è differenza tra metodi con o senza parametri, e
comincio dall’invocazione di uno di questi ultimi.
Costruisco come sopra un’istanza c di [Link], rappresentabile come -2.3+i3.15; poi invoco su
essa il metodo module e osservo il risultato:
Method mm = [Link]("module");
Object res = [Link](c);
[Link]("modulo(c)="+res);
Si noti che ho assegnato il risultato dell’invocazione a un oggetto generico; avrei potuto essere più preciso,
studiando il tipo del valore di ritorno del metodo, ad esempio ottenendo il tipo di ritorno mediante l’istruzione
Type rt = [Link]()), e poi facendo uno switch sui nomi dei tipi fino a trovare quello giusto,
cioè “double”, a questo punto avrei potuto scrivere double result = (Double) [Link](c).
Il motivo per cui preferisco usare un generico Object è dovuto al fatto che non sempre ho a disposizione il
tipo di ritorno: questo è il caso dell’esempio seguente.
Segue il codice – si ricordi che autoSum ha due parametri di tipo double in input e restituisce Complex:
Class[] pars = new Class[2];
pars[0] = pars[1] = [Link];
Method mm = [Link]("autoSum",pars);
Object[] vals = new Object[2];
vals[0] = 3.3;
vals[1] = -0.15;
Object cc = [Link](c, vals);
[Link]("cc="+cc);
Innanzi tutto, dato un certo metodo, è possibile stabilire se esso sia statico oppure no. La classe Complex
non ha metodi statici, quindi gliene aggiungo un paio:
public static Complex GeneraCasuale()
public static String HexExpr(Complex c)
il primo è un metodo senza parametri, che genera un numero complesso avente come parti due interi di
valore tra 0 e 50 scelti randomicamente; il secondo metodo ha un parametro in ingresso, un complesso, del
quale fornisce, come stringa, una specie di rappresentazione esadecimale.
pag. 12
Advanced Java rev. 1.0 Novembre 2012
Per appurare programmaticamente la natura statica di un metodo, è possibile usare il seguente codice, nel
quale si suppone che m sia un oggetto di tipo Method, quindi rappresentativo del metodo di una classe:
La chiamata di un metodo statico, con o senza parametri, è analoga a quella di un metodo non statico, a
parte il fatto che nell’invocazione si passa un null al posto dell’istanza dell’oggetto.
Mostro un esempio di codice, in cui cls è la classe di Complex: prima viene invocato il metodo senza
parametri GeneraCasuale, poi il metodo HexExpr ad un parametro:
Method m = [Link]("GeneraCasuale");
Object oc = [Link](null);
[Link]("Complesso casualmente generato: "+oc);
Class<?>[] pars = new Class<?>[1];
pars[0] = cls;
m = [Link]("HexExpr",pars);
Object[] vals = new Object[1];
vals[0] = oc;
Object os = [Link](null, vals);
[Link]("Rappresentazione esadecimale di "+oc+": "+os);
pag. 13
Advanced Java rev. 1.0 Novembre 2012
I campi privati vengono mostrati, ma non sono poi accessibili, solamente i campi pubblici sono accessibili.
si noti il carattere @ davanti alla keyword interface, per contraddistinguere l’interfaccia come
annotazione.
non serve dichiarare pubblici i campi, avendo dichiarato pubblica l’annotazione che li contiene.
La clausola Retention ha tre possibili valori di policy: CLASS (il default), SOURCE e RUNTIME; se voglio
poter ritrovare l’annotazione a runtime accedendo le classi o i metodi sui quali l’ho applicata, allora devo
scegliere [Link], in caso contrario essa risulta null.
Creo anche le annotazioni Volatile, di policy CLASS, e Effimera, di policy SOURCE, entrambe con un solo
campo, labile di tipo stringa.
Suppongo di applicare le tre annotazioni alla classe Complex; per farlo, le scrivo in [Link]:
@Remark(autore="Virgilio",commento="okay",revisione=2)
@Volatile(labile="In classe")
@Effimera(labile="In sorgente")
public class Complex
{
...
}
Creo il package prg e in esso la classe [Link], dotata di metodo main; sia il package sia la classe hanno
natura dimostrativa e sono temporanei. Scrivo il codice seguente nel metodo main di Maine:
public static void main(String[] args)
{
Annotation[] anns = ([Link]).getDeclaredAnnotations();
for (Annotation a : anns) [Link](a);
}
mentre il file [Link] è aperto nella IDE di Eclipse, mi sposto col cursore su Complex: la IDE mostra un
tooltip contenente tutte e tre le annotazioni, coi valori assegnati.
Poi compilo il progetto: nel folder build/num trovo [Link]; seleziono [Link] e con il tasto
destro scelgo Open with Class File Viewer, ecco ciò che vedo nelle prime righe:
@[Link](labile="In classe")
@[Link](autore="Virgilio",
pag. 14
Advanced Java rev. 1.0 Novembre 2012
commento="okay",
revisione=(int) 2)
public class [Link] {
………
l’annotazione Effimera è andata persa: le annotazioni con policy SOURCE sono visibili solo nei sorgenti,
infatti la IDE le mostra, ma non sopravvivono alla compilazione e quindi non compaiono nei class file.
solamente le annotazioni con policy RUNTIME sono disponibili a runtime, dato che la JVM scarta le
annotazioni con policy CLASS trovate nei class file.
Le annotazioni possono essere associate a un metodo piuttosto che alla classe in toto, ad esempio posso
scrivere il codice seguente in [Link]:
@Remark(autore="Virgilio",commento="okay",revisione=2)
public class Complex
{
...
@Remark(autore="Virgilio",commento="somma di due complessi",revisione=3)
public Complex autoSum(double r, double i)
{
...
}
...
}
Ecco il frammento di codice nel metodo main della classe Maine, usato per recuperare a runtime le
annotazioni su un metodo, qui autoSum:
Class[] pars = { [Link],[Link] };
Method ts = ([Link]).getDeclaredMethod("autoSum",pars);
anns = [Link]();
for (Annotation a : anns) [Link](a);
Le annotazioni possono essere recuperate con il proprio tipo, e non solo genericamente come ho fatto
finora, ciò permette l’accesso programmatico ai singoli campi delle annotazioni; ecco un esempio:
Remark remc = ([Link]).getAnnotation([Link]);
Class[] pars = { [Link],[Link] };
Method ts = ([Link]).getDeclaredMethod("autoSum", pars);
Remark remm = [Link]([Link]);
[Link]("Commento sulla classe: " + [Link]());
[Link]("Commento sul metodo autoSum: " + [Link]());
ecco l’output:
Commento sulla classe: okay
Commento sul metodo autoSum: somma di due complessi
L’accesso programmatico alle annotazioni avviene solamente in lettura, sono cioè immodificabili.
Uso una versione di Tester che non referenzia né include [Link], ma si serve di URLClassLoader e della
reflection per accederne le classi; in particolare, l’oggetto loader viene usato per caricare la classe cls, quindi
viene chiuso (nel codice è stata omessa la gestione degli errori, per brevità):
URL[] urls = { new URL("file:/C:/LibJar/[Link]") };
pag. 15
Advanced Java rev. 1.0 Novembre 2012
Ecco l’output:
La classe [Link] non ha annotazioni
Complesso casualmente generato: 25
Rappresentazione esadecimale di 25: 0x1.9p4+i0x0.0p0
non è quello che mi sarei aspettato, perché la classe Complex ha un’annotazione.
Ecco l’output:
@[Link](autore=Virgilio, commento=okay, revisione=2)
Complesso casualmente generato: 33+i10
Rappresentazione esadecimale di 33+i10: 0x1.08p5+i0x1.4p3
Evidentemente il problema è la chiusura del loader: la chiusura del loader di una classe impedisce la lettura
delle annotazioni della classe stessa, sebbene i metodi e le altre feature della classe continuino a funzionare
regolarmente, infatti nel mio esempio la classe può essere istanziata e i suoi metodi possono essere
invocati, solamente le annotazioni spariscono.
Se il sistema operativo sul quale è in esecuzione l’oggetto ServerSistema è Windows, allora posso ricavare il
nome dell’utente e il suo dominio di appartenenza, ad esempio ETS/Ravagliv.
Se invece il sistema operativo è Linux, il concetto di dominio non ha senso, mentre ne acquista quello del
gruppo al quale appartiene l’utente (o meglio, i gruppi, perché di solito un utente appartiene a più gruppi); ad
esempio, se l’oggetto remoto è in esecuzione su sccver, trovo user = tms e i gruppi sono quelli aventi
identificativi 16, 33, 100, 1000.
Posso ricavare il nome dell’utente per vie indipendenti dal sistema operativo, ma non posso sapere il
dominio nel caso Windows, o i gruppi nel caso Linux, senza ricorrere a classi specialistiche: nel caso
Windows, c’è la classe [Link] in grado di fornire l’informazione
pag. 16
Advanced Java rev. 1.0 Novembre 2012
il problema è che su Windows le librerie di Java dispongono solamente della classe NTSystem, mentre su
Linux dispongono solamente della classe UnixSystem.
La soluzione a questo problema è l’uso della Java reflection: stabilito a runtime quale sia il sistema operativo
sottostante, il codice del metodo SystemInfo servendosi della reflection istanzia la classe giusta e ne chiama
i metodi adeguati.
Espongo il codice del metodo, omettendo la gestione degli errori e tutte le informazioni ricavate senza
ricorrere all’uso della reflection:
public String[] SystemInfo()
{
Vector<String> infos = new Vector<String>();
String osName = [Link]("[Link]");
Class cls = null;
String methodName1 = "",methodName2 = "";
if ([Link]("Win"))
{
cls = [Link]("[Link]");
methodName1 = "getDomain";
methodName2 = "getName";
}
else if ([Link]("Linux"))
{
cls = [Link]("[Link]");
methodName1 = "getGroups";
methodName2 = "getUsername";
}
else // sistema operativo non previsto
{
String user = [Link]("[Link]");
[Link]("Corba User: "+user);
}
if (cls != null)
{
Object account = [Link]();
Method m1 = null,m2 = null;
m1 = [Link](methodName1);
m2 = [Link](methodName2);
Object res1 = null,res2 = null;
res1 = [Link](account);
res2 = [Link](account);
if ([Link]("Win"))
[Link]("Corba User: "+res1+"/"+res2);
else // Linux
{
long[] ids = (long[]) res1;
String buf = "Corba User: "+res2+" [Group IDs=";
for (int i=0;i<[Link]-1;i++) buf += ids[i]+",";
buf += ids[[Link]-1]+"]";
[Link](buf);
}
}
String[] info = new String[0];
return [Link](info);
}
Viene ricavato in osName il sistema operativo presente: se si tratta di Windows, la variabile cls di tipo Class
carica la classe NTSystem e definisce i nomi dei metodi utili allo scopo, naturalmente metodi della classe
pag. 17
Advanced Java rev. 1.0 Novembre 2012
NTSystem; se invece il sistema operativo è Linux, cls carica la classe di tipo UnixSystem e definisce anche
in questo caso i nomi dei suoi metodi da utilizzare.
Una volta che la classe giusta è stata caricata, bisogna istanziarla: l’istruzione [Link]() crea
l’istanza account, che viene vista come oggetto generico, in quanto non posso fare alcun cast esplicito.
Recupero gli oggetti descrittivi dei metodi, dei quali conosco i nomi, differenti a seconda del sistema
operativo, e compio le invocazioni: anche i risultati vengono trattati a programma come oggetti generici
(res1, res2 sono dichiarati Object); nel caso Linux, è necessaria l’effettuazione di un cast con l’oggetto di
ritorno del metodo [Link], che è di tipo long[].
Le classi caricate dai class loader di cui ai punti a), b) e c) costituiscono l’insieme delle classi locali.
In precedenza, si è visto come caricare una classe a seconda che si trovi in un JAR già caricato dalla JVM,
oppure in un JAR non caricato dalla JVM. Nel primo caso, il class loader che interviene nell’esecuzione
dell’istruzione [Link](nomeClasse) è uno dei tre sopra elencati.
La lista dei JAR acceduti in JRE_DIR/lib dal bootstrap class loader è disponibile programmaticamente
mediante il valore della proprietà [Link]; in generale il valore dipende dall’installazione, ma
comprende sempre [Link], [Link], [Link].
Oltre alla lista di JAR, è solitamente compresa nel valore di [Link], anche la directory
JRE_DIR/classes. Quindi, ogni classe presente in JRE_DIR/classes risulta locale e viene caricata dal
bootstrap class loader: se ad esempio estraggo da [Link] (un JAR accademico prodotto in LibJar) la
classe [Link] e la copio entro JRE_DIR/classes, la classe [Link] risulterà essere classe locale e
verrà caricata dal bootstrap class loader.
la proprietà è utilizzata dalla JVM alla sola partenza, successive modifiche non vengono considerate.
2
JRE_DIR qui equivale al valore della proprietà [Link]; nel caso in cui si usi un JDK, in generale essa è
JDK_DIR/jre, ad esempio glassfish3/jdk7/jre, ma non è garantito, dipende dalla configurazione.
pag. 18
Advanced Java rev. 1.0 Novembre 2012
In teoria, ogni classe, una volta che è stata caricata, fornisce il riferimento al class loader che l’ha caricata
mediante il metodo getClassLoader: ClassLoader clsLoader = [Link]().
Tuttavia nel caso in cui sia stata caricata una classe come StringTokenizer, scopro che clsLoader è null:
questo è sempre così, quando una classe è stata caricata dal bootstrap class loader.
Ciascuna istanza di un class loader ha il metodo getParent, che fornisce il riferimento al class loader che a
sua volta ha caricato l’istanza in questione. Ovviamente non si può applicare alle classi caricate dal bootstap
class loader, essendo clsLoader null. Nel caso del caricamento di ZipCoder, clsLoader è di tipo
[Link]$ExtClassLoader e [Link] è null, infatti il class loader del class loader di
ZipCoder è il bootstrap class loader.
Nel caso b), la proprietà [Link] definisce una lista di directory di estensione, ossia ogni JAR in una
directory di estensione viene considerato locale (cioè, tutte le sue classi sono locali); anche qui, il valore
della proprietà può essere riassegnato alla partenza, come –D [Link]=<lista personalizzata>.
Per quanto concerne il punto c), va ricordato che, nel caso sia stato lanciato un JAR e non una singola
classe, il valore della proprietà [Link] è sempre il path del JAR lanciato, e non è possibile
riassegnarlo tramite l’opzione –D [Link].
In generale tutte le classi che implementano un class loader sono subtipi della classe astratta
[Link], che discende direttamente da Object; è possibile costruire come suoi subtipi dei
class loader applicativi. Comunque il subtipo più importante e versatile è URLClassLoader, è il loader
solitamente usato all’interno del codice applicativo allo scopo di caricare classi contenute in qualche JAR file.
Studio adesso il caso di una classe appartenente a un JAR non caricato dalla JVM, ad esempio penso alla
classe [Link] appartenente a LibJar/[Link]: costruito il class loader loader di
tipo URLClassLoader sul JAR suddetto, carico la classe GestoreCorba con il codice seguente:
Class cls = [Link]("[Link]")
Il suo class loader clsLoader è ovviamente coincidente con loader, quidi è di tipo URLClassLoader; ecco il
resto della catena:
[Link] è di tipo [Link]$AppClassLoader
([Link]).getParent è di tipo [Link]$ExtClassLoader
(([Link]).getParent).getParent è null (bootstrap class loader)
Come case study, sfrutto una versione ad hoc del progetto Tester: il progetto contiene il package app con la
main class [Link], e anche il package alter, contenente la classe Altra.
pag. 19
Advanced Java rev. 1.0 Novembre 2012
{
...
}
}
Su tutte queste istanze, applico il metodo getClass per recuperare la classe, poi alla classe applico il metodo
getClassLoader per recuperare il class loader responsabile del caricamento della classe, ad esempio per
l’istanza altra di tipo [Link] scrivo ClassLoader cla = [Link]().getClassLoader(),
quindi stampo [Link]().getName(), e così via per tutte le altre istanze.
Nel build file di Tester, il target <jarGen> include solamente [Link], un JAR accademico prodotto in LibJar,
contenente la classe Fake nel package lib.
La classe [Link] appartiene a [Link], presente in LibJar, che non è inclusa in [Link] né è caricata
esplicitamente dal codice (in altre parole, non uso alcun URLClassLoader entro Tester), ma viene resa
disponibile lanciando [Link] con l’opzione non standard –Xbootclasspath/a:”C:/LibJar/[Link]”.
L’uso dell’opzione verrà meglio indagato nel seguito, per adesso basti dire che essa consente di aggiungere
i JAR in lista al classpath di bootstrap, cioè all’insieme di JAR acceduti dalla JVM alla partenza.
La classe Pinata del package foo appartiene a [Link], un JAR accademico prodotto in LibJar; [Link]
non è caricato da Tester, ma è reso disponibile venendo copiato nella directory C:/ExtLibs, definita directory
d’estensione, al lancio di [Link].
ove assegno la proprietà [Link], in modo che comprenda, oltre al path default, anche il percorso
C:/ExtLibs, che quindi diventa directory d’estensione. Qui ipotizzo che %JAVA_HOME%/jre abbia lo stesso
valore della proprietà [Link], il che non è affatto garantito, oppure che %JAVA_HOME%/jre sia
comunque la directory d’estensione da accedere.
pag. 20
Advanced Java rev. 1.0 Novembre 2012
Le prime quattro classi sono tutte interne a [Link]; siccome il valore della proprietà [Link] è
[Link], allora tutte le classi di [Link] vengono caricate da [Link]$AppClassLoader,
cioè dal system class loader, in accordo con quanto stabilito al punto c).
I risultati per le variabili di tipo [Link] e [Link] sono in accordo col punto a), dato che la
classe String appartiene a [Link], mentre l’opzione –Xbootclasspath/a fa sì che [Link] venga trattato come un
JAR di sistema, perciò sia String che [Link] sono classi di sistema.
È anche importante la forma –Xbootclasspath/a:<lista di JAR file>, che pospone i JAR file in lista (separati
dal carattere ‘;‘ se più d’uno) al classpath di bootstrap.
La differenza tra le due forme dell’opzione si manifesta quando i JAR presenti in lista contengono classi
omonime di quelle presenti nei JAR che la JVM accede di default alla partenza; ad esempio, suppongo che
Orbacus/lib/[Link] sia uno dei JAR in lista: allora, scrivendo –Xbootclasspath/p, alla partenza
dell’applicazione, quando si richiede il caricamento della classe [Link], viene
usata la classe [Link] di Orbacus e non l’omonima presente in [Link];
scrivendo –Xbootclasspath/a, accade il contrario.
La forma semplice, –Xbootclasspath:<lista di JAR file>, rimpiazza il classpath di bootstrap con i JAR in lista,
come tale è alquanto “estrema” e quindi di utilizzo meno frequente.
Si è già visto l’uso dell’opzione –Xbootclasspath/p a proposito di Corba, ora ne studierò un caso più
personale, anche se totalmente accademico: voglio rimpiazzare la classe [Link] contenuta
in [Link] con una classe omonima da me creata, contenuta in [Link].
Creo il package [Link] entro il progetto Uti, e in esso creo la mia classe StringTokenizer, subtipo di
[Link] come l’originale. La classe originale implementa l’interfaccia Enumerable<Object>, in prima
battuta io non la implemento.
Utilizzerò Tester per provare la mia classe StringTokenizer, partendo da una versione minimale della classe
Prg: le classi Prg e StringTokenizer evolveranno insieme.
Inizialmente Prg consiste del solo main, contenente solamente il codice StringTokenizer st = null.
Se la mia classe StringTokenizer non ha almeno tutti i metodi pubblici dell’originale, ci sono errori; ma anche
se li ha tutti, con tutte le giuste signature, ma con implementazioni vuote o troppo semplici (ad esempio
countTokens che ritorna 0), non c’è funzionamento, nemmeno minimale come quello richiesto dalla forma
minimalistica di Prg.
Questo succede perché il bootstrap class loader non si limita a caricare il codice della classe entro la JVM. Il
bootstrap class loader opera in tre fasi: loading, linking e initializing. Il loading è il caricamento del codice
binario della classe entro la JVM; il linking incorpora il codice caricato nello stato runtime della JVM;
l’initializing è l’esecuzione del codice (statico) che compie l’inizializzazione del tipo.
In sintesi, la classe sostitutiva non può essere troppo lontana dalla classe originale.
Allora scrivo una classe StringTokenizer con qualche parvenza di realismo; anche se la classe sostitutiva
funziona diversamente dall’originale, tuttavia presenta analogie:
package [Link];
pag. 21
Advanced Java rev. 1.0 Novembre 2012
Scrivo anche una classe Prg più impegnativa: aggiungo alla classe il metodo statico leggiBuffer, che
restituisce la stringa digitata a tastiera, e riscrivo il metodo main come segue:
public static void main(String[] args)
{
[Link]("Passa: ");
String bu = leggiBuffer();
StringTokenizer st = null;
if ([Link]("x")) st = new StringTokenizer(bu,"x");
else st = new StringTokenizer(bu);
int nTokens = [Link]();
[Link]("nTokens = "+nTokens);
do
{
String f = (String) [Link]();
[Link](f);
}
while([Link]());
}
pag. 22
Advanced Java rev. 1.0 Novembre 2012
d
u
e
Posso riscrivere i metodi derivati da Object, ad esempio reimplemento il metodo toString nella mia classe:
@Override
public String toString()
{
return "I'm a fake StringTokenizer";
}
d
u
e
I’m a fake StringTokenizer
La classe sostitutiva può implementare metod pubblici assenti nella classe originale, ad esempio:
public String soloMio()
{
return [Link]()+"-"+[Link]();
}
Chiamare il metodo soloMio dal codice di Prg è però arduo: il metodo soloMio risulta sconosciuto in fase di
compilazione, scriverlo genera errori in fase di compilazione. Posso però accederlo ricorrendo alla reflection,
aggiungo il codice seguente al metodo main della classe Prg (ometto la gestione degli errori):
// viene recuperato il metodo soloMio, posto che esista
Method m = null;
Class<?>[] ps = { };
try
{
pag. 23
Advanced Java rev. 1.0 Novembre 2012
m = [Link]().getMethod("soloMio", ps);
}
catch (NoSuchMethodException exc)
{
[Link]("Il metodo soloMio non esiste");
return;
}
// se siamo qua, allora il metodo soloMio esiste e quindi posso invocarlo
Object[] vs = { };
[Link]("soloMio = "+(String) [Link](st, vs));
In ultimo, si noti che l’istruzione [Link]() restituisce sempre null, sia che si tratti della
classe originale, sia che si tratti di quella sostitutiva: entrambe vengono caricate dal bootstrap class loader.
Ogni classe appartenente a [Link], anche una classe come [Link], che non ha corrispondenza tra le
classi del runtime Java, è caricabile direttamente – ad esempio, cls =
[Link]("[Link]") – e il loader che la carica è sempre il bootstrap class loader.
Le informazioni scritte nel manifest vengono chiamate attributi e sono coppie nome/valore, una per riga, con
la sintassi: name: value, ad esempio Main-Class: [Link]. Alcuni di questi attributi, come Main-Class
oppure Implementation-Title, sono riconosciuti da classi del framework Java, ma si possono anche
specificare attributi di fantasia, gestiti da classi applicative e sconosciuti al framework Java.
Inizio con un progetto Test, accademico, avente un unico package, app, contenente due classi, Maine (col
metodo main) e OtherClass; è presente il file [Link] coi soliti target <clean>, <init>, <compile>, <jarGen>,
tutti minimali. In particolare, <jarGen> è <jar jarfile="[Link]" basedir="build"/>.
Il progetto Test nasce senza alcun manifest esplicitamente definito, tuttavia nel file [Link], generato dal
target <jarGen>, è presente META-INF/[Link] (default manifest):
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.8.3
Created-By: 1.7.0_07-b11 (Oracle Corporation)
Aggiungo al progetto, nella basedir, il file [Link], che definisce [Link] come Main-Class, e rieseguo
il build; entro [Link] trovo ancora META-INF/[Link], coi seguenti contenuti:
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.8.3
Created-By: 1.7.0_07-b11 (Oracle Corporation)
Main-Class: [Link]
pag. 24
Advanced Java rev. 1.0 Novembre 2012
Di qui in poi, lavorerò in un modo più aderente agli standard, ossia creerò la sottodirectory META-INF entro
la directory build, porrò in essa il manifest, e lo chiamerò [Link]. Inoltre riscrivo il target <jarGen>:
<jar manifest="${build}/META-INF/[Link]" jarfile="[Link]" basedir="build"/>.
Fin qui, ho scritto il manifest manualmente, ma non è necessario che sia così, infatti un manifest può anche
essere generato servendosi di ant. Aggiungo a [Link] il seguente target:
<target name="manifesto" description="crea il manifest">
<property name="[Link]" value="1.0.0"/>
<tstamp>
<format property="TODAY" pattern="yyyy-MM-dd HH:mm:ss" />
</tstamp>
<manifest file="${build}/META-INF/[Link]">
<attribute name="Built-By" value="${[Link]}" />
<attribute name="Built-Date" value="${TODAY}" />
<attribute name="Implementation-Title" value="Test app" />
<attribute name="Implementation-Vendor" value="Virgilio spa" />
<attribute name="Implementation-Version" value="${[Link]}"/>
<attribute name="Main-Class" value="[Link]"/>
</manifest>
</target>
Infine, ci sono casi in cui si dispone del solo JAR, senza avere alcun progetto Eclipse e nemmeno un ant
build file. Questo può essere il caso dei JAR di Orbacus: per essi ho a disposizione i build file, ma preferisco
non modificarli. Come esempio, considero [Link], contenuto in Orbacus/lib: il suo manifest è il
default dovuto alla creazione tramite ant (stavolta ant 1.8.4, non il plugin di Eclipse):
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.8.4
Created-By: 1.7.0_07-b11 (Oracle Corporation)
Apro [Link] con WinZip o un analogo prodotto, estraggo quindi il manifest, ad esempio come C:\
META-INF\[Link]; poi edito il manifest estratto, e lo modifico come segue:
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.8.4
Created-By: 1.7.0_07-b11 (Oracle Corporation)
Implementation-Title: CosNaming
Implementation-Vendor: Orbacus
Implementation-Version: 4.3.4
pag. 25
Advanced Java rev. 1.0 Novembre 2012
Il progetto Uti ha (almeno) tre package: num, che contiene la classe Complex; meta, che contiene alcune
annotazioni; [Link], contenente la mia versione della classe StringTokenizer. Il progetto costruisce la
libreria [Link] e la scrive in LibJar.
Aggiungo il manifest a [Link], seguendo la via programmatica: aggiungo a [Link] il tag <manifesto>
(esattamente come sopra) e lo uso nella generazione del JAR, quindi [Link] ha il seguente manifest:
Manifest-Version: 1.0
Ant-Version: Apache Ant 1.8.3
Created-By: 1.7.0_07-b11 (Oracle Corporation)
Built-By: Administrator
Built-Date: 2012-11-10 01:09:35
Implementation-Title: Uti library
Implementation-Vendor: Virgilio spa
Implementation-Version: 1.0.0
Come client di [Link] utilizzo il progetto Tester, adattando di volta in volta la classe [Link] alle esigenze di
test. Comincio con un codice molto semplice, scritto nel metodo main della classe Prg:
Complex nc = new Complex(3,4);
Package pkg = [Link]().getPackage();
[Link]([Link]()+" - "+[Link]());
Per non vedere errori nella IDE di Eclipse, aggiungo [Link] alle Referenced libraries. Fatto ciò, provo a
eseguire il codice direttamente dalla IDE: stando nel codice di [Link] e servendomi del tasto destro, do il
comando Run As Java Application: ottengo l’output num – Uti library, che è quanto mi aspetto.
Adesso costruisco [Link], usando un build file minimalistico, cioè i soliti target <clean>, <init> etc, e in
particolare il seguente <jarGen>:
<target name="jarGen" description="Compression target">
<jar manifest="[Link]" jarfile="${appname}.jar" basedir="build">
<zipfileset src="${[Link]}/[Link]" />
</jar>
</target>
ove [Link], che è il manifest di [Link], consiste della sola riga Main-Class: [Link].
Apro un command prompt in ExeJar e do il comando: java –jar [Link]. L’output è: num – null, ben
diverso da quello atteso. La spiegazione è semplice: all’esecuzione del target <jarGen>, [Link] è stato
scomposto nelle sue classi e queste, assieme alla classe [Link], sono state aggiunte a [Link]; perciò le
classi come Complex sono diventate, per così dire, classi di [Link]. Il manifest di [Link] è andato perso, e
ciò che viene acceduto dal codice [Link] è il manifest di [Link]; siccome
quest’ultimo non definisce alcun Implementation-Title, allora ne consegue l’output num – null.
Per convincermene, provo ad aggiungere Implementation-Title: Tester app entro [Link], che è il
manifest di Tester, quindi rigenero [Link] e lo rilancio: stavolta ottengo l’output num – Tester app.
Questo risultato apre una problematica generale: per la costruzione di un file [Link] che necessita delle classi
di un altro jar, diciamo [Link], non è sempe consigliabile l’inclusione delle classi di [Link] entro [Link], anzi
sarebbe meglio evitare l’inclusione, quando possibile.
L’alternativa all’inclusione è la definizione di un adeguato classpath, e una delle scelte più potenti e flessibili
è l’utilizzo dell’opzione non standard –Xbootclasspath/a. L’opzione –Xbootclasspath/a:<jarFile> fa sì che,
quando necessario, vengano caricate le classi contenute in <jarFile>, dando però la precedenza ad eventuali
classi omonime contenute nei JAR di sistema; è l’opzione speculare di –Xbootclasspath/p.
Tornando al mio caso, riscrivo il target <jarGen> di Tester eliminando l’inclusione di [Link]:
<target name="jarGen" description="Compression target">
pag. 26
Advanced Java rev. 1.0 Novembre 2012
ovviamente java –jar [Link] non funziona più; per lanciare Tester, utilizzo il seguente comando:
java –Xbootclasspath/a:”C:/LibJar/[Link]” –jar [Link]
Adesso riscrivo il codice di [Link] in modo più complicato, ma interessante perché mostra l’uso delle classi
del framework [Link] e [Link], che rappresentano programmaticamente il
manifest del JAR e i suoi attributi ossia le sue righe:
Complex nc = new Complex(3,4);
URL res = [Link]().getResource([Link]().getSimpleName() +
".class");
JarURLConnection conn = (JarURLConnection) [Link]();
Manifest mf = [Link]();
Attributes attrs = [Link]();
String title = [Link]("Implementation-Title");
Package pkg = [Link]().getPackage();
[Link]([Link]()+" - "+title);
ho omesso la gestione degli errori per brevità. Rigenero [Link] e lo rilancio, sempre usando il comando
java –Xbootclasspath/a:”C:/LibJar/[Link]” –jar [Link]; l’output è ancora num – Uti library.
Notare: provo a tornare alla vecchia forma di <jarGen>, che include [Link]; fatto ciò, rigenero [Link] e lo
lancio col comando java –jar [Link]: ottengo num – Tester, per le ragioni sopra spiegate; invece il lancio
col comando java –Xbootclasspath/a:”C:/LibJar/[Link]” –jar [Link] dà il risultato atteso, però è insensato
includere [Link] se poi si utilizza l’opzione –Xbootclasspath/a !!!
In sintesi, utilizzo l’opzione –Xbootclasspath/a, perchè preferisco generare [Link] senza includere [Link].
Come già detto, questa scelta va ben al di là del caso di Tester e Uti, è di portata generale.
Tutte le applicazioni Corba possono evitare l’inclusione della libreria MyCorbaWorld, a patto di aggiungere
l’opzione –Xbootclasspath/a:”C:/LibJar/[Link]” entro ciascun file di lancio ([Link]).
pag. 27
Advanced Java rev. 1.0 Novembre 2012
Lancio [Link] come java –Xbootclasspath/a:”C:/LibJar/[Link]” –jar [Link], e ottengo l’output seguente:
[Link] – Java Runtime Environment, Oracle Corporation,
1.7.0_07
è stata utilizzata la classe appartenente a [Link] (avrei avuto lo stesso risultato con java –jar [Link]).
Lancio [Link] come java –Xbootclasspath/p:”C:/LibJar/[Link]” –jar [Link], e ottengo l’output seguente:
[Link] – Uti library, Virgilio spa, 1.0.0
La classe GestoreCorba ha il vettore di stringhe, statico, AppContextInfo, che viene popolato all’interno del
metodo CatchNamespace:
Class c = [Link]();
Package p = [Link]().getPackage();
URL res = [Link]([Link]() + ".class");
JarURLConnection conn = (JarURLConnection) [Link]();
Manifest mf = [Link]();
Attributes attrs = [Link]();
String title = [Link]("Implementation-Title");
String vendor = [Link]("Implementation-Vendor");
String version = [Link]("Implementation-Version");
String pkg = [Link]();
[Link]("RootNS: Class="+[Link]()+", Package="+pkg);
[Link]("Text(RootNS)="+[Link]());
c = [Link]();
pkg = [Link]().getPackage().getName();
[Link]("VirgoSpace: Class="+[Link]()+", Package="+pkg);
[Link]("Text(VirgoSpace)="+[Link]());
[Link]("Package "+ pkg +": Title="+title+",Vendor="+vendor+
",Version="+version);
Modifico anche una parte del codice del metodo SystemInfo dell’oggetto ServerSistema:
pag. 28
Advanced Java rev. 1.0 Novembre 2012
Serializzare un oggetto significa trasformarlo in una sequenza di byte: dato un certo codice, si dice che esso
serializza un oggetto Obj (istanza di una certa classe) quando trasforma l’oggetto in una sequenza ordinata
di byte e scrive la sequenza su un output stream.
Il processo inverso si chiama deserializzazione: dato un certo codice, si dice che esso deserializza un oggetto
Obj quando legge una sequenza ordinata di byte da un input stream e se ne serve per (ri)generare l’oggetto
Obj, completo del proprio stato.
In senso proprio, serializzazione è il passaggio da oggetto (comprensivo del proprio stato) a sequenza
statica, ordinata, di byte; tuttavia il termine serializzazione spesso indica sia la serializzazione in senso
proprio sia la deserializzazione.
pag. 29
Advanced Java rev. 1.0 Novembre 2012
Siano P1 e P2 due processi distinti, ciascuno con la propria JVM – JVM 1 e JVM2 rispettivamente, e siano O1 e
O2 due oggetti, appartenenti rispettivamente a P1 e P2: i due oggetti si dicono (reciprocamente) remoti.
Quando O1 chiama un metodo di O 2, si parla di comunicazione tra oggetti remoti: essa implica uno scambio
di oggetti tra le due JVM; dico Ω uno di questi oggetti scambiati. La JVM 1 serializza Ω, cioè trasforma Ω in
una sequenza ordinata ∑ di byte e scrive la sequenza ∑ su un output stream device del processo P 1,
tipicamente un socket; quest’ultimo comunica (a basso livello) con un input stream device del processo P 2
(tipicamente, un altro socket): la JVM2 deserializza ossia legge dall’input stream la sequenza ∑ di byte e la
trasforma nell’oggetto Ω, cioè in un oggetto dello stesso tipo e con lo stesso stato di Ω.
Oltre al remoting, la serializzazione è di grande importanza anche in molti altri processi, ad esempio nei
database object relational.
Il build file ha la solita struttura clean init compile jarGen copy; il tag <jarGen> include la libreria
LibJar/[Link]. Questa libreria è anche configurata nel build path.
Il codice di [Link] inizia chiedendo il passaggio (da tastiera) del nome di una classe; il nome va inteso
completo del package, ad esempio [Link] oppure [Link] o [Link]. La classe Serianda
e il suo subtipo XSerianda, appartenenti al package mix, sono state introdotte ad hoc nel progetto Uti per
studiare la serializzazione; in seguito fornirò dettagli su queste classi.
Il nome della classe è stato assegnato alla variabile nmCls di tipo String; se la classe è locale, allora posso
istanziarla direttamente, se invece la classe è esterna devo usare la reflection… non fa differenza, nel
seguito per semplicità assumo che le classi siano locali, ecco quindi il codice:
Object obj = null;
switch (nmCls)
{
case "[Link]":
obj = new Complex(0.5,-1);
break;
case "[Link]":
obj = new XSerianda("Virgilio",53,7.45);
break;
... altre classi ...
default:
[Link]("Non so come trattare questa classe");
return;
}
pag. 30
Advanced Java rev. 1.0 Novembre 2012
[Link](obj);
}
catch (NotSerializableException e)
{
[Link]("Classe non serializzabile");
return;
}
byte[] bSerial = [Link]();
ho lasciato solo la parte cruciale della gestione degli errori.
L’oggetto obj è tipato in base alla classe scelta, nonostante la dichiarazione di obj come oggetto generico (è
l’istanziazione a determinare il tipo di obj, e l’istanziazione è sempre tipata).
La serializzazione dell’oggetto obj consiste nella scrittura dell’oggetto su un output stream di tipo
particolare, predisposto proprio alla serializzazione di oggetti, ossia un’istanza della classe ad hoc
ObjectOutputStream: il nocciolo della serializzazione dell’oggetto obj è l’istruzione [Link](obj).
La classe ObjectOutputStream dispone di vari metodi di scrittura: come ogni subtipo di OutputStream, ha il
metodo write, con svariate signature; ha il metodo writeObject, per scrivere un oggetto dato; e inoltre ha
metodi che consentono la scrittura di vari tipi primitivi, ad esempio [Link](db) con db di tipo
double, e altri come writeLong, writeInt etc.
L’unico costruttore pubblico della classe ObjectOutputStream vuole sempre come parametro di input un
oggetto di tipo OutputStream; si può interpretare ciò come la necessità da parte dell’istanza oos di un
output stream sottostante. Dato che la serializzazione è la trasformazione dell’oggetto in una sequenza di
byte, la scelta scontata è un’istanza del tipo ByteArrayOutputStream.
Questo spiega il codice di serializzazione: prima di tutto creo baos, un’istanza di ByteArrayOutputStream,
poi creo l’istanza oos di ObjectOutputStream, costruita sopra baos. Fatto ciò, si può serializzare l’oggetto
utilizzando il metodo writeObject.
La sequenza di byte che rappresenta l’oggetto obj è recuperabile mediante l’istruzione [Link],
che copia tutto il contenuto corrente di baos entro un byte array creato sul momento, cioè bSerial.
pag. 31
Advanced Java rev. 1.0 Novembre 2012
{
String stringa = null;
int intero = 0;
double doppio = 0;
public Serianda(String s,int n,double d)
{
stringa = s;
intero = n;
doppio = d;
}
@Override
public String toString()
{
return "Serianda instance: stringa = "+ stringa+" - "+
"intero = "+intero+" - "+"double = "+doppio;
}
}
Suppongo di lanciare Serial, come java –jar [Link], e quindi passargli [Link]: l’istruzione
[Link] genera un’eccezione di tipo NotSerializableException.
Infatti, affinchè un oggetto sia serializzabile, è necessario che la classe della quale l’oggetto è un’istanza,
implementi l’interfaccia Serializable, oppure sia subtipo di una classe che implementa Serializable:
package mix;
public class Serianda implements [Link]
{
...
}
L’interfaccia Serializable non ha metodi, è solo una specie di monito per il framework Java, lo informa della
serializzabilità della classe in questione. Allora, impongo che anche [Link] e tutte le altre classi che
intendo utilizzare nella serializzazione implementino l’interfaccia Serializable.
Quando una classe implementa l’interfaccia Serializable, dovrebbe definire un identificativo univoco detto
SerialVersionUID; la IDE di Eclipse lo ricorda, e se esso non viene definito, la piattaforma lo crea; a breve
mostrerò il procedimento con cui è possibile scegliere il valore da assegnare alla classe.
Per ragioni che spiegherò nel seguito, userò come prototipo di classe da serializzare non Serianda, ma il suo
subtipo XSerianda, definito come segue:
package mix;
public class XSerianda extends Serianda
{
private static final long serialVersionUID = 1066L;
private short tag = 12000;
private char car = 'w';
public XSerianda(String s,int n,double d)
{
super(s,n,d);
}
@Override
public String toString()
{
return [Link](tag)+[Link](car)+":X"+[Link]();
}
}
pag. 32
Advanced Java rev. 1.0 Novembre 2012
la classe XSerianda è implicitamente serializzabile (cioè, non va dichiarata tale) perché è subtipo di una
classe, Serianda, che è stata esplicitamente dichiarata serializzabile 3. Anche qui, la IDE mi ha avvertito della
necessità di definire il SerialVersionUID; per farlo, bisogna dichiarare il membro statico di tipo long e nome
serialVersionUID, quindi lo si può valorizzare come si crede.
Scelgo il valore 1066 per il SerialVersionUID della classe XSerianda; nel caso di Serianda, ho usato l’attributo
@SuppressWarnings("serial"), lasciando al sistema la scelta del valore.
Il build file ha la solita struttura clean init compile jarGen copy; il tag <jarGen> non include
alcuna libreria, mentre nel build path è configurata [Link] per ragioni di compilazione.
Il codice di [Link] inizia chiedendo il passaggio (da tastiera) del nome di una classe; il nome va inteso
completo del package, ad esempio [Link], e viene passato entro la variabile stringa nmCls. Il nome
del file da accedere, contenuto nella variabilestringa nmFile, viene costruito aggiungendo “.obj” al nome
della classe: ad esempio, [Link]; la directory è C:/Serials.
Posto che il file esista (altrimenti il programma termina), ne viene letta la lunghezza entro la variabile intera
fLen, quindi viene creata la variabile byte[] bSerial = new byte[fLen]. Queste operazioni servono
allo scopo di allocare un byte array della lunghezza giusta per ospitare la sequenza contenuta nel file; la
lettura effettiva viene compiuta dal seguente codice:
FileInputStream fis = new FileInputStream(nmFile);
[Link](bSerial);
[Link]();
poi viene mostrata la sequenza a video: hexDump(bSerial,[Link]), il valore di ciascun byte
è visualizzato in notazione esadecimale su due cifre.
3
Se una classe C implementa l’interfaccia I, allora anche i subtipi di C implementano I, senza bisogno di
dichiararlo esplicitamente.
pag. 33
Advanced Java rev. 1.0 Novembre 2012
L’operazione fondamentale di un input stream è la lettura di una sequenza di byte; essendo InputStream
una classe astratta, ogni suo subtipo deve implementare almeno un metodo read.
La deserializzazione dell’oggetto obj consiste nella lettura della sequenza di byte rappresentativa
dell’oggetto precedentemente serializzato da un input stream di tipo particolare, predisposto proprio alla
deserializzazione di oggetti, ossia un’istanza della classe ad hoc ObjectInputStream: il nocciolo della
deserializzazione dell’oggetto obj è l’istruzione obj = [Link]().
La classe ObjectInputStream dispone di vari metodi di lettura: come ogni subtipo di InputStream, ha il
metodo read, con svariate signature; ha il metodo readObject, per leggere un oggetto; e inoltre ha metodi
che consentono la lettura di vari tipi primitivi, ad esempio db = [Link]() con db di tipo double, e
altri come readLong, readInt etc.
L’unico costruttore pubblico della classe ObjectInputStream vuole sempre come parametro di input un
oggetto di tipo InputStream; si può interpretare ciò come la necessità da parte dell’istanza ois di un input
stream sottostante. Questo spiega il codice di deserializzazione: prima di tutto creo fis, un’istanza di
FileInputStream agganciata al file contenente l’oggetto serializzato, poi creo l’istanza ois di
ObjectInputStream, costruita sopra fis. Fatto ciò, si può deserializzare l’oggetto utilizzando il metodo
readObject: dopo questa istruzione, l’oggetto è pronto.
Si noti che avevo già usato l’oggetto fis per leggere il contenuto del file nmFile, in modo da poter mostrare il
dump a video; siccome l’oggetto fis rappresenta uno stream sequenziale, non dotato di possibilità di rewind
o riposizionamento, trovandomi in fondo al file ho dovuto ricreare fis, in modo da ripartire dall’inizio dei
dati nel file.
l’oggetto di tipo [Link] è stato ricostruito con lo stato che aveva all’atto della serializzazione.
Notare: non ho usato un oggetto di tipo ByteArrayInputStream, qui non avrebbe avuto senso, dato che
dispongo già della sequenza di byte, letta dal file entro bSerial.
pag. 34
Advanced Java rev. 1.0 Novembre 2012
Si noti che non c’è alcun errore, ossia la creazione dell’oggetto di tipo [Link] va a buon fine, attivando
Deserial con il comando java –Xbootclasspath/a:” LibJar/[Link];LibJar/[Link]” –jar [Link], infatti ciò
equivale a porre [Link] sul default path della JVM.
Il problema nasce dal modo di operare del metodo readObject: questo legge il nome della classe all’interno
della sequenza di byte ricevuta in input e cerca tale classe o all’interno del JAR corrente (qui, [Link]),
oppure nei JAR che incontra nel default path della JVM. L’uso dell’opzione –Xbootclasspath aggiunge il JAR
referenziato nel default path, quindi risolve il problema aggirando l’ostacolo.
Una soluzione migliore sarebbe la costruzione di un subtipo della classe ObjectInputStream, modificando il
codice del metodo readObject, tuttavia è al di là dei miei attuali scopi.
Il protocollo di serializzazione.
Supponendo d’aver serializzato l’istanza di [Link], new XSerianda("Virgilio",53,7.45),
ecco il dump della sequenza di byte rappresentativa, visualizzata da hexDump:
AC ED 00 05 73 72 00 0D 6D 69 78 2E 58 53 65 72
69 61 6E 64 61 00 00 00 00 00 00 04 2A 02 00 02
43 00 03 63 61 72 53 00 03 74 61 67 78 72 00 0C
6D 69 78 2E 53 65 72 69 61 6E 64 61 63 B0 64 D4
33 22 89 AF 02 00 03 44 00 06 64 6F 70 70 69 6F
49 00 06 69 6E 74 65 72 6F 4C 00 07 73 74 72 69
6E 67 61 74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67
2F 53 74 72 69 6E 67 3B 78 70 40 1D CC CC CC CC
CC CD 00 00 00 35 74 00 08 56 69 72 67 69 6C 69
6F 00 77 2E E0
Si parla di protocollo di serializzazione per indicare le regole e le modalità con cui il processo di
serializzazione trasforma un certo oggetto in una sequenza di byte.
Osservazione importante: lo studio in corso si basa in larga misura sull’esempio della classe [Link] e
come tale non copre tutte le possibilità, esistono sequenze diverse, nelle quali compaiono campi non
trattati nel mio studio; tuttavia quanto esposto costituisce un buon punto di partenza, un ragionevole
compromesso tra concretezza e generalità.
Si tenga anche in conto la possibilità di serializzare entità diverse dall’istanza di una classe: posso
serializzare tipi primitivi, array, anche la sola costante null. In ogni caso, la serializzazione dell’istanza di una
classe è il caso più importante, e per la sua frequenza, e perché essa costituisce la serializzazione di base, e
la serializzazione di altre entità, come gli array, può essere spiegata solo dopo aver chiara la prima.
pag. 35
Advanced Java rev. 1.0 Novembre 2012
In generale, la sequenza descrittiva dell’istanza obj della classe Cls, avente uno o più supertipi non banali,
può essere così schematizzata:
a) sottosequenza iniziale costituita dai byte AC ED 00 05
b) descrizione della classe Cls, sempre iniziata dai byte 73 72 e terminata da 78
c) descrizione di tutti i supertipi, banale incluso: è sempre iniziata dal byte 72 e terminata dal byte 70
se la classe Cls ha almeno un supertipo non banale, altrimenti consiste del solo byte 70
d) rappresentazione dello stato dell’istanza obj.
La classe [Link] ha il supertipo [Link], che a sua volta ha come supertipo solamente Object.
Nella sottosequenza iniziale, i primi due byte, AC ED, sono detti stream magic; i due byte a seguire, detti
stream version, identificano la versione del protocollo di serializzazione: in Java 1.7 la versione utilizzata è 5.
La sottosequenza iniziale è sempre la stessa, qualunque sia l’entità serializzata; non è così per i byte a
seguire, ad esempio se avessi serializzato un array, nel quinto byte troverei un valore diverso da 73.
Il valore 0x73 = TC_OBJECT dice che l’entità serializzata è un oggetto in senso proprio, cioè l’istanza di una
classe, e la descrizione della classe inizia sempre col byte 0x72 = TC_CLASSDESC.
Lo schema precedente vale per la serializzazione dell’istanza di una qualsiasi classe; tuttavia il dettaglio dei
campi nelle sezioni b), c) e d) dipende dalle particolarità della classe Cls e dai particolari valori assunti nello
stato dell’istanza obj.
Nel seguito mostrerò un caso, particolare ma paradigmatico, cioè l’insieme dei campi dell’istanza della
classe [Link] creata come new XSerianda("Virgilio",53,7.45):
esempio di [Link] Campo
AC ED STREAM_MAGIC
00 05 STREAM_VERSION
73 TC_OBJECT
72 TC_CLASSDESC
00 0D Lunghezza del nome della classe
6D 69 78 2E 58 53 65 72 69 61 6E 64 61 Nome (completo) della classe: [Link]
00 00 00 00 00 00 04 2A SerialVersionUID: 1066
02 Flag: SC_SERIALIZABLE
Descrizione dei campi
00 02 Numero di campi (XSerianda ha 2 campi)
43 Tipo del primo campo: ‘C’ – character
00 03 Lunghezza del nome del primo campo
63 61 72 Nome del primo campo: car
53 Tipo del secondo campo: ‘S’ – short
00 03 Lunghezza del nome del secondo campo
74 61 67 Nome del secondo campo: tag
78 Terminatore: TC_ENDBLOCKDATA
Descrizione del supertipo
72 TC_CLASSDESC
00 0C Lunghezza del nome della classe
pag. 36
Advanced Java rev. 1.0 Novembre 2012
Oltre al nome in chiaro, fa parte della descrizione anche l’identificativo univoco della classe,
SerialVersionUID; nel caso della classe XSerianda, ho assegnato io il valore, 1066.
Segue un byte, chiamato flag, che contiene alcuni attributi descrittivi della classe; per i miei scopi, questo
campo contiene sempre il solo valore SC_SERIALIZABLE, che ribadisce la natura serializzabile della classe.
A questo punto segue la lista descrittiva dei campi della classe: prima c’è il numero dei campi (su 4 byte),
poi la descrizione di ciascun campo. I campi sono ordinati in ordine alfabetico.
pag. 37
Advanced Java rev. 1.0 Novembre 2012
La descrizione del supertipo, nell’esempio la classe [Link], è la descrizione di una classe e quindi vale
quanto detto sopra, né più né meno. Il supertipo di [Link] è [Link], quindi la catena dei
supertipi di [Link] finisce qui: la serializzazione di [Link] si traduce nella scrittura sullo
stream del byte 0x70 = TC_NULL. Siccome Object è il supertipo finale di ogni classe, e la sezione descrittiva
dei supertipi comprende tutti i supertipi, allora la descrizione dei supertipi termina sempre con 0x70.
Il caso di una classe senza alcun supertipo non banale è immediato: la sezione del supertipo consiste del
solo byte 0x70 = TC_NULL, a seguito del byte 0x78 che chiude la descrizione della classe; in questo caso,
non c’è alcun byte 0x72 ad iniziare la descrizione dei supertipi.
Supponiamo che una classe abbia più di un supertipo non banale: detta C una classe con due supertipi non
banali, posso rappresentare la catena della sua ereditarietà come [Link] T1 T2 C, ove T1 e
T2 sono i supertipi in cascata di C.
È immediata l’estensione dello schema al caso di classi con n > 2 supertipi non banali. Questa struttura a
blocchi è del tutto equivalente alla struttura in proposizioni già esposta.
Segue la descrizione formale dei campi, che è la stessa per la classe come per i supertipi. Nel caso di una
classe priva di campi, viene scritto 00 00 sull’output stream e, di seguito, il byte 0x78 che pone fine alla
descrizione della classe in questione.
I campi tag e car della classe XSerianda, e anche i campi doppio e intero della classe supertipo Serianda,
hanno tipi primitivi, perciò la loro descrizione consiste di tre sole informazioni: tipo, lunghezza del nome e
valore del nome.
Il campo stringa della classe Serianda è invece trattato come un oggetto, il suo tipo è 0x4C, e infatti per esso
è passato il nome del tipo, cioè [Link], scritto però nella notazione utilizzata dalla JVM, ossia
Ljava/lang/String;.
Studierò altri esempi nel seguito, tra i quali campi definiti come array.
pag. 38
Advanced Java rev. 1.0 Novembre 2012
Se il campo è di tipo primitivo, ad esempio double oppure integer, viene semplicemente scritto il suo
valore4; quando il campo è di tipo object, la struttura del valore è più complessa.
Nell’esempio di XSerianda, il campo stringa di tipo String (ereditato dal supertipo Serianda) ha valore scritto
in notazione stream protocol, tuttavia la sua struttura è peculiare del tipo String, per altri tipi la struttura è
diversa, come si vedrà nel seguito.
I tipi primitivi non sono classi e quindi una variabile primitiva non è un oggetto, infatti scrivendo int x = 10,
la variabile x contiene il valore 10, mentre scrivendo String s = “Virgilio”, la variabile s non contiene il valore,
ma contiene l’indirizzo (il reference) in cui si trova il valore.
I tipi primitivi dispongono di una pseudoclasse: l’istruzione Class cls = [Link] restituisce un
oggetto cls non null, tuttavia applicando la reflection scopro che la classe cls ha package null, nessun
costruttore, nessun metodo e nessun campo, e nemmeno alcun supertipo, neppure [Link], ma ha
solo un nome, infatti l’istruzione [Link] restituisce la stringa boolean.
Osservazione importante: i tipi primitivi possono essere serializzati nativamente, cioè non come oggetti,
usando i metodi appositi della classe ObjectOutputStream, come writeInt, writeDouble etc. È anche
possibile serializzare una variabile di tipo primitivo come se fosse un oggetto, servendosi ancora del metodo
[Link], tornando così al caso di serializzazione dell’istanza di una classe. I due
procedimenti di serializzazione danno risultati diversi.
Nel caso nativo, anche il codice, sia di serializzazione sia di deserializzazione, è un po’ diverso; il frammento
seguente mostra come serializzare nativamente una variabile di tipo double:
FileOutputStream fos = new FileOutputStream(nmFile);
ObjectOutputStream oos = new ObjectOutputStream(fos);
double reale = 7.45;
[Link](reale);
[Link]();
[Link]();
la gestione degli errori è stata omessa per semplicità; è importantissimo ricordarsi di chiudere l’oggetto
ObjectOutputStream (oos) prima di terminare l’applicazione5 !
Si noti che non è stato usato il tipo ByteArrayOutputStream come substrato per l’oggetto oos, il suo
impiego avrebbe comportato errori; l’utilizzo del tipo FileOutputStream consente di serializzare sia oggetti,
sia tipi primitivi nativamente, nonché un misto dei due.
4
Il formato del valore che viene scritto non ha nulla a che fare con la notazione dello stream protocol. Ad esempio, il
double 7.45 ha rappresentazione binaria 40 1D CC CC CC CC CC CD, e tale è il suo valore, scritto all’atto della
serializzazione.
5
La chiusura dell’oggetto FileOutputStream (fos) non è da sola sufficiente; non è neppure necessaria, tuttavia è buona
norma effettuarla.
pag. 39
Advanced Java rev. 1.0 Novembre 2012
Serializzare una variabile di tipo primitivo significa serializzare un’istanza della classe wrapper.
Ciò che è stato serializzato è un’istanza della classe [Link], wrapper del tipo primitivo boolean.
pag. 40
Advanced Java rev. 1.0 Novembre 2012
Posso partire direttamente dal wrapper, usando il codice standard di Serial; allo scopo, aggiungo al switch
su nmCls il ramo seguente:
case "[Link]":
obj = new Boolean(true);
break;
tutto il resto del codice, rimasto immodificato, va come già illustrato. L’output che ottengo è identico a
quello già esposto.
Dallo studio della serializzazione, si evince che il wrapper di un tipo primitivo P è una classe con un solo
campo, il cui nome è value e il cui tipo è P.
Anche qui, è indifferente che io utilizzi la variabile esplicita int n = 1008357163 (hex 3C1A4F2B),
oppure utilizzi il codice standard aggiungendo allo switch su nmCls il ramo:
case "[Link]":
obj = new Integer(1008357163); //
break;
pag. 41
Advanced Java rev. 1.0 Novembre 2012
Stato dell’istanza
3C 1A 4F 2B Valore del campo value: 1008357163
Esaminando l’output, si vede che è assai analogo a quello ottenuto serializzando un’istanza di
[Link], anzi è sottocaso della serializzazione di un’istanza della classe [Link], in quanto
compare pure qui un supertipo non banale.
Ecco l’output:
serializzazione di una variabile stringa Campo
AC ED STREAM_MAGIC
00 05 STREAM_VERSION
74 TC_STRING
00 0D Lunghezza del valore
50 72 6F 76 61 20 73 65 72 69 61 6C 65 valore: Prova seriale
Si tratta di un comportamento peculiare, si manifesta solo quando l’oggetto serializzato è una stringa. È
importante osservare che la stessa struttura è presente nel caso della classe XSerianda, nella sezione
dedicata al valore del campo stringa di tipo String, mentre non è così nella descrizione del tipo del campo
stringa di tipo String (sempre di XSerianda).
Un altro caso a sé è la serializzazione di un oggetto null: ciò che ottengo in output è AC ED 00 05 70.
Serializzazione dell’istanza di una classe con campi tipizzati mediante altre classi.
Le classi esaminate finora avevano tutti i campi di tipo primitivo oppure di tipo String; adesso esaminerò il
caso della classe [Link], avente tre campi: it di tipo int, cx di tipo [Link] e db di tipo double.
Prima mostro la serializzazione di un’istanza della classe [Link], prevista fin dall’inizio in Serial; il
valore dell’istanza è new Complex(0.5,-1):
serializzazione di una variabile di tipo [Link] Campo
AC ED STREAM_MAGIC
00 05 STREAM_VERSION
73 TC_OBJECT
72 TC_CLASSDESC
00 0B Lunghezza del nome della classe
6E 75 6D 2E 43 6F 6D 70 6C 65 78 Nome (completo) della classe: [Link]
C3 F4 C2 20 85 A4 98 4C SerialVersionUID
02 Flag: SC_SERIALIZABLE
Descrizione dei campi
00 02 Numero di campi
44 Tipo del primo campo: ‘D’ – double
00 03 Lunghezza del nome del primo campo
69 6D 67 Nome del primo campo: img
pag. 42
Advanced Java rev. 1.0 Novembre 2012
pag. 43
Advanced Java rev. 1.0 Novembre 2012
02 Flag: SC_SERIALIZABLE
Descrizione dei campi
00 02 Numero di campi
44 Tipo del primo campo: ‘D’ – double
00 03 Lunghezza del nome del primo campo
69 6D 67 Nome del primo campo: img
44 Tipo del secondo campo: ‘D’ – double
00 04 Lunghezza del nome del secondo campo
72 65 61 6C Nome del primo campo: real
78 Terminatore: TC_ENDBLOCKDATA
70 TC_NULL
BF F0 00 00 00 00 00 00 Valore del campo img: -1
3F E0 00 00 00 00 00 00 Valore del campo real: 0.5
La serializzazione di [Link] ha seguito lo schema di [Link], con una novità per quanto
riguarda la descrizione dello stato: il valore del campo cx, di tipo [Link], è stato scritto riportando
tutta la serializzazione dell’istanza new Complex(0.5,-1), eccezion fatta per i primi 4 byte AC ED 00 05.
Osservazione importantissima: affinchè una classe sia serializzabile, devono esserlo tutte le classi che
compaiono come tipi dei suoi membri; nel caso di [Link], la serializzazione è garantita perché non
solo la classe [Link] è serializzabile (cioè implementa l’interfaccia Serializable), ma lo è anche la
classe [Link] che tipizza due suoi membri.
Ad ogni oggetto serializzato, cioè scritto sull‘output stream, viene assegnato un handle, cioè un codice che è
in seguito usato per riferirsi all’oggetto. Ciò consente un potenziale risparmio sul volume dei dati.
I handle sono assegnati sequenzialmente, partendo dal valore 0x7E0000 (valore definito come
[Link], di tipo int); la numerazione ricomincia da 0x7E0000
quando lo stream è resettato.
Quando una classe ha due campi t1 e t2 dello stesso tipo T, con T non primitivo, la serializzazione implica la
descrizione del tipo T per quanto concerne t 1, mentre per la descrizione del tipo di t 2 viene usato un handle
che referenzia la precedente descrizione di T.
Mostro un primo esempio di utilizzo dei handle, è il caso di una classe con campi di tipo ripetuto:
package mix;
public class Sestupla implements [Link]
{
private static final long serialVersionUID = 1002L;
private String abc = "abc";
private [Link] fcx = new [Link](0.75,-0.25);
private int it = 10000;
private [Link] scx = new [Link](0.5,1.25);
private int tag = 1000000;
private String xyz = "xyz";
}
i campi sono sei, coi tipi ripetuti a coppie: it e tag sono campi dello stesso tipo primitivo int, abc e xyz sono
entrambi di tipo String e quindi vengono trattati come oggetti (sia pure sui generis), mentre i campi fcx e scx
sono entrambi istanze della classe Complex.
pag. 44
Advanced Java rev. 1.0 Novembre 2012
Segue l’output della serializzazione di un’istanza della classe [Link], creata col costruttore default
dato che i campi vengono comunque valorizzati all’inizializzazione (il significato dei numerini rossi tra
parentesi verrà chiarito nel seguito):
pag. 45
Advanced Java rev. 1.0 Novembre 2012
74 TC_STRING (4)
00 03 Lunghezza del valore
61 62 63 Valore: abc
Valore del campo fcx
73 TC_OBJECT
72 TC_CLASSDESC (5)
00 0B Lunghezza del nome del tipo del valore
6E 75 6D 2E 43 6F 6D 70 6C 65 78 Nome del tipo del valore: [Link]
C3 F4 C2 20 85 A4 98 4C SerialVersionUID
02 SC_SERIALIZABLE
00 02 Numero di campi
44 Tipo del primo campo: ‘D’ – double
00 03 Lunghezza del nome del primo campo
69 6D 67 Nome del primo campo: img
44 Tipo del secondo campo: ‘D’ – double
00 04 Lunghezza del nome del secondo campo
72 65 61 6C Nome del secondo campo: real
78 TC_ENDBLOCKDATA
70 TC_NULL (6)
BF D0 00 00 00 00 00 00 Valore del campo img: -0.25
3F E8 00 00 00 00 00 00 Valore del campo real: 0.75
Valore del campo scx
73 TC_OBJECT
71 TC_REFERENCE
00 7E 00 05 Handle a (5)
3F F4 00 00 00 00 00 00 Valore del campo img: 1.25
3F E0 00 00 00 00 00 00 Valore del campo real: 0.5
Valore del campo xyz
74 TC_STRING (7)
00 03 Lunghezza del valore
78 79 7A Valore: xyz
Si noti che, nella descrizione della classe Sestupla, vengono prima elencati i campi di tipo primitivo, in
ordine alfabetico, poi segue l’elencazione dei campi di tipo oggetto (istanze di String o di Complex, non fa
differenza), anch’essi in ordine alfabetico. Si tratta di una regola generale.
Il meccanismo dei handle non riguarda i tipi primitivi, in quanto un handle viene assegnato a un oggetto
serializzato, e un dato di tipo primitivo non è un oggetto.
Il valore iniziale 0x7E0000 (baseWireHandle) è assegnato sempre al primo oggetto scritto sull’output
stream, che qui è la sequenza descrittiva della classe [Link], sequenza iniziata dal byte 0x72 =
TC_CLASSDESC, qui referenziata come (0). Il secondo oggetto ad essere scritto sull’output stream è la
sequenza (1), iniziata da 0x74 = TC_STRING, perciò a questa sequenza viene associato un handle, e il valore
di tale handle è uguale al precedente più uno, e ciò spiega l’assegnazione del valore 0x7E0001.
Il terzo oggetto serializzato è la sequenza referenziata come (2), perciò il valore del suo handle è 0x7E0002.
La descrizione del campo fcx avviene come di consueto, ma quando si giunge alla descrizione del campo
successivo, scx, c’è la ripetizione del tipo Complex, e qui interviene il meccanismo dei handle: i primi tre
dati, ossia tipo = 0x4C seguito dal nome 0x73 0x63 0x78 = scx, vengono necessariamente scritti perché su
essi non c’è la ripetizione, poi dovrebbe seguire la descrizione del tipo Complex, ma questa è già stata
scritta, e al suo posto compare il suo handle 0x7E0002, preceduto da 0x71 = TC_REFERENCE (il quale
specifica che ciò che segue nello stream è un handle, su 4 byte).
pag. 46
Advanced Java rev. 1.0 Novembre 2012
Procedendo con la descrizione dei campi, incontro xyz di tipo String, e anche qui interviene l’uso
dell’handle: dopo il nome, xyz, dovrebbe seguire la sequenza 0x74 . . . 0x3B, ma essendo questa già
presente e contrassegnata (1), con handle 0x7E0001, nella descrizione del campo xyz viene scritto 0x71
0x00 0x7E 0x00 0x01.
Terminata la descrizione dei campi, giungo alla descrizione del supertipo, che consiste di un solo TC_NULL
perché la classe Sestupla discende direttamente da [Link]; comunque anche in questo caso
banalizzato siamo in presenza di scrittura d’un oggetto – il null object – sull’output stream, riferito come (3),
quindi ad anch’esso viene associato un handle, il cui valore è necessariamente 0x7E0003.
Procedendo, incontro la descrizione dello stato: i primi due campi sono di tipo primitivo e non implicano
assegnazioni di handle, ma il terzo campo è di tipo String e come descrizione del suo valore c’è la sequenza
0x74 . . . 0x63, referenziata come (4), alla quale viene associato l’handle di valore 0x7E0004.
Segue la descrizione del valore del quarto campo, fcx, di tipo Complex: qui c’è la descrizione del tipo,
[Link], cioè la sequenza referenziata come (5), cui viene associato l’handle di valore 0x7E0005.
Nella descrizione del valore del quinto campo, scx, anch’esso di tipo [Link], viene scritto il
riferimento alla sequenza (5), ossia TC_REFERENCE 0x7E0005.
Per ultimo, viene il valore del campo xyz di tipo String, che data la sua immediatezza non implica ripetizioni
e quindi non c’è uso di alcun handle.
L’uso dei handle interviene sia nella descrizione dei campi, sia nella descrizione dello stato cioè dei valori
dei campi, tuttavia in ambo i casi esso concerne la descrizione di tipi.
pag. 47
Advanced Java rev. 1.0 Novembre 2012
74 TC_STRING (1)
00 0F Lunghezza del nome del tipo
4C 6D 69 78 2F 52 69 63 6F 72 73 69 76
61 3B Nome: Lmix/Ricorsiva;
78 TC_ENDBLOCKDATA
70 TC_NULL
71 TC_REFERENCE
00 7E 00 02 Handle a (?)
Come si vede, i handle sono utilizzati; c’è tuttavia un problema: mi sarei atteso il valore 0x7E0000
nell’ultimo campo, e non il valore 0x7E0002, che a me sembra puntare a TC_NULL.
Serializzazioni iterate.
È possibile invocare più volte l’istruzione [Link] (oos è l’oggetto di tipo ObjectOutputStream,
costruito sopra a un oggetto, fos, di tipo FileOutputStream), ossia effettuare due o più distinte operazioni di
scrittura sullo stesso output stream; anche in questo caso, può intervenire il meccanismo dei handle. È
anche possibile scrivere sullo stesso output stream un misto di oggetti e dati primitivi, ossia usare istruzioni
writeObj assieme ad istruzioni come writeInt, writeDouble etc.
pag. 48
Advanced Java rev. 1.0 Novembre 2012
Serializzazione di array.
In generale, un array T[] è sempre una classe, anche quando T è un tipo primitivo; con il termine classe
array indico T[], mentre con il termine classe item indico T.
Inoltre T[] è sempre subtipo diretto di [Link], indipendentemente dal tipo T. Il package, ricavabile
come T[].[Link], è sempre null.
Per scoprire ciò, non è necessario analizzare i risultati delle serializzazioni, si può giungere alle stesse
conclusioni anche per altra via, usando la reflection: scrivendo Class cls = T[].class, trovo che cls ha
nove metodi, nessun costruttore, nessun campo, ha supertipo [Link] e package null.
Il nome, cioè [Link], è sempre costituito dal carattere ‘[‘ seguito dal nome del tipo T scritto in
notazione stream protocol, cioè una sola lettera per i tipi primitivi e L<[Link]>; negli altri casi;
nella tabella che segue mostro alcuni esempi d’interesse, ossia array di tipi primitivi, array di stringhe e
array di classi applicative come [Link]:
pag. 49
Advanced Java rev. 1.0 Novembre 2012
Nella quarta colonna ho riportato il SerialVersionUID della classe array, ricavato con il seguente codice:
ObjectStreamClass osc = [Link](T[].class);
long SerialVersionUID = [Link]();
Si noti che osc è null per i tipi primitivi, ma non per i loro array.
Nel caso di array di dati primitivi, ad esempio T = boolean, non c’è classe item perché boolean non è una
classe, però c’è la classe array, infatti boolean[] è una classe.
La classe array è sempre priva di campi, non importa quanti e quali campi abbia la classe item.
L’array T[] è serializzabile se e solo se T è serializzabile; ma la serializzazione degli array segue uno schema
un po’ diverso da quello proprio delle istanze di oggetti.
In generale, la sequenza descrittiva dell’array arr di tipo T[] può essere così schematizzata:
a) sottosequenza iniziale costituita dai byte AC ED 00 05
b) descrizione della classe array (cioè T[]), sempre iniziata dai byte 75 72 e terminata da 78 70, ove
il byte 0x70 rappresenta la descrizione del supertipo della classe array, cioè [Link]
c) rappresentazione della lista di valori arr[i].
pag. 50
Advanced Java rev. 1.0 Novembre 2012
00 05 STREAM_VERSION
75 TC_ARRAY
72 TC_CLASSDESC
00 02 Lunghezza del nome della classe array
5B 49 Nome della classe array: [I
4D BA 60 26 76 EA B2 A5 SerialVersionUID
02 SC_SERIALIZABLE
00 00 Numero di campi
78 TC_ENDBLOCKDATA
70 TC_NULL
00 00 00 03 Numero di item dell’array
FF FF FF FF Primo item = -1
00 01 86 A0 Secondo item = 100000
00 0F 42 40 Secondo item = 1000000
Siccome la classe item è int, che non è una vera classe, la scrittura dei valori è immediata (non c’è
descrizione di tipo).
pag. 51
Advanced Java rev. 1.0 Novembre 2012
70 TC_NULL
00 00 00 02 Numero di item dell’array
Primo item
74 TC_STRING
00 05 Lunghezza del valore
50 72 6F 76 61 Valore: Prova
Secondo item
74 TC_STRING
00 07 Lunghezza del valore
73 65 72 69 61 6C 65 Valore: seriale
La scrittura dei valori avviene nella modalità tipica del tipo String, diversa da tutte le altre; è comunque una
scrittura immediata, senza descrizioni di classe, e così non vengono usati i handle.
pag. 52
Advanced Java rev. 1.0 Novembre 2012
Primo item
73 TC_OBJECT
72 TC_CLASSDESC (2)
00 0B Lunghezza del nome della classe
6E 75 6D 2E 43 6F 6D 70 6C 65 78 Nome (completo) della classe: [Link]
C3 F4 C2 20 85 A4 98 4C SerialVersionUID
02 SC_SERIALIZABLE
00 02 Numero di campi
44 Tipo del primo campo: ‘D’ – double
00 03 Lunghezza del nome del primo campo
69 6D 67 Nome del primo campo: img
44 Tipo del secondo campo: ‘D’ – double
00 04 Lunghezza del nome del secondo campo
72 65 61 6C Nome del secondo campo: real
78 TC_ENDBLOCKDATA
70 TC_NULL
BF F0 00 00 00 00 00 00 Valore del campo img: -1
3F F8 00 00 00 00 00 00 Valore del campo real: 1.5
Secondo item
73 TC_OBJECT
71 TC_REFERENCE
00 7E 00 02 Handle a (2)
3F E8 00 00 00 00 00 00 Valore del campo img: 0.75
3F D0 00 00 00 00 00 00 Valore del campo img: 0.25
Terzo item
73 TC_OBJECT
71 TC_REFERENCE
00 7E 00 02 Handle a (2)
40 10 00 00 00 00 00 00 Valore del campo img: 4
40 08 00 00 00 00 00 00 Valore del campo img: 3
Come si vede, i handle sono usati, in modo semplice; nel caso di array con molti item e classe item
voluminosa, dall’uso dei handle risulta una grande ottimizzazione.
pag. 53
Advanced Java rev. 1.0 Novembre 2012
al = new long[molt];
for (int i=0;i<molt;i++) al[i] = 1000000*v*(i+1);
}
}
cui segue la consueta [Link](obj); la sua deserializzazione è operata dall’applicazione Deserial, che
utilizza la consueta obj = [Link]() ed esegue il successivo codice:
case "[Link]":
[Link] clar = ([Link])obj;
[Link]("Classarray clar = "+clar);
break;
pag. 54
Advanced Java rev. 1.0 Novembre 2012
il codice dell’applicazione Deserial resta invariato; viene correttamente ricostruito l’array di (tre) oggetti
Complex: 0.25i, 0.25+0.5i, 0.5+0.75i. Segue il dump dell’array serializzato:
analisi di Serials/[Link] Campo
AC ED STREAM_MAGIC
00 05 STREAM_VERSION
73 TC_OBJECT
72 TC_CLASSDESC (0)
00 0E Lunghezza del nome della classe
6D 69 78 2E 43 6C 61 73 73 61 72 72 61 79 Nome (completo) della classe: [Link]
00 00 00 00 00 00 04 06 SerialVersionUID
02 SC_SERIALIZABLE
00 02 Numero di campi
49 Tipo del primo campo: ‘I’ – integer
00 04 Lunghezza del nome del primo campo
6D 6F 6C 74 Nome del primo campo: molt
5B Tipo del secondo campo: ‘[‘ – array
00 03 Lunghezza del nome del secondo campo
61 63 78 Nome del secondo campo: acx
Descrizione del tipo del secondo campo
74 TC_STRING (1)
00 0E Lunghezza del nome del tipo
5B 4C 6E 75 6D 2F 43 6F 6D 70 6C 65 78 3B Nome del tipo: [Lnum/Complex;
78 TC_ENDBLOCKDATA
70 TC_NULL (2)
Descrizione dello stato
00 00 00 03 Valore del campo molt: 3
Valore del campo (array) acx
75 TC_ARRAY
Descrizione della classe array [Link][]
72 TC_CLASSDESC (3)
pag. 55
Advanced Java rev. 1.0 Novembre 2012
L’uso dei handle è del tutto analogo a quanto si è visto nella serializzazione di un array di classi.
pag. 56
Advanced Java rev. 1.0 Novembre 2012
L’applicazione Telefono permette a due computer connessi sulla stessa LAN di scambiarsi messaggi testuali
in tempo reale, è una applicazione chat di tipo console; è basata sull’uso diretto dei socket.
L’applicazione ha comportamento analogo a quello di un comune scambio telefonico: i due utenti, indicati
come A e B, sono rappresentati ciascuno da un endpoint, ossia una coppia <host:porta>; se ad esempio A
parte per primo, la sua chiamata resta appesa finchè non parte anche B: a questo punto i due utenti sono
connessi e possono scambiarsi messaggi6. Quando uno dei due chiude la comunicazione (inviando all’altro
la stringa “fine”), entrambi si scollegano, cioè terminano entrambe le applicazioni.
Se A chiama B, ma B non risponde, dopo un certo intervallo di tempo l’applicazione termina: è il timeout di
connessione non avvenuta.
L’applicazione Telefono è costruita mediante un progetto Java; il package app contiene quattro classi, le due
principali sono Telephone, munita di metodo main (è la classe scritta nel manifest), e Receiver, subtipo di
Thread; sono inoltre presenti le classi d’appoggio ExitObject e UnrespTask. Il build è standard, e produce in
ExeJar il file [Link], lanciabile come java –jar [Link] par1 par2 par3.
La struttura dell’applicazione.
Il metodo main della classe Telephone inizia recuperando gli eventuali parametri di passaggio da linea di
comando, poi istanzia le classi ExitObject, destinata alla chiusura ordinata dell’applicazione, e Receiver:
mentre l’invio dei messaggi avviene ad opera del main di Telephone, la ricezione avviene (in un thread
separato) ad opera dell’istanza della classe Receiver.
Poi il metodo main prosegue creando il client socket cs, ossia il socket destinato a inviare i messaggi; nel
frattempo, l’istanza di Receiver crea il server socket, destinato alla ricezione, e fa anche partire il timeout di
connessione non avvenuta.
Il metodo main passa all’istanza di Receiver l’istanza di ExitObject e il client socket, nel caso dovesse essere
l’istanza di ricezione ad occuparsi della chiusura dell’applicazione: mentre il passaggio dell’istanza di
ExitObject avviene già nel costruttore, quello del client socket avviene in un secondo tempo.
Fatto ciò, il metodo main entra nel ciclo di invio: attende l’input da operatore e lo scrive sull’output stream del
client socket. La classe Telephone ha, oltre al main, il metodo leggiBuffer, che recupera l’input da tastiera e
lo restituisce come una stringa; l’input da tastiera è terminato dal tasto Invio.
Il ciclo di invio ha durata indefinita, termina quando l’operatore digita “fine”: la stringa viene spedita, ma c’è
l’uscita dal loop; questo è uno dei due modi non patologici e non banali di chiusura dell’applicazione, diciamo
una chiusura attiva dal punto di vista dell’utente dell’applicazione (è lui che ha scritto “fine”); l’altro modo è la
chiusura passiva, esaminata nel seguito.
In uscita dal loop, il metodo main effettua le operazioni di chiusura: chiude l’output stream di cs, attende la
fine del thread di ricezione, e poi invoca il metodo termina dell’oggetto ExitObject, che chiude il client socket,
chiude il server socket, e forza la terminazione dell’intera applicazione (istruzione [Link](0)).
Mentre il metodo main di Telephone si trova nel ciclo di invio, il metodo run della classe Receiver esegue nel
suo thread separato. Come già detto, il codice del metodo run crea il server socket e lo pone in attesa di
connessioni; immediatamente prima dell’inizio dell’attesa di connessioni, viene attivato il timer di
connessione non avvenuta, che si serve dell’istanza di UnrespTask, classe subtipo di TimerTask: se il timer
scade prima che avvenga una connessione, il metodo run dell’oggetto UnrespTask chiude il server socket e
forza la terminazione dell’intera applicazione (istruzione [Link](0)); si tratta in questo caso di un
modo di chiusura banale dell’applicazione (per mancata risposta).
Se invece la connessione da parte di un’altra applicazione Telefono avviene prima che il timer scada, il
metodo run cancella il timer, passa il server socket all’istanza di ExitObject ed entra nel ciclo di ricezione. Il
6
Lo scambio dei messaggi è asincrono, ossia ogni utente può ricevere anche mentre sta scrivendo,
esattamente come in una telefonata gli utenti possono reciprocamente parlarsi addosso.
pag. 57
Advanced Java rev. 1.0 Novembre 2012
ciclo di ricezione legge dall’input stream del socket: ciò che viene letto, viene stampato a video; l’istruzione di
lettura è bloccante; il ciclo di ricezione ha durata indefinita, termina quando l’input stream riceve end of file.
L’uscita dal ciclo di ricezione può avvenire in due modi (non patologici): il primo modo è la chiusura attiva,
ossia l’operatore ha digitato “fine” nel ciclo di invio della stessa applicazione. Oltre alla chiusura attiva, è
possibile la chiusura passiva: questo si verifica quando l’applicazione riceve la stringa “fine” dalla sua
controparte, e questo evento accade proprio nel ciclo di ricezione.
Se A e B sono le due applicazioni interlocutorie, è ovvio che se A compie la chiusura attiva (cioè l’operatore
che usa A digita e invia “fine”), allora B compie una chiusura passiva, e viceversa.
Supponiamo che sia A a compiere la chiusura attiva: A spedisce a B la stringa “fine”, esce dal suo ciclo di
invio e chiude l’output stream del client socket; B riceve la stringa “fine”, che stampa, inoltre essendo stato
chiuso l’output stream del client socket di A, l’input stream di B in ricezione riceve un end of file e con ciò il
ciclo di ricezione termina. A sua volta B, chiudendo il proprio client socket, determina il verificarsi di un end of
file nel ciclo di ricezione di A, e così anche il ciclo di ricezione di A termina.
Una volta uscito dal ciclo di ricezione, il codice del metodo run chiude l’input stream, quindi verifica se si
tratta di chiusura passiva o attiva: nel primo caso, invoca il metodo termina dell’oggetto ExitObject, che
chiude il client socket, chiude il server socket, e forza la terminazione dell’intera applicazione (istruzione
[Link](0)). Nel caso di chiusura attiva, il codice del metodo run non compie azioni, in questo caso
sarà il metodo main di Telephone a preoccuparsi di chiamare il metodo termina dell’istanza di ExitObject.
Il caso di chiusura patologica più frequente, dovuto alla brusca caduta dell’applicazione remota, è trattato
adeguatamente; la sua trattazione verrà esposta studiando i dettagli del codice della classe Receiver.
La classe Telephone.
La classe Telephone ha, oltre al main, un solo altro metodo, private static String leggiBuffer(),
che recupera l’input da tastiera (terminato dal tasto Invio) e lo restituisce come una stringa.
Il main recupera gli eventuali tre parametri passati da linea di comando: par1 va nella variabile stringa
remHost (il default è “localhost”), par2 va nella variabile intera locPort (default 15000) e par3 va nella
variabile intera remPort (default 16000). È definita anche la variabile intera backlog, uguale a 50.
Le variabili remHost e remPort definiscono l’endpoint remoto, mentre locPort rappresenta la porta sull’host
locale: l’applicazione Telefono è in ascolto (riceve) sull’endpoint locale <localhost:locPort> e trasmette
all’endpoint remoto <remHost:remPort>.
Nello scenario d’esempio, ipotizzo che l’applicazione Telefono sia lanciata sui computer Ufficio-PC
(macchina Windows, di indirizzo [Link]) e sccver (macchina Linux, di indirizzo [Link]), in LAN
tra loro; la porta d’ascolto su Ufficio-PC è 15000, quella su sccver è 16000.
L’applicazione Telefono viene lanciata per prima in un command prompt sul computer Ufficio-PC, mediante
java –jar [Link] sccver 15000 16000, quindi remHost = sccver, remPort = 16000, locPort = 15000.
Recuperati i dati iniziali, il main prosegue creando l’istanza della classe Receiver e attivandola:
ExitObject exitObj = new ExitObject();
Receiver recv = new Receiver(locPort,backlog,exitObj);
[Link]();
La creazione dell’oggetto exitObj avviene tramite il costruttore implicito (la classe ExitObject non definisce un
costruttore); l’istanza di Receiver opera nel suo thread separato. Il main di Telephone prosegue creando il
client socket di trasmissione:
Socket cs = null;
boolean pass = false;
do
{
try
{
cs = new Socket(remHost,remPort);
pass = true;
}
pag. 58
Advanced Java rev. 1.0 Novembre 2012
catch (Exception e)
{
[Link](2000);
}
}
while (!pass);
[Link]("Connesso");
[Link](cs);
Nello scenario ipotizzato, il socket cs dovrebbe essere creato sull’endpoint <Ufficio-PC:door>, ove door è
una porta scelta dal sistema operativo. Il codice Socket cs = new Socket(remHost,remPort) opera
la creazione del socket e la sua connessione all’endpoint remoto <sccver:16000>7.
Tuttavia il socket cs non viene creato, perchè non ho ancora attivato l’applicazione Telefono su sccver,
quindi la connessione non va a buon fine: devo intercettare l’eccezione, fermarmi per un periodo di tempo
(due secondi) e ritentare. Così l’applicazione Telefono su Ufficio-PC ha il thread principale entro un loop di
durata indefinita, mentre il thread di ricezione è a sua volta bloccato, come si vedrà nel seguito; non c’è
alcun output visibile.
Lancio l’applicazione sul computer sccver: java –jar [Link] Ufficio-PC 16000 15000, quindi remHost =
Ufficio-PC e remPort = 15000, locPort = 16000. Così facendo, la creazione del socket cs e la sua
connessione all’endpoint remoto <sccver:16000> vanno a buon fine, il codice esce dal loop e prosegue
passando cs all’istanza di Receiver (sarà necessario nel caso di chiusura passiva).
La prima parte del codice rappresenta il ciclo di invio vero e proprio; se non giunge la chiusura passiva,
attraverso il thread di ricezione, proveniente dall’operatore su sccver, allora l’uscita dal ciclo è determinata
dal passaggio della stringa “fine” da parte dell’operatore su Ufficio-PC.
Nel caso di chiusura attiva su Ufficio-PC (e quindi chiusura passiva su sccver), prima di tutto viene chiuso
l’output stream, viene poi attesa la fine del thread di ricezione (perché il thread di ricezione termini, verrà
spiegato studiando i dettagli del codice di Receiver), infine viene invocato il metodo termina dell’istanza di
ExitObject, che determina la fine dell’applicazione.
7
Il codice Socket cs = new Socket(remHost,remPort) equivale alle tre seguenti righe:
SocketAddress remEndpoint = new InetSocketAddress(remHost,remPort);
Socket cs = new Socket();
[Link](remEndpoint);
pag. 59
Advanced Java rev. 1.0 Novembre 2012
La classe Receiver.
La classe Receiver è subtipo di Thread; il suo costruttore, invocato dall’istanza di Telephone alla partenza,
salva i valori di locPort e backlog nei membri locali port e backlog, e salva il riferimento all’istanza di
ExitObject nel membro locale exitObj.
Il lavoro di ricezione è compiuto dal metodo run, che parte subito dopo l’istanziazione della classe Receiver:
@Override
public void run()
{
ServerSocket ascolto = null;
try
{
ascolto = new ServerSocket(port,backlog);
}
catch (IOException e)
{
[Link]("ServerSocket creation error:" +[Link]());
[Link](cs);
}
[Link]("ServerSocket creation: ascolto=" +ascolto);
Socket rets = null;
Timer tim = new Timer(true);
[Link](new UnrespTask(ascolto),1500000);
try
{
rets = [Link]();
}
catch (IOException e)
{
[Link]("accept error: "+[Link]());
[Link](cs);
}
[Link](rets, ascolto);
[Link]();
[Link]("Si e' connesso: rets=" + rets);
InputStream is = null;
try
{
is = [Link]();
}
catch (IOException e)
{
[Link]("rets getInputStream error: "+[Link]());
[Link](cs);
}
byte b[] = new byte[100];
boolean fine_ricevuta = false;
int n = 0;
do
{
try
{
n = [Link](b);
}
catch (IOException e)
{
[Link]("rets read error: "+[Link]());
[Link](cs);
}
if (n != -1)
{
StringBuffer sb = new StringBuffer("");
for (int i=0;i<n;i++) [Link]((char)b[i]);
[Link]("n="+n+": "+[Link]());
if ([Link]().matches("fine"))
pag. 60
Advanced Java rev. 1.0 Novembre 2012
{
[Link]("L'interlocutore ha riagganciato");
fine_ricevuta = true;
}
}
}
while (n != -1);
try
{
[Link]();
}
catch (IOException e)
{
[Link]("rets inputStream closing error: "+[Link]());
[Link](cs);
}
if (fine_ricevuta) [Link](cs);
}
Viene creato il server socket ascolto8; il parametro backlog determina il massimo numero di connessioni che
il server socket è in grado di accodare.
Supponendo che il server socket ascolto sia stato creato senza problemi, viene richiamato il suo metodo
accept: questo resta bloccato in ascolto finchè un client non si connetterà all’endpoint locale d’ascolto
<localhost:locPort>, che nello scenario è <Ufficio-PC:15000>; la natura bloccante dei metodi accept e read
rende necessario l’uso di un thread di ricezione separato dal thread principale.
Tuttavia va previsto il caso in cui la controparte, cioè sccver nello scenario esaminato, non si connetta:
questo è il motivo per cui, prima di invocare il metodo accept, il codice del metodo run crea il timer tim e ne
schedula l’attivazione di lì a 15 secondi.
Nel caso sccver non si connetta, allo scadere del delay di 15 secondi viene eseguito il metodo run
dell’oggetto di tipo UnrespTask, subtipo di TimerTask: il server socket ascolto, passato come parametro nel
costruttore dell’oggetto UnrespTask, viene chiuso, e l’applicazione terminata dall’istruzione [Link](0).
Un task attivato da timer dev’essere implementato come un subtipo della classe astratta TimerTask, che
discende direttamente da Object e implementa l’interfaccia Runnable; bisogna quindi scrivere il metodo run.
Allo scadere del timer, il metodo run dell’oggetto entra in esecuzione, in un thread apposito (qui, un thread
diverso sia dal thread principale in cui esegue il metodo main di Telephone, sia dal thread di ricezione in cui
esegue il metodo run della classe Receiver).
Nel mio scenario di riferimento, lancio l’applicazione anche sul computer sccver: java –jar [Link]
Ufficio-PC 16000 15000, quindi remHost = Ufficio-PC e remPort = 15000, locPort = 16000; in questo modo,
nell’applicazione Telefono su Ufficio-PC il metodo accept va a buon fine, e restituisce il socket rets, detto
returning socket.
Il codice passa a exitObj i riferimenti ai socket rets e ascolto, invocando il metodo setServerSocket
dell’oggetto ExitObject, il metodo termina dell’oggetto exitObj chiude infatti rets e ascolto quando invocato.
Il timer viene cancellato, giunti a questo punto esso non serve più; trattandosi di un timer one-shot e non di
un timer ciclico, esso non scatterebbe più, tuttavia è buona norma eliminarlo in ogni caso.
Il socket rets è pronto alla ricezione; allo scopo, viene recuperato l’input stream entro la variabile is, e viene
allocato il buffer b (array di 100 byte) destinato a contenere la ricezione; se il messaggio ricevuto supera
l’ampiezza del buffer, vengono effettuate due letture consecutive.
Prima del codice che implementa il ciclo di ricezione, viene definita la variabile booleana fine_ricevuta,
settata a false.
8
Il codice ServerSocket ascolto = new ServerSocket(port) equivale alle tre seguenti righe:
SocketAddress locEndpoint = new InetSocketAddress(“localhost”,port);
ServerSocket ascolto = new ServerSocket();
[Link](locEndpoint);
pag. 61
Advanced Java rev. 1.0 Novembre 2012
Il ciclo di ricezione ha la forma di un do-while; l’istruzione centrale è la lettura dall’input stream di rets,
[Link], che è bloccante, ossia ritorna quando avviene la lettura, e restituisce in n, variabile intera, il numero
di byte letti. Ciò che è stato ricevuto viene quindi stampato a video, premettendo il numero n di byte ricevuti.
Tuttavia l’istruzione [Link] può restituire n = -1, valore che determina la fine del ciclo do-while; tale valore
rappresenta l’end of file, e viene ricevuto quando la controparte chiude l’output stream del client socket cs;
questo punto, molto importante, verrà ulteriormente discusso e chiarito nel seguito.
Se la stringa ricevuta coincide con “fine”, allora siamo nel caso di chiusura passiva: la variabile booleana
fine_ricevuta viene settata a true, mentre l’uscita dal ciclo avviene al ciclo seguente, quando [Link]
restituisce n = -1.
Nel caso di chiusura attiva, il ciclo di ricezione termina per lo stesso motivo, però in questo caso il valore di
fine_ricevuta rimane false.
In uscita dal ciclo di ricezione, il codice del metodo run chiude l’input stream is, poi, nel solo caso di
fine_ricevuta true, cioè chiusura passiva, invoca il metodo termina dell’istanza di ExitObject, che determina
la fine dell’applicazione; altrimenti, nel caso di chiusura attiva, il metodo run si limita a terminare, e con esso
termina il thread di ricezione.
Può accadere che l’applicazione Telefono venga posta in ascolto su una porta già occupata da un’altra
applicazione, in questo caso la creazione del server socket ascolto non va a buon fine: l’eccezione che si
verifica, di tipo IOException, è intercettata dal codice di Receiver (tramite un costrutto try-catch) e gestita
dando un messaggio di errore e poi terminando sul nascere l’applicazione, mediante l’invocazione del
metodo termina dell’oggetto exitObj.
Un altro caso patologico importante concerne il comportamento che l’applicazione deve assumere quando la
comunicazione con l’interlocutore cade in modo non previsto, ad esempio a causa di un crash
dell’interlocutore oppure di una caduta della rete: in questi casi si verifica
Scenario di utilizzo.
Segue uno scenario concreto di utilizzo dell’applicazione tra due macchine distinte, poste sulla stessa rete.
Qui assumo che gli interlocutori siano newconfsrv e sccver, due macchine Linux, in rete locale.
Suppongo di lanciare per prima l’applicazione su newconfsrv: java –jar [Link] sccver 15000 16000, poi
(entro 15 secondi) quella su sccver: java –jar [Link] newconfsrv 16000 15000.
pag. 62
Advanced Java rev. 1.0 Novembre 2012
Come si vede dalle figure, newconfsrv crea il server socket sull’endpoint <newconfsrv:15000>; i tentativi di
creare il client socket cs non vanno a buon fine finchè sccver non parte e crea il suo server socket,
sull’endpoint <sccver:16000>. Quando ciò accade, la creazione del client socket cs su newconfsrv va a buon
fine, viene creato il socket che ha endpoint locale <newconfsrv:61287> ed è connesso all’endpoint remoto
<sccver:16000>.
Le cose vanno analogamente su sccver: qui la creazione del client socket va subito a buon fine, con
endpoint locale <sccver:43184>, e connessione all’endpoint remoto <newconfsrv:15000>.
Quando sccver ha creato il socket cs, il sistema ha assegnato la porta locale 43184, mentre l’endpoint
remoto è stato deciso dal codice – si tratta di <newconfsrv:15000>; il socket appena creato si è connesso
all’endpoint remoto: dal punto di vista dell’applicazione Telefono su newconfsrv, ciò si è tradotto in una
accept a buon fine del server socket ascolto: l’istruzione [Link] ha restituito il scoket rets. Il socket
rets su newconfsrv ha endpoint locale <newconfsrv:15000>, mentre l’endpoint remoto al quale è connesso è
l’endpoint locale del socket (remoto: è cs su sccver) che ha compiuto la accept, ossia <sccver:43184>.
Analogamente, il socket rets su sccver ha come endpoint locale <sccver:16000>, ed è connesso all’endpoint
remoto che è l’endpoint locale di cs su newconfsrv, cioè <newconfsrv:61287>.
Una volta avvenuta la creazione dei socket rets sulle due macchine, la comunicazione avviene tra questi
ultimi e i client socket cs; una volta stabilite le connessioni, potrei anche chiudere i server socket e la
comunicazione continuerebbe senza problemi.
15000 cs
rets 43184
L
ascolto
61287 rets
cs 16000
L
ascolto
newconfsrv sccver
pag. 63
Advanced Java rev. 1.0 Novembre 2012
Lo scambio di messaggi avviene quindi tra le coppie rets, cs. Nello scenario, il primo a scrivere è sccver
(scrive ciao, chi sei ?), ciò significa che la stringa è scritta sull’output stream di sccver-cs ed è ricevuta
sull’input stream di newconfsrv-rets. Analogamente, la risposta di newconfsrv (stringa ciao sccver, sono
newconfsrv) è scritta sull’output stream di newconfsrv-cs ed è ricevuta sull’input stream di sccver-rets. Non
c’è sincronismo, le due applicazioni possono parlarsi addosso.
Infine, l’operatore su sccver scrive la stringa fine: questa determina la fine del loop di invio e l’immediata
chiusura dell’output stream di sccver-cs; fatto ciò, l’applicazione su sccver attende l’attesa della fine del
proprio thread di ricezione, testando [Link], inizialmente true. Che accade nel frattempo ?
La stringa fine viene ricevuta sull’input stream di newconfsrv-rets: dopo la stampa a video e la scritta
L’interlocutore ha riagganciato, viene posta fine_ricevuta = true, quindi viene eseguito un altro ciclo:
stavolta l’istruzione [Link], lettura dall’input stream di newconfsrv-rets, restituisce -1, perché su sccver è
stato chiuso l’output stream di cs. Ciò provoca l’uscita dal ciclo di ricezione di newconfsrv.
Sempre su newconfsrv, dopo l’uscita dal ciclo di ricezione, il codice del metodo run di Receiver scopre che
fine_ricevuta è true, quindi chiude l’input stream di newconfsrv-rets e subito dopo invoca [Link],
che chiude tutti i socket ancora aperti e pone fine all’applicazione.
La chiusura dei socket e dei loro stream su newconfsrv fa sì che nel ciclo di ricezione su sccver, l’istruzione
[Link], lettura dall’input stream di sccver-rets, restituisca -1: così avviene l’uscita dal ciclo, con fine_ricevuta
a false; con ciò, il codice del metodo run di Receiver su sccver si limita a chiudere l’input stream di sccver-
rets e termina (senza invocare [Link]).
A questo punto, nel main di Telephone, sempre su sccver, l’istruzione [Link] restituisce false, così mil
codice prosegue invocando [Link], che pone fine all’applicazione anche su sccver.
Il socket permette lo scambio di dati tra i due endpoint, mediante uno stream di input e uno di output: lo
stream di input, sul quale si dice che il socket legge, va dall’endpoint remoto all’endpoint locale, mentre lo
stream di output, sul quale si dice che il socket scrive, va nel senso opposto.
Il server socket è una comodità messa a disposizione da alcune piattaforme, tra le quali Java; in pratica è un
socket che vede solamente un endpoint locale. Il server socket, una volta istanziato, attende connessioni da
parte dei (client) socket, che indirizzano l’endpoint gestito dal server socket come loro endpoint remoto:
quando il server socket accetta la connessione di un client socket, viene istanziato un socket, detto returning
socket, che da quel momento in poi dialoga con il client socket. Il returning socket vede come endpoint
locale quello del server socket, e come endpoint remoto l’endpoint locale del client socket connesso.
Il server socket restituisce un returning socket per ogni singola connessione, non per singolo client; se lo
stesso client effettua tre connessioni, vengono restituiti tre returning socket distinti.
La classe che implementa un socket è [Link], quella che implementa un server socket è
[Link].
pag. 64
Advanced Java rev. 1.0 Novembre 2012
L’applicazione console EmployeeServer è costruita tramite un Java project; ha due package, app contenente
le classi Server e RemoteEmployeeClass, e rem, che contiene la classe Empl e l’interfaccia
RemoteEmployeeInterface.
Il build file è semplicissimo, minimale, con le voci <clean>, <init>, <compile> e <jarGen> minimali; c’è anche
il tag <copy>, che scrive il JAR file in ExeJar. In quanto al lancio, è sufficiente il comando seguente, eseguito
in un command prompt in ExeJar: java –jar [Link] %1.
Il metodo main alla partenza legge tutti i dati della table PEOPLE (database DBPeople) entro il membro
statico Empl[] employees della classe Server; la classe Empl appartiene al package rem:
public class Empl implements Serializable
{
public int m;
public String nome;
public String cognome;
public String CF;
public Empl()
{
m = 0;
nome = cognome = CF = "ND";
}
@Override
public String toString()
{
return m + " " + nome + " " + cognome + " " + CF;
}
}
Letti i dati, la connessione con Oracle viene chiusa, tutte le ricerche avverranno sui dati in memoria.
Ogni classe usata per il trasporto dei dati in RMI deve implementare l’interfaccia Serializable, altrimenti si
verifica un’eccezione runtime, NotSerializableException, ogni volta che un’istanza della classe viene inviata.
L’architettura RMI prevede un object registry, cioè una struttura temporanea nella quale le applicazioni
possono immagazzinare o reperire i riferimenti a oggetti remoti; il riferimento a un oggetto remoto nel registry
è di tipo nominativo: il server che vi scrive il riferimento all’oggetto assegna al riferimento un nome
(arbitrario), che i client dovranno utilizzare per accedere i metodi dell’oggetto remoto.
Il registry viene acceduto mediante un endpoint noto, con porta default 1099; l’applicazione EmployeeServer
utilizza localhost e porta 1000, tuttavia può essere passato un diverso valore della sola porta mediante il
parametro di passaggio %1 al lancio dell’applicazione, letto nel main come args[0].
Il registry non è una struttura permanente; il registry viene creato da un’applicazione server S, che lo mette a
disposizione sull’endpoint <localhost:port> e vi scrive dentro (operazione bind) riferimenti a uno o più oggetti
remoti. Ogni altra applicazione A può accedere il registry e referenziare gli oggetti, il che ne fa
un’applicazione client; ma se A, una volta acceduto il registry, cerca di comportarsi da server scrivendovi
dentro il riferimento ad un oggetto ossia tentando un’operazione bind, allora A riceve un errore bloccante, di
tipo BindException.
Quindi il registry è strettamente associato all’applicazione server S che l’ha creato, e quando S termina, il
registry viene disallocato e i tentativi dei client di referenziare in esso un oggetto falliscono. Alla ripartenza di
S, viene creato un nuovo registry, sia pure sullo stesso endpoint, vuoto: non è possibile salvare i riferimenti.
pag. 65
Advanced Java rev. 1.0 Novembre 2012
In generale, un oggetto remoto si presenta al mondo dei client tramite un’interfaccia, di cui l’applicazione
server fornisce un’implementazione attraverso una propria classe. Qui, l’interfaccia è definita nel package
rem, nel file [Link]; essa espone i metodi ammissibili di invocazione remota:
public interface RemoteEmployeeInterface extends Remote
{
public Empl getEmployeeByMatr(int m) throws RemoteException;
public Empl[] getEmployeeByRange(int mIni,int mFin) throws
RemoteException;
public Empl[] getEmployeeByName(String s) throws RemoteException;
}
ciascun metodo deve avere la clausola throws RemoteException, necessariamente, pena errori runtime
di tipo IllegalArgumentException che impediscono la partenza dell’applicazione.
Ha due membri privati, listaEmpl di tipo Empl[] e l’intero nEmpl; il costruttore riceve l’array con i dati, e con
tali dati riempie listaEmpl e valorizza nEmpl, così l’istanza di RemoteEmployeeClass dispone dei dati letti
dalla table PEOPLE, e i tre metodi ricavano di lì i dati richiesti.
Il primo metodo restituisce un oggetto di tipo Empl in base al match sulla matricola passata; il secondo
restituisce un array Empl[] che comprende tutti gli impiegati aventi matricola nel range passato; il terzo
restituisce un array Empl[] che comprende tutti gli impiegati il cui cognome inizia con il parametro s.
Torno al main di Server: una volta creato l’oggetto remobj, viene prodotto lo stub dell’oggetto da esporre nel
registry, si dice che l’oggetto viene esportato:
RemoteEmployeeInterface estub =
(RemoteEmployeeInterface)[Link](remobj,port);
Dopo l’esportazione, lo stub è già pronto sulla porta a ricevere le chiamate e girarle all’oggetto server, però
per raggiungerlo, un client dovrebbe spedire messaggi direttamente all’endpoint <localhost:port>, ciò che
manca è l’associazione del nome simbolico allo stub e quindi all’oggetto.
Si dice che il binding è il processo di assegnazione del nome simbolico all’oggetto, ma più precisamente il
binding è l’assegnazione di un nome simbolico visibile ai client allo stub già in ascolto sull’endpoint
<localhost:port> del registry. Il nome simbolico è una stringa qualsiasi; qui non esistono namespace: anche
se si possono scegliere nomi come [Link], non si può dire che esista poi un namespace
VirgoSpace indipendente dagli oggetti che contiene, come invece è in Corba.
Eseguita la bind, l’oggetto server è disponibile alle chiamate dei client, e l’applicazione resta in attesa.
Quando il ciclo d’attesa viene interrotto, il codice del main esegue l’unbind dell’oggetto dal registry, ciò che
viene eliminato è solo il nome simbolico dal registry, anche se l’oggetto resta attivo finchè l’applicazione host
non termina: ciò significa che un client già connesso può continuare a invocarne i metodi, sebbene un nuovo
client non possa più raggiungerlo – esattamente come in Corba.
Adesso studio un’applicazione client prototipale, EmployeeClient, anch’essa di tipo console; è costruita
tramite un Java project, ha due package, app contenente la classe Client, e rem, completamente copiato
dall’omonimo package dell’applicazione server – ogni client deve fare ciò.
Il build file è semplicissimo, minimale, con le voci <clean>, <init>, <compile> e <jarGen> minimali; c’è anche
il tag <copy>, che scrive il JAR file in ExeJar. In quanto al lancio, è sufficiente il comando seguente, eseguito
in un command prompt in ExeJar: java –jar [Link] %1 %2.
pag. 66
Advanced Java rev. 1.0 Novembre 2012
Il metodo main alla partenza recupera gli eventuali parametri di passaggio, nome del nodo e porta sui quali è
presente il registry creato da EmployeeServer, entro i membri registryHost e registryPort; i valori default
sono localhost e 1000. I due membri vengono usati per reperire il registry:
Registry reg = [Link](registryHost,registryPort);
In realtà, il successo dell’operazione non è garanzia di correttezza, non si verificano errori nemmeno se
l’applicazione server non è in esecuzione e quindi il registry non esiste. Gli eventuali errori si verificano al
passaggio successivo: stub = (RemoteEmployeeInterface)[Link]("EmployeesObj"), ove
stub è membro privato della classe Client, di tipo RemoteEmployeeInterface.
Il metodo lookup è l’analogo RMI del metodo resolve di Corba: se tutto va bene, crea un oggetto stub
dell’oggetto remoto.
A questo punto l’applicazione entra nel ciclo indefinito di chiamata dei metodi dell’oggetto remoto: l’operatore
passa da tastiera una certa stringa, il codice del main tenta di interpretarla come un numero; se si tratta
davvero di un numero, lo assegna alla variabile intera n che usa come matricola di ricerca:
Empl e = [Link](n);
if (e == null) [Link]("Impiegato inesistente");
else [Link]("Impiegato: " + e);
Se invece si verifica un errore di formato, il codice del main invoca un altro metodo dell’oggetto remoto:
Empl[] eList = [Link](txt);
if (eList == null) [Link]("Nessun impiegato trovato");
else for (int i=0;i<[Link];i++) [Link](eList[i]);
In ultimo, c’è ancora un metodo fornito dall’interfaccia Registry, molto utile, che non è stato usato dalle due
applicazioni: è il metodo list, che restituisce un array di stringhe contenente tutti i nomi simbolici presenti nel
registry; ecco un esempio d’uso, ove reg è oggetto di tipo Registry: String[] srvs = [Link]().
Eventi remoti.
Faccio una premessa: aggiungo un contatore di richieste ricevute all’oggetto remoto, istanza della classe
RemoteEmployeeClass. Aggiungo alla classe RemoteEmployeeClass il membro privato, intero, cReq: il
contatore parte da 0 e aumenta di un’unità ogni volta che un metodo remoto viene invocato.
Voglio che l’oggetto remoto, istanza della classe RemoteEmployeeClass, al verificarsi di certe condizioni al
suo interno, possa effettuare la raise di eventi verso i client connessi; i client riceveranno dati d’evento, e
attiveranno event handler.
Ad esempio, voglio che l’oggetto remoto, ogni volta che un suo metodo, eseguendo la ricerca voluta da un
client, si imbatta in un impiegato la cui matricola è un quadrato (ad esempio 1 oppure 100 o 900), sollevi un
evento nel quale passa il nominativo (cioè cognome e nome separati da un blank, in un’unica stringa) e il
numero della richiesta (cioè il valore di cReq) che ha provocato la raise dell’evento.
Aggiungo alla classe RemoteEmployeeClass il metodo privato int isSquare(int m): se la matricola m è
un quadrato, ne restituisce la radice; altrimenti restituisce -1.
pag. 67
Advanced Java rev. 1.0 Novembre 2012
}
return null;
}
Effettuo un’analoga modifica negli altri metodi, getEmployeeByRange e getEmployeeByName: anche in essi,
ogni volta che il valore della matricola trattata è un quadrato, viene invocato il metodo fireSquareEvent;
siccome possono esserci più casi di matricole quadrate, una sola chiamata può sollevare più eventi.
Lato server.
Ecco la definizione del metodo (privato, non serve che sia visibile al di fuori della classe):
private void fireSquareEvent(String nominativo, int r)
{
... codice applicativo ...
}
Il codice applicativo del metodo fireSquareEvent deve realizzare i punti sopra definiti.
La classe dalla quale vengono istanziati gli oggetti evento, che chiamerò SquareEventObject, dev’essere un
subtipo della classe [Link]; creo la classe nel package rem, in quanto dovrà essere
ovviamente utilizzata anche dai client. Non serve farle implementare l’interfaccia Serializable, in quanto lo fa
già il supertipo EventObject.
Ecco come viene generata la classe SquareEventObject, avendo chiesto alla IDE di creare gli stub per i
costruttori e i metodi astratti derivati dal supertipo:
public class SquareEventObject extends EventObject
{
public SquareEventObject(Object source,String nomen,int rad, int
cnt)
{
super(source);
// TODO Auto-generated constructor stub
}
}
Il parametro source è tipico delle classi evento. A tutti gli effetti, è un dato d’evento.
Definisco i campi corrispondenti ai dati d’evento e le proprietà readonly usate dai client per accederli, e
riscrivo il costruttore (quello ereditato non mi basta):
public class SquareEventObject extends EventObject
{
private Object source = null;
private String nominativo = "";
private int radice = 0;
private int cntReqs = 0;
public SquareEventObject(Object source,String nomen,int rad, int
cnt)
{
super(source);
[Link] = source;
[Link] = nomen;
[Link] = rad;
[Link] = cnt;
}
public Object getSource()
{ return [Link]; }
public String getNominativo()
{ return [Link]; }
public int getRadice()
pag. 68
Advanced Java rev. 1.0 Novembre 2012
{ return [Link]; }
public int getCntReqs()
{ return [Link]; }
}
Quindi la prima riga del codice del metodo fireSquareEvent, che realizza il punto i), è la seguente:
private void fireSquareEvent(String nominativo, int r)
{
SquareEventObject eve = new SquareEventObject(this, nominativo, r,
cReq);
...
}
Esamino il punto ii): ogni client dell’oggetto di tipo RemoteEmployeeClass, interessato a ricevere l’evento
SquareEventObject, deve presentarsi all’oggetto come un listener dell’evento; quindi devo definire
un’interfaccia unica attraverso la quale l’oggetto server vede tutti i client, al di là delle loro specificità.
Ciò si traduce nella definizione dell’interfaccia SquareEventListener, che pongo ovviamente in rem (la
useranno i client); essa deve contenere la dichiarazione dell’event handler:
public interface SquareEventListener extends Remote
{
public void SquareEventHandler(SquareEventObject e) throws
RemoteException;
}
L’interfaccia deve estendere Remote, per ovvi motivi.
Dal punto ii) si evince che l’oggetto remoto deve avere al suo interno una lista dei client ai quali dovrà
mandare gli eventi; inoltre, ogni client si presenta all’oggetto attraverso l’interfaccia SquareEventListener.
Mettendo assieme le due proposizioni, segue che la classe RemoteEmployeeClass deve avere un membro
definito come private Vector<SquareEventListener> vListeners = null.
L’oggetto deve creare il vettore, vuoto, quando viene istanziato; quindi aggiungo al costruttore di
RemoteEmployeeClass l’istruzione vListeners = new Vector<SquareEventListener>().
L’aggiunta degli elementi al vettore non è responsabilità dell’oggetto server, ma sarà fatta dai client che si
registreranno per ricevere gli eventi.
Ecco il codice finale del metodo fireSquareEvent, che scorre la lista dei listener e per ogni listener trovato
invoca l’event handler:
private void fireSquareEvent(String nominativo, int r)
{
SquareEventObject eve = new SquareEventObject(this, nominativo, r,
cReq);
ListIterator<SquareEventListener> ite = [Link]();
while ([Link]())
{
try
{
[Link]().SquareEventHandler(eve);
}
catch (Exception exc)
{
<segnalazione d’errore>
}
}
}
Resta ancora un punto importante: i client devono potersi registrare e anche deregistrare in merito alla
ricezione degli eventi, cioè aggiungere e togliere elementi al vettore vListeners. Quest’ultimo è privato, ed è
giusto che sia così, la soluzione corretta è fornire due metodi pubblici, li aggiungo all’interfaccia che l’oggetto
remoto espone ai client, ossia l’interfaccia RemoteEmployeeInterface del package rem:
public interface RemoteEmployeeInterface extends Remote
pag. 69
Advanced Java rev. 1.0 Novembre 2012
{
public void addSquareEventListener(SquareEventListener sel) throws
RemoteException;
public void removeSquareEventListener(SquareEventListener sel) throws
RemoteException;
public Empl getEmployeeByMatr(int m) throws RemoteException;
public Empl[] getEmployeeByRange(int mIni,int mFin) throws RemoteException;
public Empl[] getEmployeeByName(String s) throws RemoteException;
}
La classe RemoteEmployeeClass implementa i due nuovi metodi:
@Override
public void addSquareEventListener(SquareEventListener sel) throws
RemoteException
{
[Link](sel);
}
@Override
public void removeSquareEventListener(SquareEventListener sel) throws
RemoteException
{
[Link](sel);
}
Questo completa le modifiche necessarie alla gestione degli eventi remoti. Si noti che la classe Server è
rimasta inalterata.
Lato client.
Adesso passo all’applicazione client, EmployeeClient: prima di tutto, aggiorno il suo package rem,
copiandovi quello di EmployeeServer.
Per poter gestire gli eventi di tipo SquareEventObject, la classe Client deve implementare l’interfaccia
SquareEventListener, e deve quindi implementarne il metodo SquareEventHandler:
@Override
public void SquareEventHandler(SquareEventObject e)
{
[Link]("L’impiegato " + [Link]() +
" ha radice(matricola) = " + [Link]() +
" <richiesta n. " + [Link]() + ">");
}
Ecco come si presentano le applicazioni, una volta funzionanti – la figura seguente è un’anticipazione, la
prima finestra è quella in cui è attivo EmployeeServer, mentre le altre due mostrano due istanze
contemporanee dell’applicazione EmployeeClient:
pag. 70
Advanced Java rev. 1.0 Novembre 2012
Come si può vedere, la prima istanza client ha scatenato l’evento, invocando il metodo getEmployeeByMatr
su un impiegato avente matricola quadrata: l’evento è evidentemente giunto a entrambe le istanze client,
nonostante la seconda istanza non avesse ancora invocato alcun metodo.
Torno al codice del client. La classe client deve porre il proprio event listener nella lista dell’oggetto remoto,
per farlo deve invocare il metodo [Link], prima di entrare nel ciclo di chiamata
dei metodi remoti. Inoltre deve invocare il metodo [Link] una volta uscito
dal ciclo e prima di terminare, in modo da rimuovere l’event listener, che altrimenti resterebbe come un ramo
secco nella lista dell’oggetto remoto, causando anzi un’eccezione su EmployeeServer.
C’è un problema: i metodi [Link] e [Link]
devono ricevere in ingresso un oggetto di tipo SquareEventListener, verosimilmente il riferimento al client
stesso, per cui la scelta più semplice sarebbe la scrittura di un’istruzione come
[Link](this). Tuttavia il codice della classe Client è contenuto nel metodo
statico main, perciò non si può usare this.
L’ostacolo è facilmente aggirabile, mediante il seguente codice (non serve definire un costruttore esplicito):
public static void main(String[] args)
{
... codice iniziale ...
Client cli = new Client();
[Link](cli);
... ciclo di chiamata dei metodi remoti ...
[Link](cli);
... codice finale ...
}
pag. 71
Advanced Java rev. 1.0 Novembre 2012
Posso far proseguire l’oggetto remoto, però il ripetersi dell’eccezione è causa di forte rallentamento.
Per ovviare a questo problema, è sufficiente che nel codice del metodo fireSquareEvent della classe
RemoteEmployeeClass venga effettuata la rimozione dell’event listener causa dell’errore:
private void fireSquareEvent(String nominativo, int r)
{
SquareEventObject eve = new SquareEventObject(this,
nominativo,r,cReq);
ListIterator<SquareEventListener> ite = [Link]();
while ([Link]())
{
try
{
[Link]().SquareEventHandler(eve);
}
catch (Exception exc)
{
<segnalazione d’errore>
[Link]();
}
}
}
La classe AppReflex ha i membri statici remEmplHost e remEmplPort, i cui default sono rispettivamente
localhost e 1000, ma possono assumere valori diversi, passati dalla riga di comando e assegnati nel main; i
due membri servono per localizzare il registry messo a disposizione da EmployeeServer.
Oltre ad essi, la classe dispone del membro privato: RemoteEmployeeInterface estub = null.
Alla partenza, viene eseguita la routine initialize, che alla fine compie la connessione col registry, la
risoluzione dell’oggetto server e l’export dell’istanza di AppReflex (this) come fruitore d’eventi:
Registry ereg = [Link](remEmplHost,remEmplPort);
estub = (RemoteEmployeeInterface)[Link]("EmployeesObj");
[Link](this,0);
ho trascurato la possibilità di errori.
pag. 72
Advanced Java rev. 1.0 Novembre 2012
Si può notare che manca ancora un tassello nella gestione degli eventi, cioè la registrazione dell’event
handler: quest’operazione tuttavia alla partenza non viene effettuata, per cui gli eventi non vengono ricevuti.
La dialog dispone di un tasto, eveBtn, e di una label affiancata, lblGE, che permettono ciclicamente
l’attivazione/disattivazione della ricezione eventi: inizialmente lblGE ha valore (scritta) Non attivi,
concordemente all’inattività della ricezione eventi; premendo il tasto, la label commuta il suo testo a Attivi,
e viene invocato il metodo (privato, della dialog) listenEvent, il quale esegue il codice di inizio della ricezione
eventi, ossia: [Link](this), al netto della gestione degli errori.
Se in seguito il tasto eveBtn viene premuto ancora, la label torna alla scritta Non attivi, e viene invocato il
metodo (privato, della dialog) disposeEvent, il quale esegue il codice di terminazione della ricezione eventi,
ossia: [Link](this), al netto della gestione degli errori.
Così, la ricezione degli eventi nell’applicazione AppReflex può essere abilitata o disabilitata a runtime quante
volte si vuole.
Nella figura soprastante, AppReflex ha la ricezione eventi attiva; la dialog non ha eseguito query sugli
impiegati (le textbox sono vuote), però un altro client di EmployeeServer l’ha fatto, e AppReflex riceve i dati
d’evento e li visualizza nella lista centrale.
pag. 73
Advanced Java rev. 1.0 Novembre 2012
Il programma [Link] (ORB Daemon) è un runtime Corba, ne è parte integrante il servizio di naming
persistente NameService. Il programma [Link] è un’applicazione console che permette le operazioni
di registrazione, deregistrazione, startup e shutdown di server persistenti. Il programma [Link] è l’utility
usata per generare le classi Java da un file IDL.
Attraverso due applicazioni, NativeServer e NativeClient, mostrerò le principali feature di Corba nativo. Prima
di operare con Corba nativo, è necessario attivare orbd; nel mio modus operandi, mi posiziono con un
command prompt in ExeJar e do il comando d’attivazione: orbd –ORBInitialPort 1050, che attiva il
programma sull’host locale e sulla porta 1050, quest’ultima diviene la porta d’ascolto di NameService.
Il programma orbd è bloccante; se voglio lasciar libero il command prompt, devo premettere start al
comando d’attivazione; in ogni caso, orbd dev’essere terminato con ^C.
L’opzione –fall garantisce la generazione di tutte le classi, sia lato client sia lato server; l’opzione –td sta per
target directory, usandola compare il package cheers entro src. All’interno del package cheers compaiono i
tipici file Corba: le classi GreetingsHelper, GreetingsHolder, _GreetingsStub, GreetingsPOA, e le interfacce
GreetingsOperations e Greetings.
pag. 74
Advanced Java rev. 1.0 Novembre 2012
al posto del predefinito return null, scrivo il codice effettivo dei due metodi:
public String say_hello(String nomeCli, String msg)
{
return "Hello Client " + nomeCli + ", hai scritto " +
new StringBuilder(msg).reverse();
}
public String say_cnthello(String nomeCli, long cnt)
{
return "Hello Client " + nomeCli + ", hai passato il numero " +
[Link](cnt);
}
Qui ho usato il sistema d’indirizzamento corbaloc, con la specificazione d’uso di GIOP 1.2 (il default sarebbe
GIOP 1.0). Avrei potuto usare due diverse proprietà:
[Link]("[Link]", [Link](NSdoor));
[Link]("[Link]", NShost);
Notare: a differenza di quanto accade con Orbacus, scrivendo [Link]((String[])null, props) non ci
sarebbero stati problemi.
Seguono la risoluzione e il narrow di RootPOA, quindi l’invocazione del metodo activate del POAManager
(con omissione della gestione errori):
[Link] obj = orb.resolve_initial_references("RootPOA");
[Link] rootPOA =
[Link](obj);
[Link] manager = rootPOA.the_POAManager();
[Link]();
se non venisse chiamato il metodo activate, tutto andrebbe apparentemente bene, ma alla fine le chiamate
dei client ai metodi dell’oggetto server resterebbero inservite.
pag. 75
Advanced Java rev. 1.0 Novembre 2012
try
{
obj = [Link](myCtx);
vs = [Link](obj);
}
catch ([Link]>[Link] e)
{
vs = nsRoot.bind_new_context(myCtx);
}
ho lasciato la sola gestione errori relativa alla non esistenza del contesto. In uscita dal codice precedente,
se non ci sono stati altri errori rispetto all’eventuale NotFound, l’oggetto vs rappresenta il contesto.
Segue quindi il vero e proprio bind dell’oggetto, ossia l’esposizione dello stub greetings nello spazio dei nomi
di NameService, all’interno del contesto SpazioNativo, con il nome simbolico ServerGreetings:
String srvName = "ServerGreetings";
NameComponent[] myObj = new NameComponent[1];
myObj[0] = new NameComponent();
myObj[0].id = srvName;
myObj[0].kind = "Singleton";
[Link](myObj, greetings);
il metodo rebind agisce come bind, ma non dà errore se c’è già un’istanza dell’oggetto server (cioè, dello
stub), si limita a rimpiazzarla; questa scelta è utile se si teme la presenza di possibili stub rami secchi.
Per effetto dell’operazione di bind, l’oggetto server viene associato ad un nome simbolico, che i client
potranno utilizzare per accedere all’oggetto, servendosi ovviamente del servizio di naming.
La classe Taskent, appartenente al package app, estende TimerTask; il suo costruttore si limita a valorizzare
i membri interni, poi ogni volta che il timer scade (cioè ogni 10 secondi) viene eseguito il metodo run della
classe: viene eseguito il test di esistenza del file [Link], nella directory di lancio dell’applicazione (cioè in
ExeJar), e se il file non esiste, il metodo run termina; se invece il file esiste, viene cancellato e viene eseguito
il codice seguente:
NameComponent[] myObj = new NameComponent[1];
myObj[0] = new NameComponent();
myObj[0].id = srvName;
myObj[0].kind = "Singleton";
[Link](myObj);
[Link](true);
[Link]();
la prima parte si occupa dell’operazione di unbind dell’oggetto server, ossia cancella il riferimento simbolico
all’oggetto dallo spazio dei nomi, più precisamente dal contesto SpazioNativo. L’oggetto server è ancora
vivo, un client già connesso può continuare a invocarne i metodi, ma nessuna nuova connessione all’oggetto
tramite il suo nome simbolico può essere effettuata.
pag. 76
Advanced Java rev. 1.0 Novembre 2012
Poi viene invocato il metodo shutdown dell’oggetto ORB, e dopo viene invocato il metodo destroy
dell’oggetto ORB: l’oggetto ORB viene distrutto e quindi il thread precedentemente bloccato nella [Link]
viene sbloccato, e l’applicazione NativeServer termina.
Finora ho esposto il solo target <idlGen>; altri target sono <clean>, <init>, <compile>, <jarGen> e <copy>.
In caso di primo build, devo eseguire per primi <idlGen> e <init>, non importa in quale ordine; il target <init>
crea la consueta directory build.
Se si tratta di un build completo ma successivo al primo, allora inizio con il target <clean>, che elimina le
directory build e src/cheers, quest’ultima è la directory generata in src dall’azione del target <idlGen>; poi
eseguo <idlGen> e <init>, non importa l’ordine, come nel caso di primo build.
In caso di build completo, non importa se sia il primo o meno, proseguo con i target <compile>, <jarGen> e
<copy>, nell’ordine. Segue il codice ant, molto semplice:
<property name="[Link]" value="C:/LibJar/[Link]" />
<path id="[Link]">
<pathelement location="${[Link]}"/>
</path>
<target name="compile" description="Compilation target">
<javac includeantruntime="false" destdir="build" encoding="ISO-8859-
1">
<classpath>
<path refid="[Link]"/>
</classpath>
<src path="." />
</javac>
</target>
<target name="jarGen" description="Compression target">
<jar manifest="[Link]" jarfile="${appname}.jar"
basedir="build">
<zipfileset src="${[Link]}" />
</jar>
</target>
L’encoding in <javac> evita problemi di lettere accentate, che compaiono nei file generati da <idlGen>:
utilizzo come set di caratteri gli stessi usati dal progetto Eclipse. Il manifest si limita a referenziare laclasse
[Link] come starting point dell’applicazione.
Il lancio dell’applicazione è immediato: java –jar [Link] orbdHost orbdPort, se ho lanciato [Link]
sullo stesso host dell’applicazione e la porta è 1050 posso omettere i due parametri di lancio.
Il build file ha il target <idlGen>, identico all’omonimo presente nel build file del progetto NativeServer.
pag. 77
Advanced Java rev. 1.0 Novembre 2012
Trattandosi di applicazione client, è assente tutto il codice di gestione relativo a RootPOA e POAManager, si
passa direttamente al collegamento al NameService, che avviene con lo stesso codice impiegato nell’ambito
del progetto NativeServer.
Poi il codice del metodo main di Client risolve il contesto SpazioNativo, è lo stesso codice impiegato
nell’ambito del progetto NativeServer – con la differenza che stavolta il contesto deve essere presente, se
non lo fosse l’applicazione terminerebbe, anziché tentare di creare il contesto mancante.
Se la risoluzione,comprensiva di narrow, va a buon fine, greetings è un valido stub per l’oggetto remoto, ed è
possibile l’invocazione dei metodi servendosi di esso; ciò viene fatto nel ciclo di funzionamento a regime
dell’applicazione NativeClient: si tratta di un ciclo di durata indefinita, nel quale l’applicazione entra appena
ha risolto l’oggetto server; nel ciclo, l’applicazione attende l’input da tastiera: se viene passato il carattere x,
l’applicazione esce dal ciclo, altrimenti utilizza l’input (stringa) come parametro di passaggio da passare al
metodo invocato.
L’input da tastiera, contenuto nella variabile stringa input, viene convertito come se si trattasse di un intero
lungo (cioè di tipo long): se la conversione riesce, il valore viene assegnato alla variabile cnt di tipo long e la
chiamata è String result = greetings.say_cnthello(nomeClient,cnt); se la conversione fallisce, viene eseguita
String result = greetings.say_hello(nomeClient,input). Segue in ambo i casi la stampa a video di result.
Il file [Link] del progetto NativeClient è uguale al file [Link] del progetto NativeServer, con un’unica
differenza, cioè il nome del progetto. Anche le sequenze d’uso dei vari target sono identiche. Rimando quindi
alla descrizione data nel progetto NativeServer.
Il lancio dell’applicazione è immediato: java –jar [Link] nomeClient orbdHost orbdPort; se accetto il
nome default del client, ClientNativo, posso omettere il primo parametro di lancio, mentre se ho lanciato
[Link] sullo stesso host dell’applicazione e la porta è 1050 posso omettere gli ultimi due parametri.
pag. 78
Advanced Java rev. 1.0 Novembre 2012
Ho scaricato i sorgenti di Java, disponibili nel download di JDK (versione 1.7). Poi ho richiesto ad un
programma che usa Corba nativo, ad esempio NativeClient, di informare a quale classe appartenga l’oggetto
restituito dalla chiamata [Link]:
ORB orb = [Link](new String[]{}, props);
[Link]("ORB type = "+[Link]().getName());
Nel codice della classe ORBImpl non trovo riferimenti interessanti al protocollo GIOP, così provo a
esaminare il suo supertipo, la classe [Link], della quale trovo il sorgente in
JavaSrc/com/sun/corba/se/spi/orb/[Link]. Questa classe non va confusa con la classe astratta
[Link], importata nel codice applicativo di NativeServer e NativeClient.
Perciò, per stampare i messaggi GIOP è sufficiente ridefinire il membro giopDebugFlag della classe
[Link], come public boolean giopDebugFlag = true.
Soluzione adottata.
Non ho voluto modificare i sorgenti originali di Java e rigenerare la JVM, via assai discutibile; ho preferito
creare una mia versione della classe [Link], identica all’originale tranne che
nel valore del membro pubblico giopDebugFlag.
Entro il progetto Uti, ho creato il package [Link] e in esso ho creato la classe ORB,
copiando il codice dell’originale e modificando solamente il valore di giopDebugFlag, posto true. Ho quindi
rigenerato la libreria [Link] in LibJar.
Per poter vedere il dump dei messaggi GIOP, mi basta lanciare le applicazioni (NativeServer, NativeClient)
usando l’opzione –Xbootclasspath/p:LibJar/[Link].
l’output viene scritto entro il file di testo [Link], consentendo una comoda analisi dei messaggi GIOP.
pag. 79
Advanced Java rev. 1.0 Novembre 2012
La narrow di NameService invece porta ad uno scambio di quattro messaggi tra NativeServer e ORBD: una
prima richiesta da parte di NativeServer, al quale ORBD risponde con un location forward, quindi seconda
richiesta da parte di NativeServer e risposta finale da parte di ORBD.
Dal GIOP message header si vede che la versione di protocollo utilizzata è proprio la 1.2, e che si tratta di
un messaggio di richiesta.
Analizzando il Request header, si scopre che come Addressing Disposition è usata la object key, però viene
passata da NativeServer non l’effettivo valore di object key (quale potrei leggere dal IOR di ORBD), ma la
sola stringa NameService. L’operazione richiesta è _is_a, e il suo parametro è passato nel Request body, ed
è IDL:[Link]/CosNaming/NamingContextExt:1.0. Tornando al Request header, dopo il nome
dell’operazione e l’allineamento al paragrafo di 4 byte, segue la ServiceContextList, molto lunga.
pag. 80
Advanced Java rev. 1.0 Novembre 2012
Apparenza e navigazione.
La pagina iniziale del sito consente l’accesso a una pagina JSP, a un servlet e alla pagina statica
[Link]; da ciascuno dei tre rami, è possibile solamente il ritorno alla pagina iniziale (home page):
pag. 81
Advanced Java rev. 1.0 Novembre 2012
Posso, dalla pagina JSP, tornare alla home page e di lì accedere il servlet oppure la pagina Others.
All’interno della directory First, creo tre file: [Link], [Link], [Link] – posso anche pensarli
inizialmente vuoti; poi creo le cartelle images, META-INF e WEB-INF, e dentro WEB-INF creo le
sottodirectory classes e lib.
pag. 82
Advanced Java rev. 1.0 Novembre 2012
La struttura delineata contiene alcuni elementi obbligati, ossia tali che la loro mancanza impedisce alla
directory di fungere da ricettacolo per un sito ospitabile da Tomcat; il resto è opzionale; evidenzierò man
mano gli elementi obbligatori. Un analogo discorso varrà per i contenuti: ce ne saranno di obbligati, senza i
quali Tomcat non accetterà il sito, e di opzionali.
In First, il welcome file consiste di hyperlink alle altre due pagine e al servlet; inoltre [Link] carica
un’immagine, [Link], e contiene testo statico. Ecco il codice HTML di [Link]:
<html>
<head><title>Applicazione First</title></head>
<body bgcolor=white>
<table border="0">
<tr>
<td><img src="images/[Link]" width=125 height=200></td>
<td>
<h1>Applicazione web First</h1>
<p>Home page di un'applicazione accademica usata per<BR>
dimostrare l'organizzazione di un'applicazione web
dinamica.
</td>
</tr>
</table>
<p>Per provare che l'applicazione funziona, si possono usare i seguenti
link:
<ul>
<li>A una <a href="[Link]">pagina JSP</a>.
<li>A un <a href="Bye">servlet</a>.
</ul>
<p>...oppure si può provare questo:<br>
<a href="[Link]">Accesso a Others</a>
</body>
</html>
pag. 83
Advanced Java rev. 1.0 Novembre 2012
La presenza della sottodirectory images non è obbligatoria, in quanto non sono obbligatorie le immagini e
non è obbligatorio porle in una directory separata; è convenzione comune tuttavia porre le immagini, quando
presenti, in una sottodirectory separata, e il nome images è il più comunemente scelto.
Il codice HTML per l’indirizzamento delle pagine [Link] e [Link] è del tutto evidente; tratterò più
avanti il codice di indirizzamento del servlet, meno immediato.
La pagina [Link] è davvero banale, non serve esporne il codice HTML.
Anche la pagina [Link] è molto semplice, con un minimo di codice Java in mezzo al codice HTML:
<html>
<head><title>Pagina JSP di saluti</title></head>
<body bgcolor=white>
<table border="0">
<tr>
<td align=center><img src="images/[Link]"></td>
<td>
<h1>Pagina JSP di saluti</h1>
output di una pagina JSP di prova
</td>
</tr>
</table>
<%= new String("Ciao, mondo Java") %>
<BR><A HREF="[Link]">Torna alla Home Page</A>
</body>
</html>
Ogni spiegazione sulla natura delle pagine JSP verrà fornita nel seguito.
La directory META-INF.
La directory META-INF non è necessaria, tuttavia è quasi sempre presente, e contiene (almeno) il manifest,
cioè il file [Link]; nel caso di First, il manifest è di nessun interesse.
La directory WEB-INF .
La directory WEB-INF non è strettamente necessaria, a meno che non siano presenti servlet o altre feature
che ne richiedono la presenza; la pagina [Link] non ne richiede la presenza. Nel caso di First, il servlet è
presente ed è usato dalla pagina [Link]; in tal caso, è obbligatorio che la directory WEB-INF esista e che
contenga la sottodirectory classes, a sua volta contenente la classe che implementa il servlet.
Inoltre WEB-INF contiene anche il file descrittivo [Link], file avente certi contenuti obbligatori; questo file
non è obbligatorio: nelle ultime versioni di servlet, conformi alle specifiche Servlet 3.0 – che Tomcat 7
implementa, il file [Link] può essere rimpiazzato (tutto o in parte) da apposite annotazioni. Tuttavia
l’applicazione web First non usa le annotazioni, quindi deve usare [Link].
Il servlet di First.
In generale, un servlet è un’applicazione, attiva in un web application server (come Tomcat), scritta in Java; il
servlet è quindi un componente server, capace di rispondere alle richieste dei client.
Dal punto di vista del sito First, il servlet è l’insieme di una classe entro WEB-INF/classes, completa del
proprio package, e di un file descrittivo entro WEB-INF, di nome [Link]; la classe che implementa il servlet
non può essere generica, ma deve soddisfare alcuni requisiti.
La locazione del codice sorgente della classe servlet è arbitraria, ma io seguo la seguente regola: creo la
sottodirectory sources entro tomcat-apps, e dentro sources creo la sottodirectory First, destinata ad ospitare
i sorgenti Java relativi al sito First.
Intendo assegnare il package [Link] alla classe servlet, perciò entro First creo le due sottodirectory,
poi creo la classe servlet (entro tomcat-apps/sources/First/app/servlets), che chiamo [Link].
pag. 84
Advanced Java rev. 1.0 Novembre 2012
First, il servlet viene acceduto tramite hyperlink dalla pagina [Link], quindi tramite HTTP GET, con ciò la
classe Ciao implementa il solo metodo doGet.
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
@SuppressWarnings("serial")
public class Ciao extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
[Link]("text/html");
PrintWriter out = [Link]();
[Link]("<html>");
[Link]("<body>");
[Link]("<h1>Ciao, mondo Java</h1>");
[Link]("<h2>è il migliore dei mondi possibili</h2>");
[Link]("<p><A HREF='[Link]'>Torna alla Home Page</A>");
[Link]("</body>");
[Link]("</html>");
}
}
L’istanza response dispone del metodo getWriter, che ritorna un oggetto di tipo PrintWriter, in grado di
spedire caratteri verso il client: tutto ciò che viene scritto sullo stream di output viene passato al browser, che
lo visualizza – ecco perché dev’essere formattato HTML. La risposta che il servlet dà al browser è una
pagina HTML, completa di hyperlink di ritorno all’home page.
Allo scopo di poter compilare la classe Ciao, è necessario che il classpath referenzi la libreria
Tomcat/lib/[Link], infatti le classi dei package [Link].* appartengono ad essa; segue il
comando di compilazione, dato da un command prompt posizionato in tomcat-apps/sources/First:
javac –classpath Tomcat/lib/[Link] app/servlets/[Link]
con ciò, viene creato il file [Link] nella directory tomcat-apps/sources/First/app/servlets. Il file [Link]
va quindi copiato nella directory tomcat-apps/First/WEB-INF/classes/app/servlets.
pag. 85
Advanced Java rev. 1.0 Novembre 2012
<servlet-mapping>
<servlet-name>CiaoServlet</servlet-name>
<url-pattern>/Bye</url-pattern>
</servlet-mapping>
</web-app>
Il servlet name è arbitrario, ma dev’essere lo stesso tra i due elementi, <servlet> e <servlet-mapping>.
L’elemento <servlet-class> entro <servlet> specifica quale sia la classe servlet, che deve trovarsi entro
WEB-INF/classes; l’elemento <url-pattern> all’interno di <servlet-mapping> assegna il link simbolico con cui
va referenziato il servlet da parte delle pagine client: assegnando /Bye (arbitrario), i client possono
raggiungere il servlet come [Link]
Ciò spiega perché la pagina [Link] acceda il servlet come <A HREF="Bye">: per una pagina che si
trova in First, il path relativo Bye equivale a First/Bye all’interno della root di Tomcat installato su user-pc, in
ascolto sulla porta 8081.
Anche un client sui generis come SocketClient può dialogare con il servlet, spedendo sulla porta 8081 di
USER-PC il seguente messaggio: GET /First/Bye HTTP/1.1
Host: user-pc
Tomcat risponde con un messaggio HTTP/1.1 200 OK, il cui body è la pagina HTML di risposta della servlet.
Effettuo il deployment di
[Link] utilizzando Tomcat
Manager: il sito First viene
creato ed è accessibile ai
client browser come un
qualsiasi sito Tomcat.
Nota Bene: il nome del sito non dipende assolutamente da quello della directory che conteneva inizialmente
i materiali, ma solamente dal nome del WAR file di cui s’è fatto il deployment.
Se avessi chiamato Primo la directory entro tomcat-apps, continuando però a chiamare First il WAR file
contenente i materiali della directory Primo, il sito continuerebbe a chiamarsi First (e in Tomcat/webapps
troverei ancora la directory First). Infatti il WAR file contiene i materiali della directory Primo, ma non la
directory Primo, per cui l’informazione sulla directory iniziale va persa.
pag. 86
Advanced Java rev. 1.0 Novembre 2012
Uso dell’annotazione.
Allo scopo di modificare il codice di [Link], creo la directory First2 entro tomcat-apps/sources,
inizialmente copia di tomcat-apps/sources/First, poi modifico [Link] introducendo l’annotazione:
@WebServlet("/Bye")
public class Ciao extends HttpServlet
{ ... }
A parte l’uso dell’annotazione, che definisce l’URL del servlet, analogamente a quanto fatto in First da
[Link], il codice della classe resta invariato. L’annotazione @WebServlet appartiene al package
[Link], e si trova nella libreria [Link].
Allo scopo, modifico la chiamata al servlet nella pagina [Link]: anziché <a
href="Bye">servlet</a>, scrivo <a href="Bye?
paruno=11&pardue=22&partre=Virgi">servlet</a>. La nuova chiamata pone cioè una query string
dopo il path del servlet: il carattere ? funge da separatore tra il path del servlet e la query string; la forma
della query string è una successione di coppie key=value, separate tra loro dal carattere &. Nel mio esempio,
la query string è paruno=11&pardue=22&partre=Virgi.
L’oggetto request ha il metodo getQueryString: se il servlet è stato invocato senza query string, come
continua ad essere nel caso venga chiamato dalla pagina [Link], allora qs è null e tutto va come nel
caso della vecchia applicazione, altrimenti mi aspetto che qs sia una successione di coppie key=value
separate da &; la classe MultiUso (package uti) della libreria LibJar/[Link] ha il metodo statico
QueryStringDecomposta, che trasforma la query string in un array di coppie key-value. Il codice del metodo
doGet effettua la stampa (formattata HTML) delle coppie presenti nella hash table.
pag. 87
Advanced Java rev. 1.0 Novembre 2012
Servlet standalone.
Finora ho considerato il servlet come parte integrante di un sito, sia esso First oppure Second nelle sue varie
versioni, tuttavia un servlet può anche essere standalone, ossia stare per conto suo. Di seguito, realizzo una
versione di servlet simile a quella finora utilizzata, ma standalone.
Costruzione ingenua del servlet standalone Ciao e del sito client LightSite.
Creo in tomcat-apps la directory Ciao, contenente la consueta META-INF con il manifest banale, e WEB-INF
contenente le directory lib e classes; entro classes creo la sottodirectory app/servlets, e in questa
sottodirectory copio il file [Link] già utilizzato nell’ultima versione del sito Second. Di conseguenza,
pongo la libreria LibJar/[Link] entro WEB-INF/lib.
Creo quindi [Link]:
pag. 88
Advanced Java rev. 1.0 Novembre 2012
Ridenomino [Link] in [Link] e ne faccio il deployment usando Tomcat Manager: il servlet standalone
Ciao è pronto a servire le richieste dei client.
Come primo client utilizzo SocketClient, che invia sulla porta 8081 di USER-PC il seguente messaggio:
GET /Ciao/Bye?paruno=11&pardue=22 HTTP/1.1
Host: user-pc
Tomcat risponde con un messaggio HTTP/1.1 200 OK, il cui body è la pagina HTML di risposta del servlet:
<html><body>
<h2>Ciao, mondo Java</h1>
<h3>è il migliore dei mondi possibili</h2>
<h3>paruno=11</h3>
<h3>pardue=22</h3>
<p><A HREF='[Link]'>Torna alla Home Page</A>
</body></html>
L’unico cambiamento è il mapping del servlet, non più /First/Bye ma /Ciao/Bye, ed è un cambiamento quasi
scontato, adesso il servlet “fa sito da sé”, è standalone.
Costruisco un sito client del servlet Ciao, il sito LightSite. Allo scopo, creo la directory tomcat-apps/Light
come copia dell’ultima versione di tomcat-apps/First2, quindi elimino la directory WEB-INF perché il sito non
utilizza servlet propri.
Modifico anche la pagina [Link]: al posto di <a href="Bye?
paruno=11&pardue=22&partre=Virgi">, scrivo <a href="[Link]
paruno=11&pardue=22&partre=Virgi">, in concordanza con quanto fatto usando SocketClient.
Un’analoga modifica va riportata in [Link].
Creo [Link], con i contenuti della directory tomcat-apps/Litght, poi lo ridenomino in [Link], e compio
il deployment con Tomcat Manager: il sito LightSite è pronto all’uso.
Tramite browser, accedo al sito: [Link] appare la pagina default [Link], e di qui
chiamo il servlet, che restituisce la pagina già vista. Quando però utilizzo l’hyperlink di ritorno alla home
page, ottengo un errore grave di pagina non trovata, con la descrizione The requested resource
(/Ciao/[Link]) is not available.
La spiegazione dell’errore è evidente, dopo la chiamata al servlet ci si trova nel contesto di quest’ultimo,
cioè /Ciao, non più in /LightSite.
Costruzione corretta del servlet standalone Ciao e del sito client LightSite.
Una soluzione correttamente funzionante e piuttosto elegante è quella basata sul passaggio al servlet, da
parte del client, del path della pagina cui fare ritorno.
Comincio con il servlet: la costruzione della directory tomcat-apps/Ciao e successivamente del file [Link]
resta invariata, ma cambia la classe Ciao e quindi cambia [Link] in WEB-INF/classes/app/servlets.
Creo in tomcat-apps/sources la directory Ciao, e al suo interno creo app/servlets, e vi copio la classe
[Link] già usata in precedenza, poi modifico il codice del metodo doGet:
protected void doGet(HttpServletRequest request, HttpServletResponse response)
pag. 89
Advanced Java rev. 1.0 Novembre 2012
Si suppone che il client abbia passato il proprio path nella query string, come valore della chiave caller: il
valore viene salvato in una variabile stringa locale e poi usato per comporre l’hyperlink di ritorno.
La classe [Link] viene compilata, mediante il comando già usato nell’ultima versione del sito Second,
dopodichè [Link] viene copiata entro tomcat-apps/Ciao/WEB-INF/classes/app/servlets, viene rigenerato
il file [Link] e viene compiuto il suo deployment tramite Tomcat Manager.
Per provare la modifica, riutilizzo SocketClient, che invia sulla porta 8081 di USER-PC il messaggio:
GET /Ciao/Bye?caller=[Link] HTTP/1.1
Host: user-pc
Tomcat risponde con un messaggio HTTP/1.1 200 OK, il cui body è la pagina HTML di risposta del servlet:
<html><body>
<h2>Ciao, mondo Java,</h2>
<h3>io sono un servlet standalone</h3>
<h3>caller=[Link]
<h3>par=val</h3>
<p><A HREF='http:// USER-PC:8081/LightSite/[Link]'>Torna indietro</A>
</body></html>
È il risultato desiderato, per cui il servlet Ciao può ritenersi correttamente modificato.
Adesso passo al sito client del servlet, LightSite. Mantengo la struttura già adottata al tentativo precedente,
ma apporto alcune modifiche alle pagine che indirizzano il servlet, ossia [Link] e [Link]. Ecco
come riscrivo [Link], in modo che essa passi il proprio path al servlet:
<html>
<head>
<Title>Pagina del subsito Other</Title>
<SCRIPT language=JavaScript>
function CallServlet()
{ var ego = [Link];
[Link]("[Link]
"&paruno=11&pardue=22&partre=Virgilio",'_self');
}
</SCRIPT>
</head>
<body>
<ul>
<li><A onclick="return CallServlet();"><U>Vai al servlet</U></A>
<li><A HREF="[Link]">Torna alla Home Page</A>
</ul>
</body>
</html>
pag. 90
Advanced Java rev. 1.0 Novembre 2012
Ho “svuotato” il tag <A> del suo attributo centrale, HREF, usandolo come se fosse un HTML button, cioè
demandando l’azione a uno script sull’evento di click; lo script effettua l’accesso al servlet; anzi, è
certamente consigliabile l’uso di un HTML button, sebbene al di fuori del mio attuale interesse.
Ecco come si presentano le pagine, a sinistra [Link] e a destra la pagina di risposta del servlet:
Stavolta l’hyperlink di ritorno funziona perfettamente, c’è il ritorno alla pagina chiamante, [Link].
Una modifica analoga dev’essere effettuata all’interno di [Link]; in questo caso tuttavia vedrei come
valore di caller la stringa [Link] il che ha senso trattandosi della pagina default del
sito, comunque il buon funzionamento del ritorno dalla pagina del servlet è ancora garantito.
Mostro nel seguito il servlet Matema, standalone, che fornisce dati e non pagine in risposta alle chiamate dei
client. Anche per questo servlet ho realizzato il solo metodo doGet; il client che lo interroga passa nella
query string il nome di una funzione e un valore, e il servlet restituisce il valore calcolato dalla funzione.
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
@SuppressWarnings("serial")
@WebServlet("/Eval")
public class Matema extends HttpServlet
{
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException,
IOException
pag. 91
Advanced Java rev. 1.0 Novembre 2012
Compilo col comando già usato nell’ultima versione del sito Second, ottenendo [Link]. Poi creo in
tomcat-apps la directory Matema con le sottodirectory META-INF, banale, e WEB-INF, che contiene
LibJar/[Link] in lib e la classe [Link] in classes/app. Genero come al solito [Link] e ne faccio il
deployment con Tomcat Manager: il servlet Matema è disponibile ai client.
Come primo client utilizzo SocketClient, che invia sulla porta 8081 di USER-PC il messaggio:
GET /Matema/Eval?Funzione=Fattoriale&Valore=7 HTTP/1.1
Host: user-pc
Tomcat risponde con un messaggio HTTP/1.1 200 OK, il cui body è semplicemente 5040; non si tratta di una
rappresentazione binaria del numero 5040, ma di una sua rappresentazione testuale come flusso di byte: se
pag. 92
Advanced Java rev. 1.0 Novembre 2012
esamino il dump della risposta del servlet, scopro che il body consiste della sequenza (lunga 4 byte) 35 30
34 30, seguita dal solito accapo 0D 0A.
Il fatto che viaggi la rappresentazione testuale del dato, originariamente del tipo primitivo long, è dovuto
all’uso dell’istruzione [Link](res) da parte del codice del metodo doGet del servlet, che opera una
implicita trasformazione testuale del dato binario.
Questo meccanismo è ancora più evidente se SocketClient richiama la funzione CreaComplesso, che opera
su oggetti di tipo [Link] e non su dati primitivi:
GET /Matema/Eval?Funzione=CreaComplesso&Valore=7 HTTP/1.1
Host: user-pc
Tomcat risponde con un messaggio HTTP/1.1 200 OK, il cui body è 6+8i; questa è la rappresentazione
testuale dell’oggetto di tipo Complex: l’istruzione [Link](res) implicitamente ha chiamato il metodo
toString dell’oggetto res di tipo Complex, quindi ha scritto la stringa risultante come sequenza di byte nel
body del messaggio poi spedito al client.
Notare: non tutti i servlet rispondono ai client passando i dati testualmente; è possibile che un servlet passi i
dati di risposta in formato XML, oppure in formato binario; in seguito, vedrò esempi di questi tipi.
Sull’evento di click del button, viene eseguita una routine scritta in JavaScript
<SCRIPT language=JavaScript>
function CallServlet()
{
var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
var conn = "[Link]
Matema/Eval?Funzione="+[Link]+"&Valore="+[Link];
[Link]("GET",conn,false);
[Link](null);
var result = [Link];
[Link] = res;
}
</SCRIPT>
Osservazione importante: il metodo di passaggio dati usato dal servlet Matema è applicabile quando è
disponibile un metodo toString adeguato per la classe del dato, e quando ci si accontenta di visualizzare i
dati, senza elaborarli ulteriormente. Ciò dipende dalla scelta implementata nel codice della classe servlet, la
pag. 93
Advanced Java rev. 1.0 Novembre 2012
classe scrive il dato direttamente sull’output stream; come si vedrà a breve, nel caso del servlet Booklet, un
servlet può passare dati in formato binario, spesso vantaggioso.
pag. 94
Advanced Java rev. 1.0 Novembre 2012
Il servlet Booklet.
Prima ho realizzato alcuni servlet direttamente, senza l’uso di tool; invece il servlet Booklet è realizzato
utilizzando Eclipse. L’uso di Eclipse semplifica moltissimo le operazioni.
Il servlet Booklet fornisce informazioni relative ai contenuti del database Libreria; al momento, gestisce
quattro tipi di interrogazione: la lista dei libri (denormalizzati) per range di codici libro oppure per nominativo
dell’autore, la lista degli autori il cui cognome inizia con una certa stringa data e la lista dei generi. Si tratta di
un sottoinsieme delle funzionalità dell’applicazione desktop GestioneLibreria; per ogni chiarimento sul
database Libreria, per i dettagli sulle interrogazioni effettuabili e per le osservazioni programmatiche
sull’accesso ai dati, rimando al documento L’applicazione [Link].
Lascio che il wizard non generi [Link] (la non generazione è il default, avendo scelto le specifiche 3.0).
La directory .settings contiene file di progetto, che non m’interessano, mentre la directory WebContent ha la
stessa struttura e gli stessi contenuti della sua omologa di progetto; invece build è differente: in Eclipse si
vede vuota, mentre nel file system è presente la sottodirectory classes, vuota.
I file sorgenti applicativi vengono ospitati entro src; prima creo il package app, poi in esso creo la classe
servlet, tramite il wizard NewOtherWebServlet; come sempre, la classe creata dal wizard è subtipo
della classe [Link].
Passo il nome della classe, Maine; non definisco parametri di inizializzazione nella seconda pagina del
wizard, e come URL Mappings cambio il default proposto dal wizard, ossia /Maine, in /Answers.
Vado alla terza pagina del wizard, che si presenta come nella figura seguente.
pag. 95
Advanced Java rev. 1.0 Novembre 2012
Oltre alle scelte proposte di default, doGet e doPost, aggiungo anche la generazione dei method stub per init
e destroy.
Per quanto concerne init e destroy, il codice si limita a scrivere in un log file l’atto della chiamata del metodo,
che in ambo i casi è associato ad un certo evento.
pag. 96
Advanced Java rev. 1.0 Novembre 2012
Come di consueto, in doGet e doPost viene scritto lo stesso codice, quindi un solo metodo viene
effettivamente implementato, ad esempio doGet, e l’altro metodo, in questo caso doPost, si limita a
richiamare il primo.
La gestione degli errori applicativi, come la mancanza di parametri di richiesta attesi, è stata omessa.
A prescindere che la richiesta sia di tipo GET oppure POST, il servlet si aspetta che venga sempre passato
un parametro chiamato Funzione. Le interrogazioni possibili sono quattro, una per ciascun valore
ammissibile del parametro Funzione, e ciascuna prevede parametri diversi, però la trattazione è
praticamente la stessa in tutti e quattro i casi.
pag. 97
Advanced Java rev. 1.0 Novembre 2012
Per l’accesso al database, vengono usati i metodi statici della classe DAO, appartenente al package dao; il
package dao è allo stesso livello del package app.
Considero il caso in cui il client abbia richiesto la funzione ListaLibrodnByAutore: il codice del metodo doGet
si attende che sia stato passato anche un parametro di nome Nominativo e ne recupera il valore, quindi
invoca il metodo apposito della classe DAO.
La classe Librodn appartiene al package dao e deve, ovviamente, essere serializzabile; analogamente,
devono esserlo le classi Autore e Genere, insomma le classi che viaggiano (DAO non ha bisogno di essere
serializzabile perché non viaggia).
Se la query non restituisce alcun item, l’array Librodn[] restituito è vuoto ma non null; è invece null in caso di
errori di varia natura.
Il codice del metodo doGet salva il risultato entro una variabile oggetto generico, writable, in modo da non
perdere di generalità: nel caso corrente, e di Funzione = ListaLibrodnByRangeCod, writable prende il
riferimento a un oggetto di tipo Librodn[]; nel caso di Funzione = ListaAutoreByNome, writbale prende il
riferimento a un array Autore[]; quando Funzione = ListaGeneri, ad un array Genere[].
Posto che writable non sia null, viene scritto sull’output stream dell’oggetto response, si tratta di una
serializzazione di un array d’oggetti; il metodo getWriter restituisce un oggetto PrintWriter, che scrive
caratteri (testo) su uno stream, invece il metodo getOutputStream restituisce un oggetto di tipo
ServletOutputStream10, che scrive dati binari.
La chiamata al metodo flush è necessaria, in quanto effettua il commit della risposta; la chiamata al metodo
close è comunque buona norma.
10
La classe ServletOutputStream è astratta, la classe effettivamente usata per l’oggetto restituito dal metodo
getOutputStream è [Link] (una classe di Tomcat).
pag. 98
Advanced Java rev. 1.0 Novembre 2012
:.
A questo punto, il deployment viene come di consueto effettuato usando Tomcat Manager.
Esaminando il WAR file, noto che la sua dimensione è molto contenuta; le librerie di Tomcat non servono,
essendo già disponibili in Tomcat/lib. Serve però la libreria [Link] per l’accesso al database Oracle.
Essendo di uso assai diffuso, ho copiato [Link] entro la directory Tomcat/shared/lib, ciò ne garantisce la
visibilità a tutte le applicazioni che la necessitano, come il servlet Booklet.
Richieste e risposte.
Il servlet Booklet è pronto a ricevere le richieste dai client, ma il metodo init non viene eseguito finchè non
giunge la prima richiesta da parte di un client, cioè Tomcat istanzia la classe servlet solamente quando
giunge una richiesta client. Questo comportamento, che è il default, può essere modificato utilizzando il
parametro loadOnStartup dell’annotazione WebServlet:
@WebServlet(urlPatterns="/Answers", loadOnStartup=1)
Con ciò, Tomcat istanzia la classe servlet di Booklet alla partenza, senza attendere richieste da parte dei
client; comunque non utilizzo questa possibilità.
pag. 99
Advanced Java rev. 1.0 Novembre 2012
69 63 74 69 6F 6E Codice=1,descrizione=Science Fiction
73 71 00 7E 00 02 Riferimento alla descrizione del tipo
00 00 00 02 74 00 06 48 6F 72 72 6F 72 Valori dei campi del secondo item:
Codice=2,descrizione=Horror
...
73 71 00 7E 00 02 Riferimento alla descrizione del tipo
00 00 03 E8 74 00 05 61 6C 74 72 6F Valori dei campi dell’ultimo item:
Codice=1000,descrizione=Altro
È evidente che una comune pagina HTML non può essere un buon client; ma può esserlo un applet.
Tuttavia non vedo apparire alcunchè nella colonna Display Name di Tomcat Manager.
Se invece aggiungo al progetto Eclipse, entro WebContent/WEB-INF, un file [Link] banale, come segue:
<web-app>
<display-name>Booklet Servlet</display-name>
</web-app>
allora in Tomcat Manager appare il nome desiderato nella colonna Display Name.
pag. 100
Advanced Java rev. 1.0 Novembre 2012
L’applet BookletApplet.
Così come un servlet è un’applicazione Java che opera entro un web server, un applet è un’applicazione
Java che opera entro un browser. Così come il web server deve avere una infrastruttura adeguata ad
ospitare il servlet, deve cioè essere un web application server, sul lato client, cioè nel browser, dev’essere
disponibile il Java plug-in allo scopo di mettere in esecuzione l’applet.
Il layout default del pane è di tipo BorderLayout, ma io uso un absolute layout per comodità. Utilizzando
WindowBuilder disegno i controlli, che automaticamente vengono aggiunti al default pane.
Il codice dei controlli aggiunti con la IDE finirebbe nel costruttore; tuttavia, dopo aver aggiunto un primo
controllo, la label statica “Cognome”, il cui codice va nel costruttore, sovrascrivo il metodo init e sposto il
codice relativo al controllo inserito, dal costruttore al metodo init
@Override
public void init()
{
[Link]();
JLabel lblNewLabel = new JLabel("Cognome:");
[Link](10, 47, 57, 21);
getContentPane().add(lblNewLabel);
}
di qui in poi, il codice di ogni controllo inserito con la IDE va entro il metodo init. Il metodo init non dev’essere
chiamato dal codice applicativo, in quanto viene già invocato dal browser al caricamento dell’applet.
pag. 101
Advanced Java rev. 1.0 Novembre 2012
Utilizzando WindowBuilder,
disegno i controlli, che
automaticamente vengono
aggiunti al content pane,
mentre il loro codice viene
scritto nel metodo init.
Se si sono verificati dei problemi, ad esempio se il database era inaccessibile, info è null, viene data una
segnalazione d’errore nella label in alto, e il metodo termina. Suppongo non sia andata così nel seguito.
Definisco la matrice dei dati da passare al costruttore del table model, sempre in generale:
Object[][] objs = new Object[[Link]][[Link]];
se la ricerca non ha dato risultati, allora info è vuoto (ma non null), però la trattazione non cambia.
pag. 102
Advanced Java rev. 1.0 Novembre 2012
Segue il codice di assegnazione dei nomi delle colonne e del caricamento della matrice objs, specifico della
ricerca degli autori (è un ramo di uno switch sulla funzione, cioè sul valore del parametro fun):
nomicols[1] = "Nome";
nomicols[2] = "Cognome";
for (Autore au : (Autore[])info)
{
objs[r][0] = [Link];
objs[r][1] = [Link];
objs[r][2] = [Link];
r++;
}
L’ultima parte del codice è ancora comune:
ldnTBM = new ReadonlyTableModel(objs,nomicols);
[Link](ldnTBM);
ColumnsSizing(fun);
AggiornaLabel(fun,[Link]);
il table model viene creato con i dati e i nomi delle colonne corretti, e viene assegnato alla table; vengono
infine aggiustate le ampiezze delle colonne della table (metodo ColumnsSizing) e viene aggiornato il testo
della label in alto, che riporta il numero di item trovati:
Build e deployment.
Il build file di un applet non presenta particolarità rispetto ai comuni build dei Java project: i tag sono i soliti,
nella sequenza nominale <init>, <compile>, <jarGen>; c’è il solito tag <clean>, per l’eliminazione della
working directory build.
In generale, gli applet vengono salvati in una directory virtuale del web server; io ho deciso di salvarli entro la
directory Tomcat/webapps/applets, così ho aggiunto al build file il tag <copy>, che salva l’applet nella
directory di cui sopra.
Non si può quindi parlare di un vero deployment.
pag. 103
Advanced Java rev. 1.0 Novembre 2012
Avrei potuto usare il tag <applet>, esattamente con la stessa parametrizzazione, ma le versioni più recenti di
HTML deprecano il tag <applet>, che nel seguito non sarà più supportato.
pag. 104
Advanced Java rev. 1.0 Novembre 2012
Definizione di Ajax.
Con Ajax, si intende un’architettura software, comprensiva di un certo insieme di componenti, rivolta allo
sviluppo della comunicazione tra browser e web server; l’architettura si colloca sul solo lato client.
Posto che l’obiettivo sia la realizzazione di pagine HTML che compiono interrogazioni a servizi web (siano
essi servlet o web service), le componenti essenziali dell’architettura Ajax sono tre:
HTML (con o senza l’ausilio di CSS) per la costruzione generale della pagina;
DOM (Document Object Model) per l’accesso agli oggetti HTML della pagina;
l’oggetto XMLHttpRequest per l’invio di richieste e la ricezione delle relative risposte.
In quanto a JavaScript e XML, si può parlare di componenti non strettamente essenziali, ma assai frequenti.
Per utilizzare l’oggetto XMLHttpRequest e anche il DOM, è necessario uno script language, e JavaScript è
una scelta non obbligata ma assai frequente. In quanto a XML, è certamente il formato più diffuso per i
messaggi di richiesta e risposta scambiati tra l’oggetto XMLHttpRequest e i servizi sul lato server, sebbene
possano essere usati anche altri standard, come JSON.
Prima si è utilizzato XMLHttpServlet per recuperare dati di tipo testuale, che infatti venivano restituiti nella
proprietà responseText; tuttavia il servlet Booklet restituisce dati binari, inadatti ad essere gestiti dall’oggetto
XMLHttpRequest. Volendo usare quest’ultimo, bisogna far sì che Booklet restituisca dati in formato XML,
oltre che in formato binario.
Allo scopo, introduco un nuovo parametro alle richieste: i client di Booklet potranno specificare l’aspettativa
di un formato XML della risposta; segue un esempio basato su una query string:
Booklet/Answers?Funzione=ListaLibrodnByAutore&Nominativo=S&XML=Y
Qui la funzione richiesta è la ricerca dei libri il cui autore ha cognome che inizia per S, e il client specifica al
servlet che vuole una risposta in formato XML.
Il codice del metodo doGet della classe servlet compie il test sul nuovo parametro XML: se e solo se il
parametro è presente ed ha valore Y, oppure y, la risposta avrà il formato XML; in caso contrario, la risposta
sarà in formato binario, e il codice non cambia rispetto alla precedente versione.
pag. 105
Advanced Java rev. 1.0 Novembre 2012
[Link]("<"+recTag+">");
[Link]("<"+recIte1+">"+lg[i].Codice+"</"+recIte1+">");
[Link]("<"+recIte2+">"+lg[i].descrizione+"</"+recIte2+">");
[Link]("</"+recTag+">");
}
break;
}
[Link]("</doc>");
[Link]();
ho mostrato il codice in dettaglio per la sola funzione ListaGeneri, ma è del tutto analogo negli altri casi,
cambiano solo i tag XML. Si tratta di un codice alquanto chiaro, autoesplicativo.
È molto importante settare il content type della risposta al valore text/xml (non è il default), altrimenti la
proprietà responseXML dell’oggetto XMLHttpRequest risulterà null. Va detto che la proprietà responseText
contiene comunque la risposta, sempre, come una lunga stringa XML, in effetti il servlet invia la risposta,
però rimane null la proprietà responseXML e quindi, a meno di non usare i dati in forma di stringa XML,
contenuti in responseText, il client non sa gestire la risposta ricevuta.
pag. 106
Advanced Java rev. 1.0 Novembre 2012
L’utente digita la stringa con la quale intende compiere la ricerca nella textbox presso Valore, seleziona la
funzione di ricerca desiderata usando la tendina a destra, e preme il button in basso: i risultati della ricerca
appaiono in una HTML table (inizialmente non visibile), e il numero di item trovati viene scritto nella textbox
readonly presso Risultato.
Nella figura successiva, ipotizzo che l’utente abbia compiuto la ricerca su tutti i libri il cui autore ha cognome
che inizia con la lettera M.
Si tratta di una pagina veramente dinamica, in quanto non ci sono controlli nascosti che divengono visibili,
ma il codice JavaScript riscrive il codice HTML della pagina in tempo reale, in base ai risultati ottenuti dalla
interrogazione effettuata.
Le performance della pagina sono decisamente soddisfacenti, inoltre, poiché la formattazione dei dati è
effettuata dal codice JavaScript della pagina client, il servlet si limita a fornire i dati grezzi (in formato XML),
evitando quindi un sovraccarico al web server.
pag. 107
Advanced Java rev. 1.0 Novembre 2012
Codice HTML.
Comincio da <head>, lasciando per adesso indicato il tag <script>:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-
8859-1">
<meta http-equiv="Cache-Control" content='no-cache'>
<title>Pagina client Ajax/XML di Booklet</title>
<script language="JavaScript">
...
</script>
</head>
Lo header Cache-Control settato a no-cache fa sì che la pagina sia sempre fresca, senza avvalersi di copie
in cache eventualmente presenti.
La prima table è senza nome e serve solamente come contenitore dei controlli HTML argo (ove l’utente
scrive la stringa di ricerca), Funzione (che permette di scegliere la funzione di ricerca) e risu (che mostra il
numero di item restituiti dalla ricerca).
La seconda table ha l’identificativo Griglia, in modo da essere facilmente indirizzabile dal codice JavaScript;
ha la stessa larghezza della prima table, per ragioni grafiche, ma nient’altro è definito, visto che la sua
gestione è dinamica, da parte del codice JavaScript.
L’ultima sezione della pagina definisce il button HTML utilizzato per compiere la ricerca: è qui che viene
chiamata la funzione JavaScript, CallServlet.
Codice JavaScript.
La function CallServlet prima prepara la richiesta, componendo l’URL con la query string, quindi invia la
richiesta, e poi processa la risposta: con i dati ricevuti dal servlet, costruisce le righe dati e le aggiunge alla
HTML table Griglia.
pag. 108
Advanced Java rev. 1.0 Novembre 2012
pag. 109
Advanced Java rev. 1.0 Novembre 2012
break;
case "ListaGeneri":
recTag = "Genere";
oggetto = "genere";
oggetti = "generi";
ite[1] = "Descrizione";
ite[2] = "";
w[1] = "90%";
w[2] = "0%";
break;
}
arrDati = [Link](recTag);
var nDati = [Link];
if (nDati == 0)
{ [Link] = "Nessun "+oggetto+" trovato";
return;
}
else [Link] = "Trovati "+nDati+" "+oggetti;
i dati in Domus sono organizzati per nodi XML: il root node è <doc>, che contiene una sequenza di nodi tutti
strutturalmente uguali, ad esempio nel caso di ricerca libri per autore, i nodi entro <doc> sono tutti
organizzati come <Libro>, che a sua volta contiene i nodi <Codice>, <Titolo> e <Autore>. Il nodo <Codice> è
presente in tutti i casi, sempre in prima posizione, ecco perché lo preassegno nel literal array ite.
L’istruzione [Link] crea l’array di nodi di secondo livello, figli di <doc>; il numero
di elementi dell’array, che è il numero di item trovati dalla ricerca, viene visualizzato entro risu. Se il numero
di item trovati è zero, è inutile proseguire con la costruzione della table.
pag. 110
Advanced Java rev. 1.0 Novembre 2012
Il codice HTML delle due pagine è identico, cambia però il codice JavaScript, che necessita una
riorganizzazione profonda: la funzione CallServlet in [Link] spedisce la richiesta e processa la
risposta; in [Link] la funzione CallServlet si limita a spedire la richiesta, mentre la ricezione
della risposta e il suo processing sono compiuti da un’altra funzione, la callback function resultProcessing.
La sezione a) contiene la dichiarazione var xmlHttp = null; sia la funzione di richiesta, CallServlet, che
la callback function, utilizzano l’oggetto XMLHttpRequest, e devono usare lo stesso oggetto, non due istanze
diverse. Quindi la dichiarazione dell’oggetto xmlHttp dev’essere condivisa11.
Il codice della funzione CallServlet è ancora suddivisibile in preparazione della richiesta e invio della
richiesta, con alcune sottili differenze rispetto alla funzione omonima in [Link]:
a) preparazione della richiesta
if (!xmlHttp) xmlHttp = new XMLHttpRequest();
...dalla seconda riga compresa in poi, come la sezione a) del caso sincrono
stavolta la connessione al servlet aperta dal metodo open ha modalità di colloquio asincrona, in base al
valore del terzo parametro.
L’oggetto XMLHttpRequest ha la proprietà readyState, che rappresenta l’evoluzione dello stato della
richiesta; la proprietà passa attraverso una sequenza di valori interi, da 0 (prima dell’invocazione del metodo
open) fino a 4, assunto al completamento della richiesta, ossia alla avvenuta risposta, con o senza errori. La
proprietà onreadystatechange rappresenta l’evento di variazione del valore della proprietà readyState, e la
callback function è un event handler, che viene eseguito ogni volta che readyState cambia valore.
la funzione viene invocata anche quando readyState passa (da 0) al valore 1 etc, ma deve fare qualcosa
solamente quando la richiesta è stata completata. Inoltre deve distinguere tra completamento corretto
(valore 200, tipico del protocollo HTTP) e casi di errore: il processing dei dati di risposta ha senso solamente
in caso di assenza d’errori.
Il processing della richiesta è identico alla sequenza c) g), estremi compresi, già visto nel caso sincrono.
11
Non è necessario che la sezione a) preceda le altre sezioni, infatti il codice JavaScript non è posizionale;
tuttavia il codice è più leggibile se organizzato con la sezione a) in testa.
pag. 111
Advanced Java rev. 1.0 Novembre 2012
pag. 112
Advanced Java rev. 1.0 Novembre 2012
Quanto segue fornisce ben poche nozioni teoriche sul funzionamento delle web service in axis2, e ancor
meno sul funzionamento delle web service in generale, è invece la descrizione operativa di come si possa
scrivere una web service partendo da zero, in metodologia bottom–up, usando la libreria axis2.
Installazione di axis2.
Ho scaricato la distribuzione binaria [Link] dal sito di Apache, e ne ho unzippato i contenuti
direttamente sotto C:\; così facendo, è stata creata la directory C:\axis2-1.6.2.
La directory AXIS2_HOME/webapp contiene un ant build file, [Link]; ho aperto un command prompt nella
directory, e ho dato il comando ant [Link], che ha creato la directory AXIS2_HOME/dist, contenente il
file [Link]. Per completare l’installazione, è sufficiente copiare il file [Link] entro la directory webapps
di Tomcat, con Tomcat attivo: viene creato il nodo axis2 in webapps.
Entro la directory AXIS2_HOME, sono presenti due directory di particolare interesse per lo sviluppatore: bin,
che contiene tool, e lib, contenente le librerie di axis2.
Poi creo il folder META-INF, allo stesso livello di src; entro META-INF creo il file [Link]. Questo file
stabilisce quale sia la classe web service, e inoltre definisce la lista dei web method (detti “operation”):
<service name="LibreriaWS">
<description>Libreria data provider</description>
<parameter name="ServiceClass">[Link]</parameter>
<operation name="DataRefresh">
<messageReceiver class="[Link]"/>
</operation>
...
<operation name="SystemInfo">
<messageReceiver class="[Link]"/>
</operation>
</service>
La classe LibreriaWS deve contenere tutti i web method elencati in [Link], definiti come pubblici. In
generale, questi metodi potrebbero anche essere vuoti, e non servirebbero nemmeno le classi dati.
Build.
Il file [Link] è composto dai target clean, init, compile, jarGen, copy.
Come di regola, utilizzo la directory build per costruire il JAR, tuttavia c’è una peculiarità legata alle Axis2
web service: build contiene la solita directory classes, ma quest’ultima, oltre ai package omonimi della
directory src – qui ws, contiene anche la directory META-INF. La directory META-INF deve contenere
[Link], in modo che quest’ultimo possa essere incluso nella generazione del JAR.
pag. 113
Advanced Java rev. 1.0 Novembre 2012
Con ciò, il target <compile> contiene, oltre al comando javac, che popola [Link] con le classi dei
package ws e dao, anche il comando di copia del file [Link] da <dir_progetto>/META-INF a
<dir_progetto>/build/classes/META-INF.
Poi il target <jarGen> si preoccupa della generazione del file [Link] (entro build); per quanto
riguarda il driver JDBC per Oracle, [Link], sono possibili due scelte: inclusione del driver nel JAR di ogni
web service che ne ha bisogno, oppure porre il driver nella directory shared/lib di Tomcat.
Ho optato per la seconda scelta, dato che quasi tutte le web service sviluppate accedono al database
Oracle; la scelta è decisamente conveniente in termini di risparmio dello spazio disco, poiché i JAR delle
web service risultano molto più piccoli. È certamente la scelta migliore quando i server Tomcat e Oracle
risiedono sullo stesso host, come nel mio caso.
Il target finale, <copy>, scrive il file nella directory repository, qui C:\aar files, cambiandone l’estensione
da .jar a .aar: ottengo così C:\aar files\[Link].
Installazione.
L’archivio C:\aar files\[Link] è la Axis2 web service, ma per funzionare deve essere inserita nel
giusto contesto. Con Tomcat attivo, lancio l’utility Axis2 Manager: passando [Link] nel
web browser, accedo alla console Axis2.
Utilizzo il link Administration, con i default Username = admin e password = axis2; nel menu Tools a sinistra,
scelgo Upload Service: tramite la form, accedo l’archivio C:\aar files\[Link] e ne faccio l’upload.
Dopo l’operazione di upload, trovo la web service copiata entro la struttura di Tomcat: il file [Link] è
presente in webapps/axis2/WEB-INF/services (webapps è il root site, in Tomcat).
Se dovessi in seguito aggiornare la web service, dovrei prima di tutto cancellare il file [Link] dalla
directory di Tomcat, poi ripetere la procedura di installazione già descritta.
A differenza dei servizi Windows e delle web service ASMX, non viene fornita un’interfaccia direttamente
accessibile da browser, che esponga i web methods.
Una web service invece non può essere acceduta se non richiamandone i singoli web method; ci sono due
fondamentali tipi di accesso, e quindi di client: la spedizione, più o meno diretta, di un messaggio HTTP
POST alla web service, oppure l’uso di uno stub, cioè un oggetto interno all’applicazione client, che nel
contesto del client rappresenta la web service e ne espone i web method: il codice client chiama i metodi
dell’oggetto stub come se chiamasse i web method.
Di seguito mostrerò due esempi di client del primo tipo, ossia diretti; esempi di client del secondo tipo
saranno mostrati in paragrafi appositi. In seguito, chiamerò stub client i client del secondo tipo.
SocketClient.
Tramite l’applicazione SocketClient, è possibile chiamare un metodo della web service, ad esempio il
metodo GetVotoLibro: public int GetVotoLibro(int c), con c = 1003 (c è il codice del libro di cui si
vuole conoscere il voto), spedendo il seguente messaggio alla porta 8081 di user-pc:
POST /axis2/services/LibreriaWS HTTP/1.1
Content-Type: text/xml; charset=utf-8
Host: user-pc
Content-Length: 162
pag. 114
Advanced Java rev. 1.0 Novembre 2012
<s:Envelope xmlns:s='[Link]
<s:Body>
<GetVotoLibro xmlns='[Link]
<Codice>1003</Codice>
</GetVotoLibro>
</s:Body>
</s:Envelope>
L’esempio è molto istruttivo. Intanto, la richiesta è un messaggio di tipo POST che veicola un messaggio
SOAP come proprio body; non è l’unico mezzo di comunicazione con una web service, ma di certo è il più
comunemente usato. È interessante il modo di indirizzare la web service, cioè il suo endpoint: in sostanza,
una axis2 web service è un subsito (di secondo livello) del sito axis2. L’attributo Content-Type non è
strettamente necessario, ma è bene specificarlo.
Il body, che è il messaggio SOAP, è nel mio esempio ridotto all’osso: entro il tag <Body> dev’essere scritto il
nome dell’operazione (web method) come un tag XML, con il namespace proprio della web service;
quest’ultimo è determinato da ciò che viene scritto in [Link], perciò, in assenza di ridefinizioni, coincide
con [Link] ove pkg è il package al quale appartiene la classe che implementa la web service: nel caso di
LibreriaWS, il package è ws. I parametri dell’operazione, se presenti, devono essere sottonodi del nodo
operazione; il tag name dei parametri non è obbligato.
104
<?xml version='1.0' encoding='UTF-8'?>
<soapenv:Envelope xmlns:soapenv="[Link]
<soapenv:Body>
<ns:GetVotoLibroResponse xmlns:ns="[Link]
<ns:return>750</ns:return>
</ns:GetVotoLibroResponse>
</soapenv:Body>
</soapenv:Envelope>
0
La risposta, o meglio il suo body, ha il formato SOAP; il numero che precede il messaggio SOAP è la sua
lunghezza in esadecimale (260 byte).
Adesso chiamo un web method che restituisce un array di oggetti anziché un singolo valore, ad esempio
public Librodn[] BooksDnListByAuthor(String nom), con nom = Z; ecco il messaggio di
richiesta:
POST /axis2/services/LibreriaWS HTTP/1.1
Content-Type: text/xml; charset=utf-8
Host: user-pc
Content-Length: 175
<s:Envelope xmlns:s='[Link]
<s:Body>
< BooksDnListByAuthor xmlns='[Link]
<Cognome>Z</Cognome>
</ BooksDnListByAuthor>
</s:Body>
</s:Envelope>
pag. 115
Advanced Java rev. 1.0 Novembre 2012
2bd
<?xml version='1.0' encoding='UTF-8'?>
<soapenv:Envelope xmlns:soapenv="[Link]
<soapenv:Body>
<ns:BooksDnListByAuthorResponse xmlns:ns="[Link]
xmnls:ax23="[Link]
<ns:return xmlns:xsi="[Link]
xsi:type="ax23:Librodn">
<ax23:autore>Roger Zelazny</ax23:autore>
<ax23:codice>17</ax23:codice>
<ax23:titolo>Io, l’immortale</ax23:titolo>
</ns:return>
<ns:return xmlns:xsi="[Link]
xsi:type="ax23:Librodn">
<ax23:autore>Roger Zelazny</ax23:autore>
<ax23:codice>42</ax23:codice>
<ax23:titolo>The master</ax23:titolo>
</ns:return>
</ns:BooksDnListByAuthorResponse>
</soapenv:Body>
</soapenv:Envelope>
0
I dati d’interesse, ossia gli oggetti di tipo Librodn, sono ancora contenuti nei nodi <ns:return>, però il nodo
<ns:return> è a sua volta un nodo complesso e non un semplice text node come nel caso di GetVotoLibro;
devo indirizzarne i sottonodi, però questi hanno tag name prefissato, cioè non <autore> ma <ax23:autore>, e
il prefisso non è costante: la parte ax2 è fissa, ma la parte seguente, che nell’esempio è 3, può variare. La
stessa web service, invocata sullo stesso metodo, può rispondere usando prefissi diversi nel tempo.
Queste considerazioni sono importanti ai fini dello sviluppo di client ancora diretti ma più evoluti, come la
pagina [Link], descritta nel seguito.
PaginaAjaxWS.
La pagina [Link], aggiunta al sito LightSite, somiglia molto alla pagina [Link], client
sincrono del servlet Booklet; però PaginaAjaxWS interroga (in mdalità sincrona) la web service LibreriaWS.
Il codice HTML delle due pagine è quasi identico, cambiano il titolo della pagina, il testo del button e i tag
della tendina Funzione – LibreriaWS dispone di alcuni metodi diversi da quelli di Booklet. Anche buona parte
del codice JavaScript è strettamente analoga, per cui riprenderò quant già visto per PaginaAjax.
La funzione richiamata dal button è stata ribattezzata CallWS; lo schema in sezioni adottato per CallServlet
di PaginaAjax è ancora applicabile
pag. 116
Advanced Java rev. 1.0 Novembre 2012
ope = "BooksListByGenre";
nPars = 1;
break;
case "RicaricaDati":
var resp = confirm("Sei sicuro di voler ricaricare i dati ?");
if (!resp)
{
[Link] = "Nessuna operazione";
return;
}
ope = "DataRefresh";
break;
}
var bodySoap = "<s:Envelope
xmlns:s='[Link]
"<s:Body><"+ope+" xmlns='[Link]
if (nPars == 1) bodySoap += "<p>"+[Link]+"</p>";
bodySoap += "</"+ope+"></s:Body></s:Envelope>";
Viene istanziato l’oggetto XMLHttprequest e viene assegnato conn, in conformità con quanto si è visto
nell’utilizzo di SocketClient; poi viene composto il messaggio SOAP, entro la variabile bodySoap (anche qui,
è utile il confronto con quanto si è fatto nel caso di SocketClient).
il metodo open apre una connessione alla web service, specificando che la richiesta è di tipo POST, che
l’URL da utilizzare è quello contenuto nella variabile conn, e che la modalità di colloquio è sincrona. L’invio
effettivo del messaggio avviene all’esecuzione del metodo send, la cui chiamata ha come parametro il SOAP
message, che diverrà il body della richiesta HTTP POST.
Le sezioni auccessive restano inalterate, o meglio, variano solamente perché è diversa la struttura dei dati,
sia quelli in formato XML ricevuti in risposta dalla web service, sia quelli da rappresentare nella table Griglia.
Così, le sezioni c), d), f) restano identiche, mentre cambiano le sezioni e) e g): prima di tutto, l’array
contenente i dati di risposta ha un unico tag name, <ns:return>, anziché tag name distinti come <Libro>,
<Autore> e <Genere>; poi, è necessario sapere quale prefisso utilizzi la web service, allo scopo utilizzo il
codice seguente, che calcola il prefisso in prefix:
var prefix = "";
var px = [Link]("xmlns:ax2");
var py = [Link]("=",px)
prefix = [Link](px+6,py-px-6);
con queste considerazioni, il codice delle sezioni e), g) di recupero dati continua a valere.
Notare: nel caso del servlet Booklet, il codice applicativo del servlet scrive i dati di risposta in formato XML,
perciò so esattamente quali tag sono stati usati; nel caso della web service LibreriaWS, il formato XML dei
dati non è deciso dal codice applicativo, perciò non so a priori i tag usati, ne ho un’idea di massima grazie
all’impiego di SocketClient, e devo scrivere un certo codice client per decifrare le informazioni mancanti.
Nel seguito mostro la realizzazione di un stub client; un client del genere è strettamente legato all’architettura
axis2, quindi lo chiamerò axis2 client – un sottoinsieme di stub client operante con axis2.
pag. 117
Advanced Java rev. 1.0 Novembre 2012
Il progetto di LibreriaWS non si riferisce mai esplicitamente a axis2: non vengono usate le librerie presenti in
axis2/lib, né i tool presenti in axis2/bin. L’aggancio all’ambiente axis2 è “automatico”, in buona parte
posizionale e in parte dichiarativo (il file, di estensione particolare .aar, deve stare in una certa directory di
Tomcat, sul quale è stato installato il sistema axis2, inoltre dev’essere presente [Link]).
Il discorso è completamente diverso per i axis2 client: questi sono applicazioni di natura qualsiasi, non hanno
alcun legame automatico al mondo axis2, nemmeno a Tomcat, quindi la costruzione del legame tra il client e
la web service è per così dire a carico del client, ed è realizzato tramite le librerie e i tool di axis2.
L’applicazione LibreriaWSClient.
Il client LibreriaWSClient è un’applicazione console, molto semplice, ma il meccanismo di riferimento
all’ambiente axis2 è del tutto generale, valido per ogni client.
L’applicazione è costruita con un Java project e il solo sorgente applicativo è [Link], nel package app.
Per la sua descrizione conviene attenersi alle diverse fasi del build file, [Link]; le prime due sono di
routine: il tag <init> genera la solita directory build, con la sottodirectory classes, il tag <clean> la cancella.
Prima di creare la classe client, posso generare le classi stub; allo scopo, definisco preliminarmente alcune
proprietà e un classpath in [Link]:
<property name="wsdlSource"
value="[Link]
<property name="[Link]" value="${env.AXIS2_HOME}/lib"/>
<property name="srcdir" value="${basedir}/src"/>
<property name="stubPkg" value="stubLibWS"/>
<path id="[Link]">
<fileset dir="${[Link]}">
<include name="*.jar"/>
</fileset>
</path>
Viene messa in esecuzione la classe [Link] contenuta nel package [Link] della
libreria [Link]; il classpath è l’insieme dei file JAR contenuti in axis2-1.6.2/lib. In sostanza,
equivale al seguente comando, passato al command prompt (non importa la directory di lancio):
java –cp “%AXIS2_HOME%/lib/[Link];%AXIS2_HOME%/lib/[Link];
%AXIS2_HOME%/lib/[Link]” [Link].WSDL2Java
Questa è la forma minima del comando, che lancia la classe senza passarle alcun parametro; l’output del
comando si limita a mostrare l’elenco dei parametri e delle opzioni attese.
L’oggetto sul quale agisce la classe WSDL2Java è il file WSDL, introdotto dall’opzione –uri, però quando
l’aggiungo si verificano eccezioni dovute a mancanza di librerie, che aggiungo progressivamente. Alla fine, la
forma definitiva del comando, se passato al command prompt, è la seguente:
java -cp "%AXIS2_HOME%/lib/[Link];%AXIS2_HOME%/lib/[Link];
%AXIS2_HOME%/lib/[Link];%AXIS2_HOME%/lib/[Link];
pag. 118
Advanced Java rev. 1.0 Novembre 2012
%AXIS2_HOME%/lib/[Link];%AXIS2_HOME%/lib/[Link];
%AXIS2_HOME%/lib/[Link];%AXIS2_HOME%/lib/[Link];
%AXIS2_HOME%/lib/[Link];%AXIS2_HOME%/lib/[Link]"
[Link].WSDL2Java
-uri "[Link] -p stubLibWS -d adb -s
Questa forma è equivalente alla sintassi ant del target <WSDL2JAVA>, in cui parametri e opzioni sono
passati mediante la lista dei subtarget <arg>, mentre le librerie sono rintracciabili grazie al classpath.
Il file WSDL descrittivo della web service è specificato dall’opzione –uri; il nome del package a cui
appartengono i file generati dalla classe WSDL2Java è specificato tramite l’opzione –p; il tipo di
databinding12 scelto è adb (axis2 databinding), come specificato dall’opzione –d; l’opzione –s stabilisce la
modalità sincrona di colloquio (il client dopo aver effettuato una chiamata, si blocca in attesa della risposta):
se avessi omesso l’opzione –s, sarebbero stati generati classi e metodi relativi alle chiamate asincrone.
Come risultato, viene generato il file [Link] entro il package stubLibWS (situato entro la
directory src); se non avessi usato l’opzione –p, il file sarebbe stato generato entro la directory ws, creata al
momento se non già esistente, sempre interna a src.
Nella IDE appaiono alcuni errori relativi al file [Link]; possono essere risolti tramite aggiunta di
tre librerie axis2 al build path (Configure Build Pathadd external JARs, nella directory AXIS2_HOME/lib
(vedi anche la figura successiva per i nomi delle tre librerie).
Poi creo il package app e in esso la classe Client, dotata di metodo main, senza particolarità.
Supponendo di aver già compilato e generato il file JAR,
il progetto si presenta come a sinistra.
Per poter accedere ai metodi della web service, il client deve creare uno stub:
String link = "[Link]
+"/axis2/services/LibreriaWS?wsdl";
12
Nel mondo delle web service, il termine databinding si riferisce alle tecniche di conversione dei dati,
passando dai dati XML alle strutture dati applicative (interne alle applicazioni client e server), e viceversa. Il
framework axis2 ha un databinding proprio, che è adb (axis2 data binding), ma può operare anche con altri
tipi di databinding, come XMLBeans e JiBX; l’opzione –d dice alla classe WSDL2Java quale modello di
databinding utilizzare.
pag. 119
Advanced Java rev. 1.0 Novembre 2012
In analogia con il caso di Corba, si può ancora dire che il client chiami il web method BooksDnListByRange,
passando i codici ci e cf, e si aspetti in ritorno un array di record di tipo Librodn; ma ci sono differenze
importanti tra l’accesso ai metodi di un oggetto via Corba e l’accesso ai metodi di una web service.
L’accesso client ai metodi di una web service è peculiare: nella classe LibreriaWS, il metodo è dichiarato
come public Librodn[] BooksDnListByRange(int ci,int cf); però la chiamata del metodo
mediante lo stub non è una semplice invocazione, come accade in Corba, ma è un’operazione più
complessa, che segue le strutture del file WSDL.
non esiste nella classe Stub un metodo esattamente omonimo con quello (BooksDnListByRange) della web
service, l’iniziale è minuscola anziché maiuscola (se però il metodo della web service avesse iniziale
minuscola, resterebbe inalterata); soprattutto, la signature è assai diversa: anziché due parametri di
passaggio ci e cf, è presente un solo parametro di tipo BooksDnListByRange, che è una classe statica,
interna alla classe Stub, definita public in modo da risultare visibile; il tipo del valore di risposta non è
Librodn, ma BooksDnListByRangeResponse, altra classe statica, pubblica, interna alla classe Stub.
Il client alloca un oggetto richiesta, di tipo BooksDnListByRange, definito anch’esso, come pure i seguenti,
entro la classe LibreriaWSStub, che contiene svariate classi al proprio interno. Se sono previsti parametri di
passaggio, come in questo caso, essi sono campi interni all’oggetto richiesta e possono essere settati
usando gli appositi metodi che l’oggetto fornisce (cioè setArgs0 e setArgs1).
Nel caso il web method preveda un oggetto di ritorno, allora la chiamata al metodo via stub restituisce un
oggetto risposta, di tipo BooksDnListByRangeResponse; l’oggetto risposta resp contiene al proprio interno
un membro di tipo Librodn[], quest’ultimo è proprio ciò che interessa al client ed è accessibile mediante il
metodo get_return dell’oggetto risposta.
Il resto del codice dell’applicazione LibreriaWSClient non presenta interesse particolare; in ultimo, uscendo
dall’applicazione, è bene invocare il metodo [Link] per liberare tutte le risorse, ma non è mandatario.
pag. 120
Advanced Java rev. 1.0 Novembre 2012
<zipfileset src="${[Link]}/[Link]"/>
<zipfileset src="${[Link]}/[Link]"/>
<zipfileset src="${[Link]}/[Link]"/>
<zipfileset src="${[Link]}/[Link]"/>
<fileset dir="${classes}"/>
</jar>
</target>
Queste 14 librerie devono essere sempre disponibili ad ogni applicazione client di una web service axis2,
non importa quanto semplici siano l’applicazione client e la web service.
L’applet LibreriaWSApplet.
Lo sviluppo di un applet client della web service LibreriaWS avviene in modo analogo da un lato allo sviluppo
di BookletApplet, dall’altro lato allo sviluppo di LibreriaWSClient. In sintesi, utilizzo un Java project con il
package app, nel quale creo la classe LibreriaWSAppletClass subtipo di JApplet, e il package models nel
quale è presente la classe ReadonlyTableModel, identica a quella di BookletApplet, usata per gestire la
JTable dell’applet. Segue il design dell’applet, sviluppato con WindowBuilder Editor:
L’applet ricalca la pagina [Link]: ci sono la JTextField argo, in cui l’operatore digita la stringa di
ricerca, la JComboBox Funzione con le stesse funzioni di ricerca della pagina HTML, la readonly JTextField
risu in cui mostrare il numero di elementi trovati dalla ricerca, e il JButton per l’esecuzione della ricerca; poi
c’è un separatore, al di sotto del quale sta la JTable table, posta all’interno del JScrollPane scrollPane, usata
per mostrare i dati. Il table model rotm, di tipo [Link], è assegnato a table.
Il build file [Link] consiste inizialmente dei target <clean>, <init>, <WSDL2JAVA>, <compile>, <jarGen> e
<copy>; i target <WSDL2JAVA>, <compile> e <jarGen> sono identici a quelli di LibreriaWSClient: anche qui,
utilizzando il WSDL online della web service, viene generato il package stubLibWS contenente la classe
LibreriaWSStub (stub). Lo stub viene creato (esattamente come in LibreriaWSClient) alla fine
dell’inizializzazione grafica.
Aggiungo al sito LightSite la pagina [Link], praticamente uguale alla pagina di lancio
di BookletApplet; al suo interno, trovo il tag <object> consueto:
<object id=LibWSAppl codebase="[Link]
archive="[Link]"
pag. 121
Advanced Java rev. 1.0 Novembre 2012
Quando però la pagina prova a lanciare l’applet, si verifica un errore bloccante: appena il codice dell’applet
tenta di creare lo stub utilizzando il WSDL online, si verifica un’eccezione di tipo
[Link], in sostanza un accesso negato alla risorsa (cioè il WSDL online);
l’eccezione è riscontrabile nella Java console del browser, e l’applet non viene neppure visualizzato.
Firma dell’applet.
Affinchè l’applet possa accedere alla risorsa WSDL online, e quindi alla web service, è necessario firmare
l’applet. Per firmare l’applet, bisogna disporre di un certificato; perciò bisogna che generi un certificato.
Questo passo viene compiuto usando il tool keytool: apro un command prompt, mi posiziono nella directory
in cui voglio creare il file di certificazione – scelgo Tomcat/webapps/applets, e uso il seguente comando:
keytool –genkey –alias firmaApplet –keystore Chiave
Adesso dispongo del file Chiave in Tomcat/webapps/applets, e posso utilizzarlo per firmare i miei applet.
Così, in [Link] sostituisco il tag <copy> con il seguente tag <signature>:
<target name="signature" description="Sign the JAR">
<signjar destDir="${finalDest}" jar="${lib}/${appName}.jar"
keystore="${finalDest}/Chiave" storepass="sacone58"
alias="firmaApplet"/>
</target>
Il tag <signature> richiama il tool jarsigner; il JAR file di partenza, cioè build/lib/[Link], resta
invariato, e viene prodotto nella directory di destinazione, cioè Tomcat/webapps/applets, un JAR omonimo
(non ho riassegnato il suo nome) ma diverso, in quanto firmato – il JAR firmato è un po’ più grande.
La pagina [Link] carica il JAR firmato; stavolta non ci sono errori bloccanti, la risorsa
WSDL viene acceduta senza problemi e i web method sono richiamabili.
pag. 122
Advanced Java rev. 1.0 Novembre 2012
La mancanza dei tre attributi non stupisce: siccome io non ho definito un manifest per l’applet, l’embedded
Ant di Eclipse ne ha generato uno, minimale. Allora modifico il tag <init> affinchè crei la directory
build/META-INF, e aggiungo il tag <manifesto> al file [Link]:
<target name="manifesto" description="crea il manifest">
<manifest file="${build}/META-INF/[Link]">
<attribute name="Application-Name" value="${appName}"/>
<attribute name="Permissions" value="all-permissions"/>
<attribute name="Codebase" value="[Link]
</manifest>
</target>
Con il nuovo applet risultante dal rebuild, la Java console non dà più i precedenti moniti.
pag. 123
Advanced Java rev. 1.0 Novembre 2012
Nonostante sia in parte integrato nei JDK, è necessario scaricare JAX-WS<versione>.zip dal sito (parte del
progetto Glassfish) [Link] l’ultima versione disponibile è 2.2.8.
Scompattando lo zip sotto C:\, viene creata la directory C:\jaxws-ri; di particolare importanza è la
sottodirectory lib, contenente alcuni JAR file non presenti nel JDK.
Affinchè il framework sia utilizzabile, alcuni JAR presenti in jaxws-ri/lib vanno integrati nell’installazione
dell’application server utilizzato, nel mio caso Tomcat. Per semplicità, entro jaxws-ri viene fornito un ant
command file ad hoc, [Link]; prima di poter usare il command file, è però necessario definire la variabile
d’ambiente CATALINA_HOME, che identifica la directory di installazione di Tomcat (coincide con
TOMCAT_HOME), in quanto usata dal command file. Fatto ciò, si apre un command prompt entro la
directory jaxws-ri e si dà il comando ant install.
L’effetto del precedente comando è la copia di 23 JAR file entro Tomcat/shared/lib; se shared/lib non esiste,
viene creata. Inoltre viene scritta la riga [Link]=${[Link]}/shared/lib/*.jar
entro Tomcat/conf/catalina.properties13. È bene a questo punto riavviare Tomcat.
In ultimo, definisco la variabile d’ambiente JAXWS_HOME, che punta alla root della directory d’installazione
di jax-ws, quindi JAXWS_HOME = C:\jaxws-ri. Questa variabile d’ambiente verrà usata nel seguito, in
particolare per accedere librerie e tool di jaxws-ri.
13
La riga in questione non è appannaggio di jaxws-ri; essa dev’essere presente nel file [Link],
ogni volta che si utilizza la directory shared/lib entro l’installazione di Tomcat, come nel mio caso, che già vi
avevo posto [Link]; in questi casi, il command file di jaxws-ri duplica inutilmente la riga.
pag. 124
Advanced Java rev. 1.0 Novembre 2012
Il framework jax-ws utilizza ampiamente le annotazioni; in particolare la classe che implementa una web
service dev’essere obbligatoriamente contraddistinta con l’annotazione @WebService; l’annotazione
ammette diversi parametri, che se, come nel caso presente, non vengono settati, assumono valori default
(l’argomento verrà approfondito nel seguito).
Anche l’annotazione WebMethod ammette parametrizzazione; il suo uso è fortemente consigliato, ma non è
obbligatorio: anche omettendo l’annotazione @WebMethod, ogni metodo della classe web service che sia
pubblico diventa un web method, cioè compare nel WSDL ed è richiamabile dai client.
Proprio l’annotazione @WebMethod può essere usata per evitare che un metodo pubblico della classe web
service divenga un web method richiamabile dai client. Sia M un metodo della classe web service: posso
trovare utile dichiararlo pubblico per esigenze di programmazione interne all’applicazione web service, ma
non voglio che divenga un web method accessibile da parte dei client. Come fare ?
Anche il tipo di ritorno e i parametri di passaggio di un web method possono essere annotati, rispettivamente
con @WebResult e @WebParam; si vedrà un esempio d’uso di @WebParam nel seguito.
La classe web service non ha bisogno di alcun main, infatti ad istanziare la classe ci pensa l’application
server (Tomcat). La scrittura della classe web service viene completata scrivendo il codice dei vari metodi.
Il target <init> crea la solita directory build, con le sottodirectory classes e war, quest’ultima destinata a
ospitare il WAR file. Il target <clean> elimina la directory build.
pag. 125
Advanced Java rev. 1.0 Novembre 2012
</annotationProcessing>
</target>
il processing delle annotazioni comprende anche la compilazione di tutti i sorgenti: se l’esecuzione del target
<compileService> avviene correttamente, entro build/classes trovo i file .class di tutti i sorgenti, qui trovo
build/classes/app/[Link] e build/classes/dao/[Link].
Inoltre compare anche la sottodirectory build/classes/app/jaxws che contiene per ogni web method una
coppia di classi, una per la richiesta e una per la risposta: nel caso corrente, la sottodirectory
build/classes/srv/jaxws contiene 4 classi: [Link], [Link],
[Link] e [Link].
Osservazione importante: l’esecuzione del target <compileService> genera nel progetto il package
[Link], ossia, a livello di file system, la sottodirectory jaxws entro la directory app; poi il target genera le
classi compilate entro [Link]. Alla fine, resta la directory app/jaxws entro src, e questo costituisce un
problema di classi duplicate per successive esecuzioni del target <compileService>.
Bisogna quindi eliminare app/jaxws una volta completata l’annotation processing. Allo scopo, aggiungo una
apposita riga in coda al target <compileService>:
<target name="compileService">
<annotationProcessing
...
</annotationProcessing>
<delete dir="${srcdir}/app/jaxws" includeEmptyDirs="true"/>
</target>
pag. 126
Advanced Java rev. 1.0 Novembre 2012
Convenzionalmente, si aggiunge il folder etc al progetto, allo stesso livello di src; i due file descrittivi vengono
scritti entro il folder etc. La loro struttura è convenzionale, uguale per ogni web service, soltanto i valori di
alcuni parametri sono specifici della singola web service.
Dettagli su [Link].
Ho usato come prototipo un esempio fornito dalla documentazione di jaxws-ri, adattandolo al caso di
PeopleWS (ho omesso nel seguito le parti non essenziali):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"[Link]
<web-app>
<display-name>PeopleWS (jax-ws web service)</display-name>
<listener>
<listener-class>
[Link]
</listener-class>
</listener>
<servlet>
<servlet-name>people</servlet-name>
<servlet-class>[Link]</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>people</servlet-name>
<url-pattern>/PeopleWS</url-pattern>
</servlet-mapping>
</web-app>
Questo descrittore evidenzia bene il fatto che le JAX-WS web service basano il proprio funzionamento sui
servlet. Per interpretare le caratteristiche generali della struttura del file [Link] di un servlet, rimando ad un
precedente paragrafo.
Il servlet sottostante alla web service, che si preoccupa della ricezione delle richieste dei client, è istanza
della classe WSServlet, una classe appartenente al framework jaxws-ri (libreria [Link]). Anche la classe
WSServletContextListener, da cui viene istanziato il listener del servlet, appartiene alla stessa libreria.
Si evince dalla DTD che l’architettura usata in JAX-WS per i servlet è quella contenuta nelle Servlet
Specification 2.3, tale scelta richiede la presenza di [Link] (a differenza della 3.0, che permette di evitare il
file [Link] mediante l’uso di annotazioni). Di conseguenza, il file [Link] è ineliminabile, in quanto riguarda
non tanto la web service ma il servlet sottostante, che è dato dal framework jaxws-ri.
In ultimo, si noti che, avendo definito il tag <load-on-startup>, con il valore 1, il servlet viene istanziato (da
Tomcat) alla partenza, senza aspettare le richieste da parte dei client.
Dettagli su [Link].
Ho usato come prototipo un esempio fornito dalla documentazione di jaxws-ri, adattandolo al caso di
PeopleWS:
<?xml version="1.0" encoding="UTF-8"?>
<endpoints xmlns='[Link] version='2.0'>
<endpoint name='peopleEP' implementation='[Link]'
url-pattern='/PeopleWS'/>
</endpoints>
pag. 127
Advanced Java rev. 1.0 Novembre 2012
<classes dir="${classes}"/>
</war>
</target>
Il nome del WAR file è deciso dall’attributo warfile, che stabilisce sia la directory in cui viene crato il file (cioè
build/war) sia il suo nome, che risulta essere PeopleWS, essendo appname = PeopleWS.
In generale, un WAR file è un file archivio che deve soddisfare alcuni requisiti, sia di struttura che di
contenuto: deve contenere la directory WEB-INF; la directory WEB-INF a sua volta deve contenere il file
descrittivo [Link], inoltre le classi (file .class) presenti devono stare nella sottodirectory classes, mentre i
JAR file eventualmente presenti devono stare nella sottodirectory lib.
Oltre alla directory WEB-INF, è di norma presente la directory META-INF, allo stesso livello di WEB-INF,
contenente il file manifest [Link] (senza nulla di particolare).
Quando un WAR file implementa una JAX-WS web service, quindi di un tipo particolare di WAR, allora, oltre
alle classi contenute in build/classes e a [Link], c’è bisogno di un altro file descrittivo: [Link].
Eventuali file WSDL e XSD dovrebbero essere posti nella directory etc; se presenti, il subtarget <zipfileset>
nella quarta riga del target <warGen> li copia entro il folder del WAR file; qui non sono però presenti, per cui
il target <zipfileset> entro <warGen> non compie alcuna azione.
Aggiunta di librerie.
La web service stabilisce una connessione al database Oracle DBPeople, quindi necessita della libreria
(driver Oracle) [Link]; volendo aggiungere la libreria driver al WAR file, è necessario aggiungere al target
<warGen> la riga seguente:
<webinf dir="C:\oraclexe\Drivers" includes="[Link]" prefix="WEB-INF/lib"/>
Rieseguendo il target, entro il WAR file compare la sottodirectory WEB-INF/lib contenente il file [Link]
(non scompattato nelle sue singole classi). Questo procedimento va seguito in generale ogni volta che si
intenda includere un JAR file in un WAR file; se il JAR viene incluso, ma non entro WEB-INF/lib, allora il JAR
file non verrà reperito a runtime: è mandatorio includere i JAR entro WEB-INF/lib.
Tuttavia, come già nel caso della axis2 web service LibreriaWS, ho seguito un’altra strada: supponendo che
il database server e il web application server (Oracle e Tomcat) stiano sulla stessa macchina, pongo
[Link] entro la directory shared/lib di Tomcat, così non ho bisogno di porre alcun database driver entro i
singoli WAR file. Perciò l’elemento <webinf> non serve. Tuttavia è importante ricordare che, usando
<webinf>, è possibile includere una libreria qualsiasi.
Ecco come appare il progetto alla fine dell’esecuzione del target <warGen>:
pag. 128
Advanced Java rev. 1.0 Novembre 2012
Utilizzo un Java project con il package app, nel quale creo la classe PeopleWSAppletClass subtipo di
JApplet, e il package models nel quale è presente la classe ReadonlyTableModel, identica a quella di
BookletApplet, usata per gestire la JTable dell’applet. Segue il design dell’applet, sviluppato con
WindowBuilder Editor:
pag. 129
Advanced Java rev. 1.0 Novembre 2012
Le prime fasi del build sono i soliti target <clean> e <init>; c’è la generazione del manifest tramite il target
<manifesto>; seguono poi la fase di generazione dello stub (target <wsimport>), la fase di compilazione
(target <compile>), infine la fase di generazione del JAR file (target <jarGen>) e la firma e copia in
Tomcat/webapps/applets (target <signature>).
Il target <init> genera build, build/META-INF e build/classes, ed è il primo che dev’essere eseguito; lo segue
il target <manifesto>, che scrive un manifest in build/META-INF, analogo a quello di LibreriaWSApplet.
<property name="wsdlSource"
value="[Link]
<property name="srcdir" value="${basedir}/src"/>
<property name="stubPkg" value="stubPeoWS"/>
<path id="[Link]">
<pathelement location="${env.JAVA_HOME}/lib/[Link]"/>
</path>
<target name="wsimport" description="Stub generation">
<java classname="[Link]" fork="true">
<classpath refid="[Link]"/>
<arg value="-keep"/>
<arg value="-extension"/>
<arg value="-d"/>
<arg value="${srcdir}"/>
<arg value="-p"/>
<arg value="${stubPkg}"/>
<arg value="${wsdlSource}"/>
</java>
</target>
La parametrizzazione dice alla classe di utilizzare il file WSDL di PeopleWS, inoltre stabilisce che le classi
generate apparterranno al package stubPeoXWS, e che il package starà nella directory src.
pag. 130
Advanced Java rev. 1.0 Novembre 2012
Ecco come si presenta la classe PeopleWSClassService, generata dall’esecuzione del target <wsimport>:
package stubLibXWS;
@WebServiceClient(name="PeopleWSClassService",targetNamespace="http://
app/",
wsdlLocation="[Link]
public class PeopleWSClassService extends Service
{
...
}
Le chiamate ai metodi della web service PeopleWS vengono effettuate usando l’oggetto pws, e sono più
semplici e immediate delle chiamate ai metodi di una axis2 web service:
List<Person> lista = [Link](mi, mf);
List<Person> lista = [Link](srn);
Ho usato List<Person> come tipo di ritorno dei due metodi, perché si tratta del tipo di ritorno presente
nell’interfaccia [Link]:
public interface PeopleWSClass
{
@WebMethod(operationName = "PeopleListByRange")
@WebResult(targetNamespace = "")
pag. 131
Advanced Java rev. 1.0 Novembre 2012
il resto del codice client serve alla visualizzazione dei dati ricevuti.
Generazione ed esecuzione.
Il target <compile> non presenta particolarità, tutte le classi, sia PeopleWSAppletClass sia quelle generate
da <wsimport> entro il package stubPeoWS, vengono compilate e i .class generati entro build/classes.
I target <jarGen> e <signature> sono del tutto analoghi a quelli già visti nel caso di LibreriaWSApplet.
Una volta che l’applet è disponibile in Tomcat/webapps/applets, per provarlo aggiungo al sito LightSite la
pagina [Link], che carica l’applet firmato:
pag. 132
Advanced Java rev. 1.0 Novembre 2012
<s:Envelope xmlns:s='[Link]
<s:Body>
<v:PeopleListBySurname xmlns:v='[Link]
<arg0>Z</arg0>
</v:PeopleListBySurname>
</s:Body>
</s:Envelope>
Si tratta del messaggio minimale funzionante, eccettuato l’attributo Content-Type che non è strettamente
necessario (potrebbe essere omesso, ma è bene specificarlo).
Il messaggio SOAP è analogo a quello della richiesta alla axis2 web service LibreriaWS, ma ci sono rigidità
maggiori, in particolare il tag del parametro dev’essere <arg0> e null’altro, pena errori, inoltre il tag col nome
del metodo deve appartenere al namespace [Link] e non averlo come default namespace.
6e
<?xml version='1.0' encoding='UTF-8'?>
<S:Envelope xmlns:S="[Link]
<S:Body>
180
<ns2:PeopleListBySurnameResponse xmlns:ns2="[Link]
<return>
<Matricola>2000</Matricola>
<Nome>Cristina</Nome>
<Cognome>Zanni</Cognome>
<CodiceFiscale>Gnocconissima</CodiceFiscale>
</return>
<return>
<Matricola>2100</Matricola>
<Nome>Enrico</Nome>
<Cognome>Zunino</Cognome>
<CodiceFiscale>Neonato</CodiceFiscale>
</return>
</ns2:PeopleListBySurnameResponse>
</S:Body>
</S:Envelope>
0
Si noti che 6e è 110, e i byte che vanno da <? (subito dopo il 6e) fino a <S:Body> compreso sono proprio
110. Poi c’è 180, che equivale a 384, e questo è proprio il numero di byte che vanno da <ns2 (subito dopo il
180) fino alla fine, cioè fino a </S:Envelope> incluso. Si tratta del chunk dei messaggi.
pag. 133
Advanced Java rev. 1.0 Novembre 2012
L’applet PeopleWSApplet è un client semplice e infatti non utilizza librerie proprie di jaxws-ri; adesso invece
passo ad un client avanzato, cioè un client che utilizza funzionalità avanzate: PeopleWSClient.
In una sua prima versione, PeopleWSClient non utilizza funzionalità avanzate: è una semplice applicazione
console, che permette all’operatore di chiamare i metodi della web service PeopleWS, e mostra a video i
risultati delle chiamate. I tag del build file sono i consueti <clean>, <init>, <wsimport> come in
PeopleWSApplet, infine i tre tag <compile>, <jarGen> e <copy> tipici delle applicazioni in ExeJar.
Nella seconda versione, l’applicazione PeopleWSClient svolge le stesse funzioni, ma in più effettua il log dei
messaggi scambiati con la web service PeopleWS.
Sempre in generale, i messaggi (sia HTTP, sia SOAP) sono manipolati nel loro ciclo di vita da handler; la
descrizione di un messaggio, il suo stato, composto in generale da chiavi e valori, è detta contesto del
messaggio ossia message context. Un handler quindi agisce sui message context, facendoli evolvere: in
questo senso, handler e message context sono concetti connessi.
L’interfaccia MessageContext discende dall’interfaccia Map<String,Object>, ossia è visibile come una mappa
di coppie chiave/valore. L’interfaccia MessageContext rappresenta il message context di un messaggio
HTTP orientato all’utilizzo entro il framework JAX-WS, come provano i suoi campi
(HTTP_REQUEST_METHOD, HTTP_RESPONSE_CODE, WSDL_OPERATION, WSDL_PORT etc).
L’utilizzo delle due interfacce, Handler e MessageContext, è sinergico: Handler è interfaccia generica e la
sua espressione formale è Handler<I extends MessageContext>.
All’interno di [Link], nel package [Link], sono presenti due interfacce, derivanti dalle
precedenti e più specializzate: si tratta di SOAPHandler, subtipo di Handler, e SOAPMessageContext,
subtipo di MessageContext. L’interfaccia SOAPHandler è anch’essa generica, e la sua espressione formale
è SOAPHandler<T extends SOAPMessageContext>.
Per implementare un proprio handler, in generale lo sviluppatore crea una classe che implementa
l’interfaccia SOAPHandler<SOAPMessageContext>: è la strada che ho seguito per realizzare il client,
PeopleWSClient, capace di effettuare il log dei messaggi, ho creato allo scopo la mia classe handler,
LoggingHandler, implementazione dell’interfaccia SOAPHandler<SOAPMessageContext>.
Come si vedrà, l’implementazione dell’handler avverrà in un primo tempo senza bisogno di aggiungere
librerie apposite; solo quando richiederò un certo tipo di funzionalità, il progetto client sarà costretto a
ricorrere alle librerie del framework jax-ws ri.
pag. 134
Advanced Java rev. 1.0 Novembre 2012
Questa è la classe, così come è stata generata dall’IDE di Eclipse (appartenente al package log; ho omesso
le import clause, per semplicità e brevità):
public class LoggingHandler implements SOAPHandler<SOAPMessageContext>
{
@Override
public void close(MessageContext context)
{}
@Override
public boolean handleFault(SOAPMessageContext context)
{ return false; }
@Override
public boolean handleMessage(SOAPMessageContext context)
{ return false; }
@Override
public Set<QName> getHeaders()
{ return null; }
}
I metodi getHeaders e close non mi interessano, li lascio così come sono. Riscrivo i metodi handleMessage
e handleFault: il codice è lo stesso per entrambi, cioè la chiamata del metodo logToSystemOut, privato,
che effettua la stampa a video delle informazioni inerenti al messaggio trattato – rappresentato da smc;
inoltre, faccio restituire true a entrambi i metodi.
Ecco il codice del metodo logToSystemOut, ove ho tralasciato la gestione degli errori:
private void logToSystemOut(SOAPMessageContext smc)
{
boolean outboundProperty = (boolean)[Link](MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (outboundProperty)
[Link]("Outbound message at "+[Link]());
else [Link]("Inbound message at "+[Link]());
String httpReqMethod = (String)[Link](MessageContext.HTTP_REQUEST_METHOD);
if (httpReqMethod == null)
[Link]("\nMessageContext.HTTP_REQUEST_METHOD is null\n");
else [Link]("\nMessageContext.HTTP_REQUEST_METHOD="+httpReqMethod);
Integer retc = (Integer)[Link](MessageContext.HTTP_RESPONSE_CODE);
if (retc == null)
[Link]("MessageContext.HTTP_RESPONSE_CODE is null");
else [Link]("MessageContext.HTTP_RESPONSE_CODE="+[Link]());
SOAPMessage msg = [Link]();
[Link]([Link]);
}
pag. 135
Advanced Java rev. 1.0 Novembre 2012
</bindings>
</bindings>
Il file [Link] (il nome è arbitrario) è molto importante, perché al suo interno viene definita la
cosiddetta handler-chains (catena dei handler). Si noti che al suo interno è referenziata la classe
LoggingHandler, cioè la classe che implementa l’handler applicativo.
Eseguo il target <wsimport>: viene creato il package stubPeoXWS, contenente 9 file .java aventi gli stessi
nomi di quelli del progetto PeopleWSApplet, e due file XML, PeopleWSClass_handler.xml e
PeopleWSClassService_handler.xml. I due file XML sono identici, e sono in pratica uguali al file che
risulterebbe sfrondando [Link] dai riferimenti al wsdl e lasciandone intatta la definizione della
handler chain.
Dei 9 file .java generati, due soli sono diversi dagli omonimi del progetto PeopleWSApplet: si tratta di
[Link] e [Link] (i due file più importanti tra i 9 generati).
Siccome le classi PeopleWSClass e PeopleWSClassService fanno riferimento ai rispettivi file XML descrittivi
del loro handler, mi aspetto che i due file XML debbano essere presenti nel file JAR. Supponendo di
ignorarlo, posso compilare e generare il JAR (target <compile> e <jarGen> rispettivamente), quindi copiare
[Link] in ExeJar, e infine lanciare il client: si verifica un errore, in quanto la classe
PeopleWSClassService non riesce a trovare il file PeopleWSClassService_handler.xml. Me l’aspettavo…
È necessario copiare i due file XML, generati da <wsimport>, nella directory build/classes, per cui aggiungo
un comando di copia in coda al target:
<target name="wsimport" description="Stub generation">
<java classname="[Link]" fork="true">
... come sopra ...
</java>
<copy toDir="${classes}">
<fileset dir="${srcdir}/${stubPkg}">
<include name="*.xml"/>
</fileset>
</copy>
</target>
pag. 136
Advanced Java rev. 1.0 Novembre 2012
Eseguo il target <wsimport>, poi <compile>, <jarGen> e <copy>; apro un command prompt in ExeJar e do il
comando java –jar [Link]: il client funziona correttamente.
Il primo messaggio è uscente: HTTP_REQUEST_METHOD è null, perché il campo viene analizzato troppo
precocemente (assumerà il valore POST, ma non l’ha ancora assunto nel momento in cui l’analizzo); anche
HTTP_RESPONSE_CODE è null, il che è corretto essendo un messaggio di richiesta e non di risposta.
Il SOAP message che costituisce il body è conforme a ciò che mi attendevo, ed a ciò che ho scritto quando
ho usato SocketClient.
Poi giunge il messaggio di risposta da parte della web service: qui non ha senso
HTTP_REQUEST_METHOD, trattandosi di un messaggio di risposta, mentre HTTP_RESPONSE_CODE è
valorizzato e vale 200 = OK. Anche qui, il SOAP message che costituisce il body è conforme alle attese.
Si può migliorare l’informazione fornita, cominciando a eliminare le informazioni certamente inutili; so che il
client invia richieste e riceve risposte, e quindi è inutile stampare HTTP_RESPONSE_CODE in un
messaggio in uscita, così come è inutile stampare HTTP_REQUEST_METHOD, che è sempre null per le
ragioni sopra addotte.
Finora non ho utilizzato feature proprie di jaxws-ri, infatti il computer sul quale viene eseguita l’applicazione
PeopleWSClient non ha il framework jaxws-ri installato.
pag. 137
Advanced Java rev. 1.0 Novembre 2012
però la classe Headers non viene risolta. È qui che si rende necessario il salto di qualità.
Adesso il client LibreriaXWSAdvClient è diventato un client avanzato, che ricorre alle feature proprie del
framework jaxws-ri.
pag. 138
Advanced Java rev. 1.0 Novembre 2012
Stavolta sono visibili i campi dell’header HTTP nel messaggio di risposta inviato dalla web service al client.
pag. 139
Advanced Java rev. 1.0 Novembre 2012
Anche qui, nei messaggi inviati i campi del header HTTP non sono ancora valorizzati, all’esecuzione
dell’handler – infatti HTTP_RESPONSE_CODE = 0, poi diverrà 200; perciò non ha senso stamparli. Stampo
invece i campi dell’header dei messaggi ricevuti, sono le richieste dei client: perciò entro il metodo
logToSystemOut stampo il valore di HTTP_REQUEST_METHOD, e stampo i campi di
HTTP_REQUEST_HEADERS.
L’output della classe LoggingHandler stavolta va all’interno del file di log tomcat7-stdout.<data>.log, nella
directory Tomcat/logs.
Scenario di funzionamento.
Con la web service attiva su user-pc, apro un command prompt sul PC remoto METRO e lancio il client
PeopleWSClient: java –jar [Link] user-pc, quindi chiamo il metodo PeopleListByRange
passandogli le matricole 2 (iniziale) e 8 (finale).
Nel command prompt, vedo l’output del client, e in esso si distingue il messaggio di richiesta in uscita,
corrispondente alla chiamata al metodo PeopleListByRange:
Outbound message at 18/01/2014 19:19:53
<S:Envelope xmlns:S=”[Link]
xmlns:SOAP-ENV=”[Link]
<SOAP-ENV:Header/>
pag. 140
Advanced Java rev. 1.0 Novembre 2012
<S:Body>
<ns2:PeopleListByRange xmlns:ns2=”[Link]
<arg0>2</arg0>
<arg1>8</arg1>
</ns2:PeopleListByRange>
</S:Body>
</S:Envelope>
Nel file Tomcat/logs/[Link] trovo la richiesta proveniente dal client, così come l’ha
ricevuta la web service PeopleWS:
Inbound message at 18/01/2014 19:19:54
MessageContext.HTTP_REQUEST_METHOD=POST
accept: text/xml, multipart/related
connection: keep-alive
content-length: 292
content-type: text/xml; charset=utf-8
host: user-pc:8081
soapaction: "[Link]
user-agent: JAX-WS RI 2.2.8 svn-revision#13980
<S:Envelope xmlns:S=”[Link]
xmlns:SOAP-ENV=”[Link]
<SOAP-ENV:Header/>
<S:Body>
<ns2:PeopleListByRange xmlns:ns2="[Link]
<arg0>2</arg0>
<arg1>8</arg1>
</ns2:PeopleListByRange>
</S:Body>
</S:Envelope>
Sempre nel file Tomcat/logs/[Link], vedo la risposta della web service PeopleWS:
Outbound message at 18/01/2014 19:19:55
<S:Envelope xmlns:S=”[Link]
xmlns:SOAP-ENV=”[Link]
<SOAP-ENV:Header/>
<S:Body>
<ns2:PeopleListByRangeResponse xmlns:ns2="[Link]
<return>
<Matricola>2</Matricola>
<Nome>Renzo</Nome>
<Cognome>Calabria</Cognome>
<CodiceFiscale>CLBRNZ43A26D969D</CodiceFiscale>
</return>
<return>
<Matricola>7</Matricola>
<Nome>Giovanni</Nome>
<Cognome>Arrigoni</Cognome>
<CodiceFiscale>RRGGNN46C02A794J</CodiceFiscale>
</return>
<return>
<Matricola>8</Matricola>
<Nome>Edoardo</Nome>
<Cognome>Bianco</Cognome>
<CodiceFiscale>BNCDRD46T16D969Y</CodiceFiscale>
</return>
</ns2:PeopleListByRangeResponse>
</S:Body>
</S:Envelope>
Ecco infine come appare il messaggio ricevuto dal client, nel suo command prompt:
pag. 141
Advanced Java rev. 1.0 Novembre 2012
Ci possono essere leggere differenze nei messaggi, a seconda dei diversi client utilizzati.
pag. 142
Advanced Java rev. 1.0 Novembre 2012
In generale, una pagina JSP consiste di due tipi fondamentali di elementi: template text ed elementi JSP; si
dice template text tutto ciò che non è un elemento JSP, quindi codice HTML, testo semplice, elementi XML,
immagini, codice JavaScript.
Gli elementi JSP a loro volta si possono suddividere in quattro sottotipi: direttive, azioni, scripting e
espressioni EL (Expression Language). Tutti questi elementi vengono processati sul server.
Affinchè un web server sia in grado di operare con le pagine JSP, esso dev’essere un servlet container, ma
non basta: deve anche essere un JSP container, cioè deve essere in grado di trattare le pagine JSP.
Quando un browser richiede al web server una pagina JSP, il web server core gira la richiesta al JSP
container, che opera in due fasi: compilazione (o traduzione) della pagina, e processing della richiesta.
Nella prima fase, il JSP container prima di tutto trasforma la pagina JSP in un servlet: tutto il template text è
convertito in istruzioni println, mentre tutti gli elementi JSP vengono convertiti in codice Java; la classe che
ne risulta viene quindi compilata.
La compilazione della pagina solitamente viene eseguita quando il server riceve la prima richiesta per quella
pagina, ma è anche possibile una strategia di precompilazione, evitando il ritardo nella risposta al primo
utente che richiede la pagina.
In ogni caso, una volta che la compilazione è stata eseguita, la classe servlet è disponibile e viene usata dal
JSP container per la seconda fase: la richiesta proveniente dal client è processata dalla classe servlet
equivalente alla pagina JSP, la risposta viene composta e inviata al browser.
Finchè la pagina JSP non cambia, le richieste successive alla prima non causano compilazioni, per ciascuna
richiesta viene direttamente eseguita la seconda fase (processing della richiesta).
Le direttive hanno la forma <%@direttiva ... %>, ove direttiva può essere page, include o taglib; in generale
una direttiva è seguita da parametri. Nel caso di [Link], viene usata la direttiva taglib, ed è la prima riga:
<%@ taglib prefix="c" uri="[Link] %>
I due parametri sono prefix e uri, entrambi valorizzati; discuterò a breve la direttiva taglib. I parametri delle
direttive, e anche delle azioni, vengono detti attributi.
Dopo la prima riga, seguono righe di template text, e dopo queste, segue un elemento di tipo azione:
<html>
<head>
<title>Pagina JSP di saluti</title>
</head>
<body bgcolor="white">
<jsp:useBean id="clock" class="[Link]" />
Le azioni a loro volta si suddividono in standard action, aventi prefisso jsp, e custom action, che vedrò a
breve; quindi la precedente azione è una standard action. Le standard action sono presenti in numero
limitato, sono predefinite nel framework JSP; in particolare, la standard action <jsp:useBean> dichiara e
istanzia un Java bean utilizzato nel seguito della pagina.
pag. 143
Advanced Java rev. 1.0 Novembre 2012
Un Java bean è (per definizione) una classe Java autoconsistente, dotata di un costruttore parameterless:
quest’ultimo è il costruttore che viene invocato da <jsp:useBean>.
I tag <c:choose>, <c:when> e <c;otherwise> corrispondono a custom action; a differenza degli elementi
standard action, gli elementi custom action non sono nativamente presenti nell’architettura JSP, fanno
invece parte di librerie (JAR file) aggiuntive.
È possibile creare i propri elementi di tipo custom action, tuttavia esiste una libreria standard, detta JSTL
(JSP Standard Tag Library), che mette a disposizione un grande numero di tag utili per le esigenze più
diffuse; tra essi, rientrano i tag <c:choose> etc.
JSTL è un’estensione di JSP, implementata in più file JAR, tra i quali i due fondamentali, nella versione da
me utilizzata, sono [Link] e [Link]: questi JAR file
devono essere a disposizione del JSP container, se anche uno solo di essi manca, si verificano errori.
Essendo Tomcat il mio JSP container, ho posto le due librerie nella directory Tomcat/shared/lib.
In alternativa ai due precedenti JAR file, si può usare [Link], ponendolo in Tomcat/shared/lib; non ci sono
apprezzabili differenze.
A questo punto, è chiaro il significato della prima riga, direttiva taglib: il valore dell’attributo uri è assimilabile
alla dichiarazione di un nome simbolico, non è la locazione della libreria JSTL !
È possibile sviluppare una propria libreria di tag, aggiungendo così nuovi tag a quelli già disponibili, tuttavia
esaminerò questa possibilità solamente nel seguito; solitamente non è necessario creare nuovi tag, data
l’abbondanza di tag già definiti in JSTL.
Infine, sono presenti espressioni EL all’interno delle azioni, siano esse le custom action di JSTL oppure le
standard action: le espressioni EL vengono utilizzate per valorizzare gli attributi delle azioni. Il linguaggio EL
ha un numero limitato di keyword, come empty che utilizzerò in seguito, operatori logici (come &&) e
aritmetici, e inoltre dispone di alcune variabili implicite di interesse, che utilizzerò esse pure nel seguito (sono
analoghe agli oggetti server di ASP).
Scripting elements.
Il quarto tipo di elementi JSP è costituito dai cosiddetti scripting elements, non utilizzati in [Link]. In
pratica, uno scripting element consiste di una o più istruzioni scritte in codice Java.
Esistono tre sottotipi di scripting elements, o meglio quattro, se si considerano i commenti come scripting
elements – in una pagina JSP, è possibile porre un commento utilizzando la seguente sintassi:
<%-- testo del commento --%>
Un commento JSP è invisibile al browser: essendo uno scripting element, sta sul lato server.
A parte i commenti, i tre sottotipi di scripting elements sono: espressioni, dichiarazioni e scriptlet.
pag. 144
Advanced Java rev. 1.0 Novembre 2012
Un’espressione è un’istruzione Java che restituisce un valore, il quale va a far parte della risposta; può
anche essere usata per valorizzare l’attributo di un’azione. Ecco un esempio, in cui voglio scrivere la data e
l’ora correnti come titolo della pagina (l’esempio è una pagina JSP completa):
<head><html><title>Data/ora: <%= new [Link]()
%></title></head>></html>
Con ciò, il titolo della pagina diventa Data/ora: Sat Feb 08 00:37:18 CET 2014.
Una dichiarazione è la dichiarazione, scritta in codice Java, di una variabile o di una procedura, che
diventano (rispettivamente) un campo o un metodo dell’istanza della classe servlet risultante dalla
compilazione della pagina JSP.
Nell’esempio che segue, dichiaro una variabile e me ne servo nel seguito, mediante un’espressione:
<%! StringBuffer sb = new StringBuffer("sacone"); %>
<%= [Link]() %>
Infine, uno scriptlet è un pezzo di codice Java inserito nella pagina, eseguito al momento; eccone la sintassi:
<% codice Java %>
In genere, i scripting elements e in particolare i scriptlet dovrebbero essere considerati l’ultima risorsa, è
preferibile utilizzare azioni ed espressioni EL per quanto possibile.
Ho aggiunto alla pagina [Link] il seguente scripting element, posto subito prima del link di ritorno all’home
page [Link]:
<%! [Link]<String> ident()
{
[Link]<String> infos = new [Link]<String>();
[Link]("ClassName="+[Link]().getName());
[Link]("SuperClassName="+([Link]()).getSuperclass().getName());
[Link]("ServletName="+[Link]());
[Link]("ServletInfo="+[Link]());
[Link]("[Link]="+[Link]().getContextPath(
));
[Link]("[Link]="+[Link]().getServerInfo())
;
return infos;
}
%>
Who am I:
<c:forEach items="<%= [Link]() %>" var="inf">
<br>
<c:if test="${!(empty inf)}">${inf}</c:if>
<c:if test="${empty inf}">NULL</c:if>
</c:forEach>
Ecco il risultato:
Who am I:
ClassName=[Link].Salve_jsp
SuperClassName=[Link]
ServletName=jsp
ServletInfo=Jasper JSP 2.2 Engine
[Link]=/LightSite
[Link]=Apache Tomcat/7.0.47
pag. 145
Advanced Java rev. 1.0 Novembre 2012
La classe corrispondente alla pagina JSP ha un nome evocativo; è subtipo della classe HttpJspBase, che si
trova in Tomcat/lib/[Link], ed è una classe astratta, a sua volta subtipo della classe
[Link], la classe dalla quale discendono direttamente i servlet applicativi,
come Booklet.
Notare: una soluzione migliore è salvare il codice di cui sopra, esattamente com’è, nel file [Link], poi
includerlo nelle pagine che intendono usarlo, con la direttiva <%@ include file="[Link]" %>;
ponendo la direttiva in [Link], il risultato non cambia, ma la pagina è molto più leggibile.
È possibile utilizzare, al posto della direttiva include, la standard action <jsp:include>, scrivendo
<jsp:include page="[Link]" /> entro la pagina includente. Ci sono alcune differenze importanti
tra la direttiva e la standard action: la prima è valutata nella fase di compilazione, la seconda nella fase di
request processing, inoltre la seconda permette il passaggio dinamico del nome della pagina da includere,
ad esempio tramite un’espressione EL.
Supponiamo di utilizzare la standard action anziché la direttiva, all’interno di [Link]: l’output che si ottiene
è simile, ma con la differenza che stavolta ClassName=[Link].Reflex_jsp.
Si tratta di una differenza facilmente spiegabile: la standard action <jsp:include> opera nella fase di
processing della richiesta, quando ormai la compilazione è stata effettuata e le classi servlet equivalenti
generate per entrambe le pagine, e così esiste una classe di nome Reflex_jsp, ed è questa che esegue il
codice, è ad essa che appartiene il metodo ident. Nel caso della direttiva include, che opera nella fase di
compilazione, il codice della pagina inclusa diventa parte del codice della pagina includente, perciò in
ingresso alla fase di request processing c’è una sola classe, Salve_jsp, cui appartiene il metodo ident.
pag. 146
Advanced Java rev. 1.0 Novembre 2012
Ecco come appare la pagina, dopo che l’utente ha inserito i valori nei campi reale, immag e factor
(rispettivamente, 3, 4 e 20) e premuto il submit button:
Il codice JavaScript della funzione DataOk è stato omesso per brevità, ma è abbastanza semplice: esegue il
controllo sui tre campi di input della form, e se anche uno solo di essi risulta vuoto, oppure contenente un
valore che non può essere tradotto in un numero reale, allora dà un messaggio d’errore (alert) e restituisce
false, annullando il POST della form; altrimenti restituisce true e il POST della form viene effettuato.
pag. 147
Advanced Java rev. 1.0 Novembre 2012
La classe [Link] appartiene a [Link]; ciò significa che bisogna aggiungere a tomcat-apps/Light la
directory WEB-INF/lib (se non è già presente), copiarvi dentro [Link] e rigenerare il sito.
Ingresso nella pagina, post della form e uso dell’oggetto param di EL.
Una delle variabili implicite del linguaggio EL è l’oggetto param, che è una collezione, di tipo [Link], di
tutti i valori dei parametri della richiesta. Qui, la richiesta è provocata dal post della form Mask: premendo il
submit button goBtn, e supponendo che la funzione DataOk ritorni true, allora viene spedito al server un
messaggio di richiesta, di tipo HTTP POST, i cui parametri hanno nome e valore come i controlli della form –
non tutti i controlli HTML contenuti nella form partecipano all’operazione POST, ma di certo vi partecipano i
controlli <input TYPE=text> e il submit button, quindi i controlli di nome reale, immag, factor e goBtn.
In effetti, il messaggio HTTP POST inviato nel caso illustrato nella precedente figura, ha il seguente body:
reale=3&immag=4&factor=20&goBtn=Calcola.
Aggiungo un codice temporaneo alla pagina [Link], per meglio studiare l’oggetto EL param:
<c:if test="${!(empty param)}" >
<c:forEach items="${param}" var="par">
${[Link]} = ${[Link]}<br>
</c:forEach>
</c:if>
<c:if test="${empty param}" >param is empty<br></c:if>
Il codice di cui sopra sfrutta la natura di collezione di coppie chiave/valore dell’oggetto param. Come si vede
dal codice della pagina, per indirizzare il parametro (chiave) reale, mi basta scrivere [Link], e
analogamente [Link] etc.
Come sempre nel caso delle pagine autoimpostanti, bisogna fare attenzione al diverso comportamento del
codice all’ingresso nella pagina rispetto alle successive post della form.
All’ingresso nella pagina, param è vuoto e appare la scritta param is empty; con ciò, seguendo il codice
della pagina, ai campi del bean cx non vengono assegnati valori, e la form Mask viene presentata completa
ma con tutti i campi vuoti, infatti [Link] e tutti gli altri sono ovviamente vuoti.
L’utente inserisce i dati nei tre campi e preme il button, così facendo viene effettuata la post della form; la
action della form identifica il destinatario della post, che è la pagina stessa (di qui il termine autoimpostante).
Con ciò, il codice della pagina viene rieseguito, stavolta però param non è vuoto, e infatti dal codice
provvisorio trovo ciò che mi aspettavo: goBtn = Calcola
immaginario = 4
factor = 20
reale = 3
Posso rimuovere il codice provvisorio dalla pagina [Link] e proseguire con l’analisi del codice
effettivo: stavolta i campi del bean cx vengono valorizzati, ricevendo i valori già passati nella post; gli stessi
valori vecchi (cioè del precedente server round trip) vengono scritti nei campi della form, l’effetto per l’utente
è la conservazione dei valori nei campi della form (senza questo passaggio li rivedrebbe vuoti).
L’esecuzione dell’espressione EL ${cx} scrive il valore del complesso, 3+4i, nella table, chiamando
implicitamente il metodo toString della classe [Link]; per calcolare il modulo, l’espressione EL
corretta è ${[Link]()}. Si noti anche, sempre come espressione EL, la chiamata di un metodo con
parametro di passaggio: ${[Link]([Link])}.
Accesso al database.
La pagina [Link] permette un facile accesso ai dati del database DBPeople, usando azioni JSP,
senza ricorrere a custom bean o a scripting element. Anche [Link] è una pagina autoimpostante, ed
è autosufficiente in quanto reperisce i dati che presenta. Segue il codice della pagina:
pag. 148
Advanced Java rev. 1.0 Novembre 2012
Il codice JavaScript della funzione DataOk è stato omesso per brevità, ma è abbastanza semplice: esegue il
controllo sul campo cgn della form, ove viene scritto il nominativo (parte iniziale del cognome degli
impiegati): il campo non può essere vuoto, e deve cominciare con una lettera maiuscola; se le condizioni non
sono rispettate, viene dato un messaggio d’errore (alert) e la funzione DataOk restituisce false, annullando
il POST della form; altrimenti, la funzione restituisce true e il POST della form viene effettuato.
pag. 149
Advanced Java rev. 1.0 Novembre 2012
Su param e sul meccanismo di post valgono le considerazioni già fatte a proposito della pagina
[Link]; in particolare, all’ingresso nella pagina, la HTML textbox cgn appare vuota, essendo
vuoto param e quindi [Link], e il resto del codice della pagina non viene eseguito.
Una volta scritto qualcosa di ammissibile nella textbox cgn, premendo il submit button viene compiuto un
server round trip, cioè un post della form; stavolta param non è vuoto, per cui il valore di [Link] viene
assegnato alla HTML textbox cgn, dando all’utente la sensazione di continuità. Ma soprattutto, vengono
eseguite le azioni di accesso ai dati nel database.
Nella sua forma più semplice, l’accesso in lettura ad un database prevede l’uso di due sole azioni, entrambe
definite nella libreria JSTL e con un proprio nome simbolico: l’azione <sql:setDataSource>, che apre la
connessione al database, e l’azione <sql:query>, che effettua una query; il loro uso è molto semplice, si
intuisce senza problemi leggendo il codice della pagina.
Supponendo che l’utente abbia scritto D entro la textbox cgn, la query che risulta dall’azione <sql:query>
(comprensiva di <sql:param>) è select * from people where cognome like ‘D%’:
I risultati della query vengono posti nella variabile EL empls, definita nell’azione <sql:query>, il resto è una
semplice presentazione tabellare dei dati.
I valori ammissibili per l’attributo scope sono quattro: page, request, session e application; il default è page,
ed è il valore che viene usato quando, come nel caso di [Link], l’attributo non viene assegnato.
Inizio l’analisi trattando il solo data source.
Il data source con scope = page è visibile solamente nella pagina cui appartiene, viene creato all’ingresso
nella pagina e viene ricreato ad ogni server round trip; se scope = request, allora esso ha lo stesso ciclo di
vita della richiesta: se la pagina cui il data source appartiene effettua un forward verso un’altra pagina, allora
il data source persiste ed è visibile nella pagina acceduta, dato che si tratta sempre della stessa richiesta.
Si noti che scope = page è il più ristretto in termini sia di visibilità sia di persistenza, e scope = request è
poco più ampio: anche con scope = request, il data source viene ricreato ad ogni nuovo accesso alla pagina
che lo contiene ed anche ad ogni server round trip – si tratta ogni volta di una nuova richiesta.
pag. 150
Advanced Java rev. 1.0 Novembre 2012
Assegnando scope = session, il data source è visibile in tutte le pagine del sito, anche aprendo il sito in più
finestre del browser purchè appartenenti alla stessa sessione, e perdura finchè il browser non viene
completamente chiuso, terminando così la sessione.
Il test <c:if> è necessario, senza di esso ricreerei comunque il data source. Tutto il resto del codice della
pagina [Link] resta invariato.
Posso verificare facilmente che il data source viene creato solo una volta, nel seguito viene riutilizzato, non
importa quante volte entri ed esca dalla pagina, o quanti server round trip vengano effettuati.
Ma c’è di più: posso utilizzare il data source anche in un’altra pagina. Ad esempio, posso (provvisoriamente)
aggiungere il seguente codice alla pagina [Link]:
<c:if test="${!(empty dbPeople)}" >
<%@ taglib prefix="sql" uri="[Link] %>
<sql:query var="lista" dataSource="${dbPeople}" sql="select * from
people" />
<br>Ci sono ${[Link]} impiegati nel database<br>
</c:if>
Apro il browser, accedo il sito LightSite, entro nella pagina [Link] ed effettuo una query; poi apro una
nuova scheda (quindi appartenente alla medesima sessione), accedo in essa il sito LightSite ed entro nella
pagina [Link]: vedrò comparire la scritta: Ci sono 49 impiegati nel database.
Infine, usando scope = application, il data source è visibile in tutte le pagine del sito, anche aprendo il sito in
browser operanti in sessioni diverse, o anche accedendo il sito da browser diversi (ad esempio da Internet
Explorer e da Google Chrome); il data source persiste finchè non ricarico il sito (ad esempio, usando la voce
Reload di Tomcat Manager).
La scelta del valore da assegnare all’attributo scope del data source è questione applicativa: da un lato,
utilizzare un scope non volatile, come session o application, evita la necessità di aprire e chiudere una
connessione ogni volta che si effettua una richiesta; d’altro canto, l’uso di un data source permanente
comporta il mantenimento di una connessione sempre aperta. La scelta dipende in ultima analisi dalla natura
complessiva dell’applicazione, incluse le sue condizioni di utilizzo.
Infine, anche l’azione <sql:query> ha l’attributo scope, con gli stessi valori; tuttavia l’uso di una query
persistente mi sembra meno interessante, trovo più naturale (almeno, nella grande maggioranza dei casi)
assegnare scope = page oppure request all’azione <sql:query>.
Barriere e aperture tra codice client, codice Java e azioni nelle pagine JSP.
Nell’architettura di una pagina JSP sono presenti tre componenti costitutivi: il template text, costituito da tag
HTML, client script (solitamente ma non obbligatoriamente JavaScript); il codice Java, presente nella pagina
come scriping element (dichiarazione, assegnazione o scriptlet che sia); il codice delle azioni, che chiamo
sinteticamente (e forse impropriamente) codice JSP, comprendendo in esso anche le espressioni EL.
Tutte e tre le componenti contribuiscono alla composizione della pagina che viene presentata dal browser; il
processing del template text avviene su browser e il processing del resto su server: in base a questa
considerazione, si potrebbe pensare a due ambienti, l’ambiente client (browser) al quale appartengono tutti
gli elementi del template text, e l’ambiente server al quale appartengono tutti gli altri elementi.
Tuttavia io adotto una tripartizione, cioè penso a tre ambienti: l’ambiente client al quale appartiene ogni
elemento di tipo template text; l’ambiente codice Java al quale appartiene ogni scripting element; l’ambiente
JSP al quale appartengono le azioni e le espressioni EL, ed anche le direttive.
È importantissimo capire come i tre ambienti interagiscano tra loro, in particolare quali sono le vie praticabili
per passare le informazioni da un ambiente all’altro, e quali sono invece le barriere.
pag. 151
Advanced Java rev. 1.0 Novembre 2012
Dato un controllo HTML nella pagina JSP, dotato di un nome e di un valore, esso risulta invisibile sia al
codice Java dei scripting elements sia al codice JSP; è possibile rendere noti nome e valore del controllo al
codice delle espressioni EL effettuando la post della form che contiene il controllo, indi accedendo l’oggetto
param nel codice EL.
Non si possono accedere gli oggetti e le variabili di EL, né oggetti definiti in un’azione, dal codice Java di un
scripting element; vale anche il viceversa: definito un oggetto in un scripting element, esso risulta
sconosciuto nel codice JSP.
Se definisco un bean, ad esempio l’oggetto cx di tipo [Link] entro la standard action <jsp:useBean>,
l’oggetto cx potrà essere utilizzato nelle espressioni EL di altre azioni, ma risulterà sconosciuto al codice
Java dei scriptlet.
Se viceversa definisco cx di tipo [Link] usando un scripting element di tipo dichiarazione, esso
risulterà sconosciuto nel codice JSP (EL incluso) delle azioni.
Un scripting element di tipo assegnazione può contribuire al template text, producendo esso stesso del
template text, come nel caso seguente:
<%! int cnt = 12; %>
...
<INPUT NAME="Contatore" TYPE=Text VALUE="<%= cnt %>">
ciò non significa che lo scripting element veda il codice HTML o JavaScript, né vale il viceversa, in pratica è
come se il codice Java dell’assegnazione compisse un output testuale.
Si può usare l’assegnazione anche all’interno di codice JavaScript, il che non stupisce, essendo quest’ultimo
template text; ad esempio, posso dichiarare una variabile Java e poi mostrarne il valore:
<%! int cnt = 12; %>
...
<script language="JavaScript">
alert("Valore del contatore = " + <%= cnt %>);
</script>
Allo stesso modo, un scripting element di tipo assegnazione può contribuire ad un’azione; un esempio viene
fornito nella pagina [Link]: <c:forEach items="<%= [Link]() %>" var="inf">, l’attributo
items dell’azione è valorizzato usando codice Java (la funzione ident è implementata in un scripting
element).
Anche le espressioni EL possono essere usate per produrre template text, ad esempio:
<INPUT NAME=cgn TYPE=Text VALUE="${[Link]}" >
...
<script language="JavaScript">
alert("Valore del parametro = " + "${[Link]}");
</script>
Case study.
Voglio che l’utente della pagina [Link] venga allertato quando, effettuando una query, la somma
delle matricole trovate superi un certo limite, ad esempio 6000: si tratta di una richiesta speciosa, e una volta
soddisfatta, il codice che la soddisfa può essere rimosso, ma è interessante la soluzione fornita.
Nella pagina [Link] è già presente un loop, <c:forEach>, che scrive i risultati (righe) della query in
una HTML table; allora, definisco la variabile sommaMatricole subito prima del loop e la uso per
immagazzinarvi la somma delle matricole. Segue il codice:
<c:set var="sommaMatricole" value="0" />
<c:forEach items="${[Link]}" var="row">
<tr>
<td>${[Link]}</td>
<td>${[Link]}</td>
<td>${[Link]}</td>
<td>${row.codice_fiscale}</td>
</tr>
<c:set var="sommaMatricole" value="${sommaMatricole+[Link]}" />
</c:forEach>
pag. 152
Advanced Java rev. 1.0 Novembre 2012
L’azione <c:set> crea la variabile, sommaMatricole, se essa non esiste, e la aggiorna se già esiste.
pag. 153
Advanced Java rev. 1.0 Novembre 2012
Esiste già una pagina che soddisfa questi requisiti, ed è [Link], già studiata: utilizza il sistema
AJAX per invocare il servlet Booklet e gestire la risposta. L’oggetto XmlHttpRequest è molto utile, perché con
esso non si pongono problemi di mantenimento dei valori nei campi tra una query e l’altra: sia che venga
usata una GET o una POST, essa avviene tra l’oggetto XmlHttpRequest e il servlet (o web service), senza
affliggere il resto della pagina.
Invece BookletInfo,html non utilizza AJAX, ma due semplici HTML form, che – tramite il proprio submit
button – effettuano una POST indirizzata al servlet.
pag. 154
Advanced Java rev. 1.0 Novembre 2012
Per controllare l’accettabilità dei dati in ingresso, entrambe le form, MaskA e MaskB, chiamano la funzione
booleana DataOk; quest’ultima ha il parametro opt proprio per distinguere la form chiamante: se ‘A’, la form
chiamante è MaskA, se ‘B’ è MaskB.
La form MaskA ha un solo campo visibile di input, Nominativo; i controlli svolti da DataOk(‘A’) accertano che
il valore del campo non sia vuoto e che inizi con una lettera maiuscola. La form MaskB ha due campi visibili
di input, CodiceIniziale e CodiceFinale; i controlli svolti da DataOk(‘B’) accertano che i valori dei due campi
siano entrambi non vuoti, che corrispondano a valori interi positivi, e non siano in ordine inverso.
Entrambe le form hanno due campi hidden: Funzione, che definisce quale funzionalità viene richiesta al
servlet, e XML, con il valore Y, in modo che il servlet fornisca una risposta in formato XML (e non binario).
Nominativo=Ca&Funzione=ListaLibrodnByAutore&XML=Y&goBtn=Invia
Questo è il messaggio generato, nel caso il browser sia Internet Explorer 11; se avessi usato Google
Chrome, avrei ottenuto un messaggio un po’ diverso, ma con gli stessi header essenziali e lo stesso body.
Se avessi premuto il submit button della forma MaskB, avrei ottenuto un messaggio con lo stesso header,
ma Content-Length = 81, e il seguente body:
CodiceIniziale=1&CodiceFinale=6&Funzione=ListaLibrodnByRangeCod&XML=Y&goBtn=Invia.
Al solito, i nomi dei controlli HTML campi delle form devono essere uguali ai nomi dei parametri che il servlet
Booklet si attende nel messaggio di richiesta.
Nel caso della pagina [Link], l’oggetto XmlHttpRequest spedisce la richiesta e riceve la risposta,
così facendo i dati sono disponibili direttamente sul lato client e possono essere trattati dal codice
pag. 155
Advanced Java rev. 1.0 Novembre 2012
JavaScript; questo non è possibile nel caso della pagina [Link]: la risposta del servlet giunge
direttamente, in pratica si passa dal sito LightSite al servlet Booklet, come si vede dalla figura seguente.
Nell’esempio seguente, l’utente ha scritto Z nel campo Nominativo, e ha sottoposto la form (MaskA):
Una soluzione elegante è possibile, mediante un apposito file XSLT: trovandone la dichiarazione (o meglio, il
riferimento) in testa al documento XML, il browser utilizza il file XSLT referenziato, anziché applicare il
proprio foglio di stile default.
Allo scopo, aggiungo la seguente riga al codice di Maine, metodo doGet, nel progetto Booklet:
PrintWriter pw = [Link]();
[Link]("text/xml");
[Link]("<?xml version='1.0' encoding='ISO-8859-1'?>");
[Link]("<?xml-stylesheet type='text/xsl'
href='[Link]
[Link]("<doc>");
...
la parte colorata è la riga aggiunta, il resto è codice preesistente. Con ciò, il documento XML costruito dal
servlet Booklet è il seguente:
<?xml version='1.0' encoding='ISO-8859-1'?>
<?xml-stylesheet type='text/xsl'
href='[Link]
<doc>
<libro>
<Codice>1</Codice>
<Titolo>The two-timers (Cronomoto)</Titolo>
<Autore>Bob Shaw</Autore>
</libro>
...
</doc>
Ho creato la directory misc entro Tomcat/webapps, in modo che faccia parte del default site di Tomcat e sia
quindi facilmente accessibile da tutti i siti contenuti in Tomcat/webapps, come LightSite e anche Booklet. Il
file [Link] fornisce una presentazione dei dati XML, dà al documento XML una veste grafica, ma non si
limita a questo, infatti fornisce anche una via di ritorno alla pagina [Link] e quindi al sito LightSite.
14
In figura, il browser utilizzato è Internet Explorer 11; anche Chrome applica un foglio di stile default ai dati
XML, un po’ diverso, ma non molto, da quello di Internet Explorer.
pag. 156
Advanced Java rev. 1.0 Novembre 2012
Nella direttiva xsl:stylesheet, la dichiarazione del namespace di prefisso xsl ha valore di namespace, non di
effettivo collegamento Internet; i tag XSLT appartengono a quel namespace.
<xsl:stylesheet xmlns:xsl=[Link]
version="1.0">
<xsl:template match="/">
<HTML>
<HEAD>
<TITLE>Elenco Libri</TITLE>
<SCRIPT language="JavaScript">
function back()
{ [Link]("/LightSite/[Link]","_parent"); }
</SCRIPT>
</HEAD>
<BODY>
<H3>Elenco di Libreria
<INPUT name="RetHPBtn" type="Button" value="Torna alla Home
Page"
onclick="return back();" />
</H3>
<xsl:if test="count(doc/Libro) > 0">
<H3>Trovati <xsl:value-of select="count(doc/Libro)"/>
libri</H3>
<TABLE BORDER="2">
<TR><TD>Codice</TD><TD>Titolo</TD><TD>Autore</TD></TR>
<xsl:for-each select="doc/Libro">
<TR>
<TD><xsl:value-of select="Codice"/></TD>
<TD><xsl:value-of select="Titolo"/></TD>
<TD><xsl:value-of select="Autore"/></TD>
</TR>
</xsl:for-each>
</TABLE>
</xsl:if>
<xsl:if test="count(doc/Libro) = 0"><H3>Nessun libro
trovato</H3>
</xsl:if>
</BODY>
</HTML>
</xsl:template>
</xsl:stylesheet>
C’è un solo template, applicato al documento XML in toto, infatti l’attributo match punta al root element del
documento XML.
Segue l’impostazione HTML della pagina, in particolare viene definito il HTML button, RetHPBtn, mediante il
quale – con l’invocazione di un’apposita funzione JavaScript – si può tornare alla pagina [Link]; è
importante notare il valore _parent assegnato al secondo parametro della funzione [Link], senza il
quale la pagina [Link] verrebbe visualizzata in una nuova finestra15.
15
Siccome la window è sempre la stessa, al di là che in essa vengano aperte pagine del sito LightSite o
documenti provenienti dal servlet Booklet, avrei anche potuto passare il valore _self al secondo parametro di
[Link], ottenendo lo stesso risultato ottenuto con l’uso di _parent.
pag. 157
Advanced Java rev. 1.0 Novembre 2012
Le modifiche per passare da [Link] a [Link] sono ben circoscritte: aggiungo la solita
direttiva <%@ taglib prefix="c" uri="[Link] %> in testa alla
pagina, poi circondo i campi di input delle due form con il codice JSP di recupero del valore:
Nominativo
<c:if test="${!(empty [Link])}">
<INPUT NAME=cgn TYPE=Text VALUE="${[Link]}">
</c:if>
<c:if test="${(empty [Link])}">
<INPUT NAME=cgn TYPE=Text VALUE="">
</c:if>
Modifico anche il file [Link], facendogli referenziare la pagina JSP anzichè la pagina HTML.
Tuttavia questa soluzione non funziona: utilizzando il tasto di ritorno, RetHPBtn definito entro [Link],
viene riaperta nel browser la pagina [Link], i cui campi (Nominativo, CodiceIniziale e CodiceFinale)
appaiono però vuoti: non c’è persistenza.
Ciò accade perché param è vuoto, e lo è perché, rientrando in [Link] dopo la pressione del tasto
RetHPBtn, la richiesta è già stata esaurita, quindi param – che ha lo scope della richiesta – è inizializzato.
Con ciò, l’uso della pagina JSP non può risolvere il problema della persistenza.
Un cookie è un insieme di dati, nella forma di una o più coppie nome/valore, che viene conservato più o
meno permanentemente da un browser; il browser mantiene i cookie nella directory (locale) dei file
temporanei, di qui la persistenza.
I cookie presenti nel browser possono essere stati ricevuti da un sito web, quindi originati da un server, ma
possono anche essere generati da codice client (JavaScript), come nel caso di [Link]. Nel primo
caso si suppone che il browser client e il web server si scambino i cookie, ad esempio per una
identificazione dell’utente; nel secondo caso, il browser non invia i cookie al server, cioè al servlet Booklet: i
cookie restano su client, e servono come strumenti di persistenza di informazioni residenti sul client.
pag. 158
Advanced Java rev. 1.0 Novembre 2012
Operativamente, i cookie vengono creati quando l’utente, dopo aver inserito valori nei campi delle form,
preme uno dei due submit button: in tal caso, viene chiamata la funzione JavaScript DataOk, che esegue
controlli sui dati inseriti, e, se i controlli vengono superati, crea i cookie:
function DataOk(opt)
{
... controlli (codice preesistente) ...
[Link] = "Nomen="+[Link];
[Link] = "PrimoCodice="+[Link];
[Link] = "SecondoCodice="+[Link];
return true;
}
L’oggetto DOM Document ha la proprietà cookie, che ospita tutti i cookie definiti nel documento; inizialmente
la proprietà è vuota, poi, dopo aver eseguito le istruzioni al termine della funzione DataOk, essa contiene le
tre coppie nome/valore.
Prima di tutto, indago la persistenza e la visibilità dei cookie; per ferlo, eseguo il seguente esperimento: entro
in [Link] e scrivo Nominativo = As, CodiceIniziale = 11 e CodiceFinale = 23, poi premo il button
Invia su MaskA: vengono visualizzati i libri degli autori il cui cognome comincia con As; premo il tasto di
ritorno a [Link], ignoro al momento i contenuti dei campi delle form, e torno alla home page; di qui,
scelgo un’altra pagina, ad esempio [Link], nella quale ho aggiunto un’istruzione atta a visualizzare i
cookie, come alert([Link]), ecco il risultato: Nomen=As; PrimoCodice=11; SecondoCodice=23.
Con ciò, i cookie sono persistenti, e visibili da tutte le pagine del sito; si può dimostrare facilmente che lo
scope dei cookie sopra definiti, sia per la persistenza, sia per la visibilità, è la sessione: aprendo una nuova
sessione (con l’apposita voce del menu del browser), la proprietà [Link] risulta vuota.
Prima di tutto, scrivo una funzione JavaScript in grado di restituire il valore di un cookie del quale si conosce
il nome; la scrivo nella sezione <script> di <header> della pagina (che già ospita DataOk):
function FindValOfCookie(nome)
{
var target = nome +"=";
var ca = [Link](";");
for (var i=0;i<[Link];i++)
{
var c = ca[i].trim();
if ([Link](target) == 0)
return [Link]([Link],[Link]);
}
return "";
}
Il codice è autoesplicativo: invocandola con il nome di un cookie, ad esempio Nomen, se il cookie è stato
creato ne restituisce il valore, altrimenti, non trovandolo, restituisce una stringa vuota.
Di seguito alla definizione di MaskA, che resta invariata, aggiungo un blocco di codice JavaScript:
<script language="JavaScript">
var memo = FindValOfCookie("Nomen");
if (memo != "") [Link] = memo;
</script>
Il codice viene sempre eseguito; nel tag <form> di MaskA, il campo Nominativo riceve value = "", ma se
il cookie Nomen è già stato creato all’interno del codice di DataOk, allora il campo Nominativo riceve il valore
del cookie, nell’esempio As; se invece il cookie non è presente, il valore di Nominativo resta vuoto.
pag. 159
Advanced Java rev. 1.0 Novembre 2012
memo = FindValOfCookie("SecondoCodice");
if (memo != "") [Link] = memo;
</script>
Con ciò, tornando alla pagina [Link] dopo una POST, non importa su quale delle due form, l’utente
ritrova i valori inseriti nei campi di entrambe le form.
Il problema può essere facilmente risolto, a patto di saper distinguere tra una POST e un ingresso nella
pagina; allo scopo, aggiungo un’altra funzione JavaScript nella sezione <script> di <header>:
function isFirstAccess()
{
return ([Link]("[Link]") >= 0);
}
La proprietà referrer dell’oggetto document contiene il nome della pagina di provenienza: in caso di primo
ingresso, è la pagina [Link] (home page), mentre in caso di POST, [Link] è vuoto… questo,
almeno, è il comportamento di Internet Explorer.
Stabilito il criterio di distinzione tra ingresso nella pagina e POST, aggiungo la seguente funzione JavaScript,
sempre nella sezione <script> di <header>:
function InitMask()
{
if (isFirstAccess())
{
[Link] = "";
[Link] = "";
[Link] = "";
}
return;
}
La funzione InitMask dev’essere invocata subito dopo la fine del caricamento della pagina, perciò sull’evento
onload del tag <body>: <body bgcolor="white" onload="InitMask();">.
Ciò garantisce che, nel caso di ingresso, i campi delle form, che hanno ricevuto i valori contenuti nei cookie,
vengano reinizializzati: infatti, essi appaiono vuoti.
Come si è già detto, i cookie così creati su lato client hanno come scope (sia in visibilità, sia in persistenza)
la sessione.
La funzionalità di per sé non è affatto complicata: nel progetto Booklet, aggiungo alla classe DAO il metodo
public static Giudiziodn[] giudiziAutoriByNome(String nomen); questo metodo accede,
nel database Libreria, la table MIXAUTORI in join con la table AUTORI per costruire nomi e cognomi.
Ecco il risultato della query, nel caso in cui il parametro nomen (ossia, il campo Nominativo) valga S:
pag. 160
Advanced Java rev. 1.0 Novembre 2012
Il tipo Giudiziodn, definito nel package dao del progetto Booklet, ha i campi definiti in accordo coi risultati
della query (anche se il primo campo si chiama codice, e non autore).
Tuttavia ho stabilito che la funzionalità GiudiziSuAutori sia protetta: quando il servlet ne riceve la richiesta,
prima di tutto verifica che il client abbia le giuste credenziali, cioè quelle di un critico letterario.
Il client deve inviare nella richiesta, oltre al parametro Nominativo, una coppia username/password
adeguata; il servlet estrae dalla richiesta i parametri Nickname e Password e li confronta con i contenuti della
(nuova) table CRITICI del database Libreria: se la coppia è presente nella table, il test è superato, altrimenti
il servlet risponde al client solamente con uno pseudorecord Giudiziodn, nel cui campo giudizio sta scritto
FORBIDDEN.
Se invece il test viene superato, allora il servlet invoca il metodo [Link] e passa la risposta al
client, inoltre nel messaggio di risposta il servlet passa al client il cookie Semaforo: verde. A questo punto, il
client è in possesso del cookie, e nelle successive richieste di tipo GiudiziSuAutori può accedere senza più
specificare la coppia Nickname/Password.
I campi Funzione e Nominativo devono essere sempre presenti, mentre Client è opzionale; il campo XML
può essere assente, nel qual caso il codice si comporta come se fosse presente e avesse valore negativo; i
campi Nickname e Password possono o meno essere presenti.
Nel caso di Funzione = GiudiziSuAutori, il codice del metodo doGet verifica la presenza dei cookie, anzi la
presenza di un particolare cookie, cioè quello destinato al passaggio veloce:
Cookie[] cooks = [Link]();
boolean telePass = false;
if (cooks != null)
{
for (Cookie coo : cooks)
if ([Link]().matches("Semaforo") &&
[Link]().matches("verde"))
{
telePass = true;
break;
}
}
Poiché il cookie Semaforo: verde è creato dal servlet dopo che è avvenuto un accesso client corretto,
almeno una prima volta il test non sarà superato e quindi risulterà telePass = false.
pag. 161
Advanced Java rev. 1.0 Novembre 2012
Nel caso l’accesso veloce alla funzionalità non sia possibile, allora il codice ricava le credenziali d’accesso
alla funzionalità protetta, ossia la coppia Nickname e Password: se la coppia è presente e definita nella
richiesta, e se corrisponde alle credenziali di un critico letterario – ossia, se c’è un record della table CRITICI
che ha i campi NICK e PWD rispettivamente uguali a nic e pass,
Il valore finale di telePass, in assenza del cookie Semaforo: verde, dipende solamente dalla validità delle
credenziali; in presenza del cookie, telePass è vero e le credenziali non vengono affatto controllate.
Se telePass è vero, vengono reperiti (se presenti) i giudizi sugli autori il cui cognome inizia come cgn, ove
cgn è il valore (nomen) del parametro Nominativo; come per gli altri valori di Funzione, i dati vengono passati
nell’oggetto generico writable.
Se invece telePass è falso, allora viene generata una risposta d’errore, strutturalmente identica al caso
positivo, cioè un oggetto di tipo Giudiziodn[], ma con un solo item nel cui campo giudizio è scritto
FORBIDDEN (è responsabilità del client interpretare ciò come un errore d’accesso).
Se la risposta non è attesa in formato XML, l’oggetto writable viene scritto sul response output stream;
altrimenti, il codice prosegue formattando i dati binari contenuti in writable come un documento XML
(nell’esempio seguente, Nominativo = Si):
<?xml version='1.0' encoding='ISO-8859-1'?>
<?xml-stylesheet type='text/xsl' href='[Link]
<doc>
<GiudizioAutore>
<Codice>16</Codice>
<Nominativo>Clifford D. Simak</Nominativo>
<Giudizio>Bravo</Giudizio>
<Note>Autore dalla qualità non costante</Note>
<Voto>700</Voto>
</GiudizioAutore>
</doc>
Prima però di comporre il documento XML, che costituisce il body della risposta, il codice del metodo doGet
si preoccupa della creazione e del passaggio del cookie Semaforo: verde:
if ([Link]("GiudiziSuAutori") && (cooks == null)16)
{
Giudiziodn[] agp = (Giudiziodn[])writable;
if (([Link] == 0) || (!agp[0].[Link]("FORBIDDEN")))
{
Cookie cook = new Cookie("Semaforo","verde");
[Link](cook);
}
}
Il cookie viene creato, se già non esiste, e a patto che non mi trovi nel caso di accesso negato (è invece
accettabile la risposta senza dati, che si verifica quando per quella parte iniziale di cognome, nessun autore
16
Il test effettuato funziona correttamente solo se l’unico cookie previsto è Semaforo: verde; più in generale,
dovrei utilizzare un test più selettivo, ossia verificare, a fronte di cookie non null, che non sia già presente il
cookie Semaforo: verde.
pag. 162
Advanced Java rev. 1.0 Novembre 2012
reca un giudizio): il cookie è creato e aggiunto al messaggio di risposta, ove comparirà come una riga del
HTTP header, Set-Cookie: Semaforo=verde.
Nel controllo HTML <select>, di nome Funzione, aggiungo l’opzione GiudiziSuAutori; aggiungo, nella
HTML table Fissa, a fianco della HTML textbox argo, un’altra HTML textbox argo2 e il controllo HTML di tipo
password, di nome pass; in questo modo, è possibile selezionare l’opzione GiudiziSuAutori nella tendina,
passare il nominativo (parte iniziale del cognome) entro argo, e – come risulta necessario almeno la prima
volta – passare anche le credenziali, cioè il nickname entro argo2 e la password entro pass.
Aggiungo alla funzione JavaScript CallServlet il codice necessario alla gestione del caso GiudiziSuAutori:
var conn = "[Link]
[Link]+"&Client=AjaxSync&XML=y";
... segue codice specifico per [Link] = "GiudiziSuAutori" ...
conn = conn + "&Nominativo="+[Link];
var body = "";
if ([Link] > 0) // suppongo di non avere il cookie
body += "&Nickname="+[Link]+"&Password="+[Link];
[Link]("POST",conn,false);
[Link]("Content-Type","application/x-www-form-
urlencoded");
[Link](body);
l’operatore può passare o meno Nickname servendosi del campo argo2: se non lo passa e il cookie non è
stato ancora ricevuto, riceverà un errore (FORBIDDEN); se invece lo passa e il cookie è già stato ricevuto,
allora Nickname e Password vengono semplicemente ignorati. Comunque, qui assumo che l’operatore abbia
inserito il nickname in argo2 e si tratti della prima richiesta alla funzionalità GiudiziSuAutori.
Infine, aggiungo a CallServlet il codice necessario per la corretta formattazione del documento XML,
utilizzando i nomi dei tag definiti in Booklet e adattando l’ampiezza delle celle alla grandezza dei valori.
pag. 163
Advanced Java rev. 1.0 Novembre 2012
Qui sopra viene mostrato il risultato dell’interrogazione: vengono mostrati tutti i giudizi presenti per gli autori il
cui cognome comincia con S. Come si vede, l’utente ha passato le credenziali.
A questo punto, il client riceve il cookie Semaforo: verde dal servlet; nelle successive interrogazioni non è più
necessario passare le credenziali, perché il client spedisce automaticamente il cookie al servlet.
Ecco il messaggio HTTP originato ripetendo l’interrogazione, coi campi argo2 e pass vuoti:
POST /Booklet/Answers?Funzione=GiudiziSuAutori&XML=y&Nominativo=S HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: user-pc
Content-Length: 0
Cache-Control: no-cache
Cookie: Semaforo=verde
La lunghezza del body è 0; dopo l’ultma riga dell’header, seguono due righe vuote. Si noti la presenza del
cookie, ricevuto dal servlet e automaticamente rimpallato ad ogni richiesta.
Supponiamo che [Link] richieda GiudiziSuAutori senza avere il cookie, ma passando credenziali
corrette: oltre ai dati, la pagina riceve pure il cookie Semaforo: verde. Da questo momento in poi, ogni altra
pagina, o altro componente client come un applet, quando rivolge una qualsiasi richiesta a Booklet,
automaticamente passa il cookie.
pag. 164
Advanced Java rev. 1.0 Novembre 2012
Come già detto, il passaggio del cookie avviene ogni volta che un componente client rivolge una qualsiasi
richiesta al servlet Booklet, ad esempio anche quando una pagina chiede la lista dei generi.
Terminata la sessione, il cookie perde validità, e in una nuova sessione, quando si accede alla funzionalità
GiudiziSuAutori, è necessario ripetere il passaggio delle credenziali.
pag. 165
Advanced Java rev. 1.0 Novembre 2012
Attualmente, l’ultima versione di Eclipse è Kepler SR2, tuttavia nativamente essa supporta Java 1.7 ma non
Java 1.8; affinchè sia possibile usare Java 1.8 nella IDE di Eclipse, bisogna installare un’apposita feature, in
sostanza un plugin chiamato “Eclipse JDT patch with Java 8 support”.
La via più semplice per installare la patch è l’uso della voce HelpEclipse Marketplace, che attiva il
marketplace client; sfogliando le liste dei vari tab del marketpkace client, si può trovare la feature indicata
come Java 8 support for Eclipse Kepler SR2: deve essere installata.
Fatto ciò, bisogna sincerarsi che, in ogni progetto che intenda servirsi di nuove feature come le espressioni
lambda, il compliance level del compilatore sia settato al valore 1.8; la via più semplice consiste nell’adottare
tale compliance level a livello di workspace: da un qualsiasi progetto, accedo (col tasto destro) a
PropertiesJava Compiler e di qui alla voce Configure Workspace Settings, e nella finestra successiva
scelgo 1.8 come compliance level del compilatore. Con ciò, ogni nuovo progetto nasce con compliance 1.8.
Le espressioni lambda.
A partire dalla versione 1.8, Java fornisce supporto al calcolo funzionale mediante le espressioni lambda.
Queste sono così chiamate in quanto conformi al formalismo del calcolo lambda.
Il progetto Lambda.
Creo il Java project Lambda, che fornisce un’introduzione euristica all’uso delle espressioni lambda. È un
progetto molto semplice, consistente di due package, app che contiene le classi Maine (dotata di metodo
main) e Agente, e landa, contenente l’interfaccia XFace, preposta alla costruzione delle espressioni lambda.
In un’interfaccia funzionale, è possibile usare ogni tipo, primitivo o costruito (ad esempio [Link]), sia
come parametro di passaggio, sia come tipo di ritorno; anche void è ammissibile.
pag. 166
Advanced Java rev. 1.0 Novembre 2012
[Link]([Link](t, q));
[Link]([Link](t, q));
Ecco il codice del metodo adder della classe Agente, che implementa un’espressione lambda:
public String adder(String txt,int ad)
{
[Link]("metodo adder#");
XFace xFace = (String t,int i)->
{
if (i > 15) return t+[Link](i);
StringBuilder sb = new StringBuilder(t);
for (int k=0;k<[Link]();k++)
[Link](k, (char)([Link](k)+i));
return [Link]();
};
return [Link](txt, ad);
}
Premetto che l’identità di signature tra il metodo adder e il metodo dell’interfaccia funzionale non è
necessaria, è venuta fuori per comodità e per caso.
L’espressione lambda è costituita dalle righe successive alla prima nel corpo dle metodo, con l’eccezione
dell’ultima; la sua struttura è la seguente: Interfaccia interf = (parameter list)-> { body }.
Se l’interfaccia funzionale ha un metodo senza parametri, allora parameter list è assente, ma devono restare
le parentesi tonde, cioè Interfaccia interf = ()-> { body }.
Se il metodo dell’interfaccia funzionale non ha tipo di ritorno, cioè è definito void, allora il body non può
contenere istruzioni del tipo return x, ma solo return, oppure nessuna.
L’ultima istruzione del metodo adder è importante perché mostra come utilizzare l’espressione lambda,
tuttavia non presenta caratteristiche di rilievo.
Gli altri due metodi dell’interfaccia funzionale XFace, adderPlus e hRev, sono implementazioni (o funzioni)
distinte, ma obbediscono alle stesse regole generali.
pag. 167
Advanced Java rev. 1.0 Novembre 2012
Se tentassi di accedere la variabile sum, definita nel body dell’espressione lambda, al di fuori del body,
otterrei un errore di compilazione, la variabile sum risulterebbe non visibile.
La variabile v non è dichiarata final, tuttavia è come se lo fosse, si dice che v è effectively final; infatti, se
provo ad alterarne il valore dopo averla definita, in qualsiasi parte del codice del metodo – nel body
dell’espressione lambda, ma anche nell’enclosing scope, sia prima sia dopo il body – ottengo un errore di
compilazione. In sostanza, posso compilare solo se la variabile si comporta come final.
Nota Bene: se una variabile è dichiarata non nell’enclosing scope del body dell’espressione lambda, ma più
esternamente, ad esempio come membro della classe cui appartiene il metodo, allora nel body
dell’espressione lambda è possibile non solo usarla, ma anche modificarne il valore.
i tipi dei parametri vengono inferiti dal compilatore, in base all’unico metodo (non default, non statico)
dell’interfaccia funzionale (XFace) usata nella definizione e costruzione dell’espressione lambda.
Se il metodo dell’interfaccia funzionale ha un solo parametro, ad esempio R metodo(T x), allora la parameter
list nelle espressioni lambda si sempifica ulteriormente: Interfaccia iFace = x->{ body }.
Creo due interfacce funzionali, XFactor e XBino (entrambe appartenenti al package landa):
public interface XFactor public interface XBino
{ {
long fattoriale(int n); long binomio(int a,int b,int c,String o);
} }
Aggiungo il metodo (pubblico) Comparatore alla classe Agente, che viene chiamato dal metodo main di
Maine: il metodo Comparatore effettua il rapporto tra n! e (a+b)c, nel caso o non inizi col carattere –, oppure
tra n! e (a–b)c, nel caso o inizi col carattere –; in base al valore del rapporto, fornisce una scritta informativa.
Tuttavia ipotizzo che il metodo Comparatore riceva da Maine non solo i valori, ma anche gli algoritmi di
calcolo, sia del fattoriale, sia della potenza del binomio; ecco il codice nel metodo main di Maine:
[Link]([Link](q,
N->
{
long l = 1;
if (N < 0) l = -1; else for (int i=1;i<=N;i++) l *= i;
return l;
},
r,s,u,t,
(A,B,C,O)->
{
if ([Link]("-")) return (long) [Link](A-B,C);
else return (long) [Link](A+B,C);
}));
pag. 168
Advanced Java rev. 1.0 Novembre 2012
Nel caso d’uso seguente, ho passato al codice del metodo main (da riga di comando) i valori q = 5, r = 7, s =
4, u = 2, t = +:
quindi n! = 5! = 120, (a+b)c = (7+4)2
= 121, perciò ratio = 0,9917 e
quindi 0.99 < ratio < 1, il che spiega
la scritta in output (frazione quasi
uguale a 1 per difetto).
Nelle due espressioni lambda scritte nel codice del metodo main, ho utilizzato la notazione abbreviata della
parameter list in ambo i casi, ma avrei potuto utilizzare la notazione estesa.
Considero l’interfaccia DoubleFace, contenuta nel package oldStuff del progetto Lambda:
public interface DoubleFace
{
public long bino(int a,int b);
public String nTOs(long v);
}
L’interfaccia ha due metodi, perciò non è un’interfaccia funzionale e quindi non può essere utilizzata per
costruire espressioni lambda. Tuttavia non è necessario creare una classe che la implementi, è possibile
utilizzare la tecnica della classe anonima.
pag. 169
Advanced Java rev. 1.0 Novembre 2012
};
return txt+"_"+[Link]([Link](ad, 3))+"_"+[Link](ad);
}
La costruzione della classe anonima è quella comunemente usata per definire event handler dei controlli
Swing o SWT. Anche qui vale la regola di visibilità delle variabili rispetto all’enclosing scope che racchiude la
classe anonima.
A basso livello, la classe anonima viene trattata come una sottoclasse della classe ospite; nel mio esempio,
la classe ospite è [Link], e la classe anonima ha nome [Link]$1.
Notare: l’obbligo delle interfacce funzionali di essere monometodo sembra essere una notevole limitazione
all’utilizzo delle espressioni lambda rispetto all’uso delle classi anonime; ma, come si vedrà nei successivi
paragrafi, questa limitazione viene assai ridotta da un’altra nuova caratteristica di Java 1.8, i default method.
I default method.
I default method, detti anche defender method, sono una nuova caratteristica di Java 1.8, che permette di
scrivere codice nei metodi dichiarati in un’interfaccia; grazie ai default method, le interfacce diventano di fatto
simili alle classi astratte.
Il metodo nTOs è non solo dichiarato, ma anche già codificato all’interno dell’interfaccia XXFace; suppongo
di creare (nel package mix del progetto Lambda) una classe XXClass che implementi l’interfaccia XXFace:
public class XXClass implements XXFace
{
@Override
public String opex(String str, int n)
{
return "XX:"+str+"_"+[Link](n);
}
}
pag. 170
Advanced Java rev. 1.0 Novembre 2012
{
@Override
public String opex(String str, int n)
{
return "XX:"+str+"_"+[Link](n);
}
@Override
public String nTOs(long n)
{
String r = [Link](n);
return [Link]();
}
}
È molto interessante la notazione utilizzata per recuperare il default method dell’interfaccia che la classe sta
implementando: alla keyword super ho dovuto prefissare il nome dell’interfaccia (pena errori di
compilazione), il che ha senso tenendo conto che una classe può implementare più interfacce, le quali
potrebbero dichiarare metodi omonimi.
Adesso rieseguo il codice del metodo main di Maine, sempre con t = Virgilio e q = 12000, ed ecco l’output:
metodo [Link]#XX:Virgilio_12000
metodo [Link]#2ee0
Mi servo di un esempio per mostrare la sinergia tra espressioni lambda e default method.
L’interfaccia XFace ha il solo metodo opex; l’interfaccia XXFace ha il metodo opex come XFace, inoltre ha
altri due metodi, il default method nTOs e il metodo statico factor. Più elegantemente, apporto un
cambiamento al progetto lambda: lascio XFace invariata, ma rendo XXFace estensione di XFace.
pag. 171
Advanced Java rev. 1.0 Novembre 2012
Creo il metodo smart della classe Agente, che costruisce ed usa la stessa espressione lambda già vista per
il metodo [Link], ma si serve anche degli altri metodi di XXFace:
public String smart(String txt,int ad)
{
[Link]("metodo smart#");
XXFace xxFace = (String t,int i)->
{
if (i > 15) return t+[Link](i);
StringBuilder sb = new StringBuilder(t);
for (int k=0;k<[Link]();k++)
[Link](k, (char)([Link](k)+i));
return [Link]();
};
return [Link](txt,ad)+"_"+[Link](ad)
+"_"+[Link](ad);
}
Posso creare una classe (eventualmente, anche anonima) che implementi l’ìnterfaccia XXFace, continuando
anche ad usare l’interfaccia per definire espressioni lambda.
In sintesi, un’interfaccia funzionale può avere più di un metodo, a patto che i metodi distinti da quello usato
per costruire le espressioni lambda siano default method oppure metodi statici; in questo caso, quando si
utilizza l’interfaccia per costruire un’espressione lambda, bisogna usare la signature del metodo non default
e non statico. Con ciò, se provo a utilizzare l’interfaccia XXFace usando la signature del default method
nTOs, ottengo un errore di compilazione, in quanto il compilatore si aspetta l’uso del metodo opex.
Un’interfaccia avente un solo metodo, definito però default method oppure statico, non è un’interfaccia
funzionale e non può essere usata per costruire espressioni lambda.
Programmaticamente, ogni classe che rappresenta un flusso di dati (stream) in ingresso, discende dalla
classe [Link]; ogni classe che rappresenta un flusso di dati (stream) in uscita, discende dalla
classe [Link].
Tutto ciò continua a valere in Java 1.8, ove però compare un package, [Link], assente in Java 1.7;
questo package contiene l’interfaccia Stream<T>, che rappresenta un concetto di stream un po’ diverso, ma
ancora analogo al concetto tradizionale di stream come flusso di dati. Una definizione migliore di stream
verrà fornita nel seguito.
Tra le varie interfacce che estendono Iterable, c’è [Link]<T>: programmaticamente, le collezioni
sono tutte le classi che implementano Collection<T>; di conseguenza, le collezioni hanno il metodo iterator e
possono essere oggetto dello statement foreach.
pag. 172
Advanced Java rev. 1.0 Novembre 2012
Con Java 1.8, l’interfaccia Iterable<T> si arricchisce: oltre a iterator, essa prevede i metodi default forEach e
spliterator; per il momento, ignoro spliterator. Di conseguenza, le collezioni hanno il metodo forEach.
Sempre con Java 1.8, l’interfaccia Collection<T> si arricchisce dei metodi default stream e parallelStream,
che restitcuiscono un oggetto di tipo Stream<T>: una collezione Coll<T>, chiamando il metodo stream,
restituisce un oggetto di tipo Stream<T>, ossia una sequenza di oggetti di tipo T costituita dagli elementi
presenti nella collezione Coll<T> al momento della chiamata.
Nel seguito si chiarirà la differenza tra uno stream sequenziale, quale è lo stream resituito da
Coll<T>.stream, e uno stream parallelo, quale è Coll<T>.parallelStream; per il momento, limito il mio
interesse a stream sequenziali.
Programmaticamente, uno stream è una classe che implementa l’interfaccia Stream; si può però dare una
definizione più esplicativa del concetto di stream: uno stream rappresentato da Stream<T> è una sequenza
(un flusso) di elementi di tipo T, che supporta operazioni aggregate.
Le operazioni aggregate sono possibili solamente su flussi cioè su stream, e sono definite come metodi in
Stream<T>; essenzialmente sono tre: filter, map, forEach; sono operazioni aggregate anche alcune
specializzazioni delle tre precedenti, ad esempio mapToInt oppure forEachOrdered.
Non confondere il metodo forEach della collezione con il metodo forEach dello stream.
Una qualsiasi classe collezione, ad esempio Vector<T>, ha il metodo forEach, in quanto implementa
l’interfaccia Collection<T>, che eredita tale metodo dall’interfaccia Iterable<T>; inoltre la classe collezione ha
il metodo stream, il quale ritorna un oggetto di tipo Stream costituito da tutti gli elementi della collezione, ed
anche l’interfaccia Stream ha il metodo forEach.
Con ciò, data ad esempio la collezione vx di tipo Vector<Complex>, è possibile scrivere l’istruzione
[Link](x -> [Link]()), oppure [Link]().forEach(x -> [Link]()).
L’effetto delle due istruzioni è lo stesso: tutti gli elementi del vettore vx vengono coniugati. Tuttavia sarebbe
sbagliato identificare le due istruzioni.
Il metodo forEach di un oggetto di tipo Stream è una vera operazione aggregata, mentre il metodo forEach
della classe collezione non lo è: nell’interfaccia Stream, il metodo forEach è definito come un semplice
instance method, void forEach(Consumer<? super T> action); nell’interfaccia Collection, il metodo
forEach è definito come default void forEach(Consumer<? super T> action), e
l’implementazione default esegue l’azione su ogni elemento della collezione utilizzando un ciclo foreach.
Mentre i cicli come foreach operano sempre e soltanto sequenzialmente, le operazioni aggregate possono
anche operare parallelamente: ciò accade quando lo stream sul quale avvengono è parallelo, anche se non
è sempre detto che, dato uno stream parallelo, le operazioni aggregate eseguano parallelamente.
pag. 173
Advanced Java rev. 1.0 Novembre 2012
Supponiamo che il metodo sia stato invocato da [Link], con u = 20: il vettore vx contiene 20 numeri
complessi, 1+2i, 2+3i, e così via fino a 20+21i.
L’istruzione [Link]() restituisce un oggetto sx di tipo Stream<Complex>, uno stream coi 20 numeri
complessi. Agire sugli elementi del vettore o su quelli dello stream dà gli stessi risultati, dato che essi fanno
riferimento agli stessi oggetti, cambia solo il modo di referenziarli.
La prima operazione aggregata è filter: applicata allo stream sx, essa genera lo stream sxf, ancora di tipo
Stream<Complex>, ma riguardante i soli numeri complessi, tra i 20 originari, aventi modulo il cui quadrato
sia intero – si tratta di 3+4i, che ha modulo al quadrato 16 = 42, e 20+21i, di modulo al quadrato 841 = 292.
Segue l’operazione aggregata forEach, applicata al nuovo stream sxf, che stampa i due numeri complessi:
3+4i ha modulo quadrato
20+21i ha modulo quadrato
Osservazione importante: una volta che un’operazione aggregata è stata eseguita, il riferimento simbolico
allo stream diventa inutilizzabile.
Ad esempio, riprendo il codice precedente: dopo aver applicato il primo filtro e ottenuto sxf, provo ad
applicare un nuovo filtro sullo stream referenziato da sx, come segue:
Stream<Complex> sx = [Link]();
Stream<Complex> sxf = [Link](v ->
{ ... });
Stream<Complex> sx2 = [Link](v -> [Link]() > 3);
il primo filtro genera, a partire da sx, uno stream referenziato come sxf costituito dai due numeri complessi
3+4i e 20+21i; il secondo filtro si applica ancora a sx, che si suppone sia rimasto invariato e quindi
contenente i 20 complessi iniziali, e dovrebbe generare lo stream sx2 contenente i soli complessi aventi
parte reale > 3.
Quando lancio il programma, si verifica un’eccezione appena viene tentata l’esecuzione del secondo filtro:
stream has already been operated upon or closed.
pag. 174
Advanced Java rev. 1.0 Novembre 2012
[Link]();
return v;
})
.forEach(v -> [Link](v + " ha modulo quadrato"));
}
Premesso che le prime istruzioni si limitano a caricare i dati nel vettore, è molto interessante notare come
tutto il lavoro restante sia compiuto da un’unica, lunga istruzione, costituente un buon esempio di pipeline di
operazioni aggregate.
In generale, una pipeline consiste di uno stream sul quale agire (qui, [Link]), un certo numero di
operazioni intermedie (qui, tre: due filter e un map; potrebbero anche essere zero, cioè nessuna operazione
intermedia), e al più un’operazione terminale (qui, forEach); quest’ultima, se presente, chiude la pipeline.
L’operazione terminale non è sempre presente, e in questo caso la pipeline restituisce uno stream,
possibilmente con elementi di tipo diverso dal tipo dello stream di partenza.
Nella precedente pipeline, se avessi omesso il forEach finale, avrei ottenuto uno stream di tipo
Stream<Complex>; considero una forma un po’ modificata dello stesso codice:
Stream<String> ss = [Link]()
.filter(v ->
{
double m = [Link]();
return (m - (int)m == 0);
})
.filter(v -> [Link]() > L)
.map(v ->
{
[Link]();
return v + " ha quadrato del modulo intero";
});
in questo caso l’ultima operazione della pipeline è intermedia, trattandosi di map; in base al tipo di ritorno
dell’espressione lambda utilizzata in map, ciò che viene restituito è uno stream di stringhe.
Stream nativi.
L’interfaccia Stream è generica, ma oltre ad essa esistono interfacce specializzate, da utilizzarsi quando gli
elementi dello stream appartengono a certi tipi primitivi di dati: appartengono al package [Link] le
tre interfacce DoubelStream, IntStream e LongStream.
Considero un esempio concreto, fornito dal metodo mediaComplessi della classe Agente:
public void mediaComplessi(int u)
{
Vector<Complex> vx = new Vector<Complex>();
for (int i=1;i<=u;i++) [Link](new Complex(i,i+1));
OptionalDouble mReal = [Link]()
.mapToDouble(v -> [Link]())
.average();
OptionalDouble mImg = [Link]()
.mapToDouble(v -> [Link]())
.average();
Complex mcx = new Complex([Link](),[Link]());
[Link]("Complesso medio = "+mcx);
}
pag. 175
Advanced Java rev. 1.0 Novembre 2012
Scopo del metodo è, data una certa sequenza di numeri complessi, trovarne la media (come media di una
successione di numeri complessi, intendo il numero complesso avente come parte reale la media aritmetica
delle parti reali dei complessi appartenenti alla sequenza, e analogamente per la parte immaginaria).
Le due prime righe di codice creano il vettore vx ospite e lo popolano con u numeri complessi, poi segue
l’istruzione che calcola la parte reale della media, si tratta di una pipeline in cui lo stream oggetto è
[Link], che è di tipo Stream<Complex>; l’interfaccia Stream ha il metodo mapToDouble, specializzato nel
generare un flusso di double.
Per esteso, senza pipeline, sarebbe: DoubleStream ds = [Link]().mapToDouble(v-
>[Link]())
ove si ottiene uno stream di dati native, di tipo double, descrivibile come DoubleStream ma non come
Stream<Double>17, essendo quest’ultimo uno stream di oggetti e non di dati primitivi.
Il metodo average calcola la media aritmetica dei dati di tipo double, e restituisce un oggetto di classe
OptionalDouble; questa classe è stata introdotta in Java 1.8, essa è riguardabile come un contenitore
(container) che può contenere un valore di tipo double oppure essere vuoto.
L’oggetto di tipo OptionalDouble ha metodi appositi per appurare se un valore è presente; qui non vengono
usati, siccome do per scontato che sia u > 0. Posto che un valore sia presente, esso è recuperabile
mediante il metodo getAsDouble.
Entrambi i metodi hanno lo scopo di ordinare gli elementi, di tipo T, dello stream; il primo utilizza il criterio dì
ordinamento proprio del tipo T, mentre il secondo utilizza un criterio di comparazione esterno.
È interessante lo stratagemma utilizzato per caricare i dati entro il vettore che funge da contenitore dei
numeri complessi.
Con Java 1.8, la classe Random è stata dotata di metodi che generano flussi (stream) infiniti di dati primitivi
– i metodi sono ints, longs, doubles; qui ho utilizzato lo stream di interi randomici, iStream, di tipo IntStream:
la parametrizzazione scelta garantisce che gli interi assumano valori compresi tra 1 e 99.
Recupero l’iteratore sullo stream, ite di tipo [Link],OfInt, quest’ultima è un’interfaccia che
rappresenta un iteratore su valori interi; il metodo next dell’iteratore restituisce l’intero (randomico) letto dal
flusso, me ne servo per generare u numeri complessi da immagazzinare nel vettore vx.
17
La notazione Stream<double> genera un errore di compilazione.
pag. 176
Advanced Java rev. 1.0 Novembre 2012
L’esecuzione dell’istruzione di ordinamento causa un errore (eccezione), perché Complex non è un tipo
ordinato; se voglio usare il metodo sorted nella sua forma più semplice, devo rendere Complex ordinato.
Come criterio di comparazione tra due numeri complessi, ho utilizzato il confronto tra i loro moduli, che,
essendo numeri reali, sono immediatamente comparabili.
L’ordinamento ha funzionato.
Come esempio, espongo il metodo ordinaPersone della classe Agente: il metodo prima crea il vettore px, di
tipo Vector<Persona>, ove [Link] è una classe della libreria LibJar/[Link], avente il campo intero
matricola pubblico, e altri due campi di tipo stringa, nome e cognome; la classe Persona non è ordinata.
Aggiungo al vettore px alcuni elementi, uno dopo l’altro, con le matricole disposte disordinatamente; voglio
ricavare (e stampare) uno stream di tipo Stream<Persona> ordinato per matricole crescenti. Ecco il codice:
Stream<Persona> sord = [Link]().sorted((p1,p2) ->
{
if ([Link] < [Link]) return -1;
if ([Link] > [Link]) return 1;
return 0;
});
[Link](v -> [Link](v));
ho usato il metodo sorted nella sua seconda forma, passandogli un’espressione lambda congrua con
l’interfaccia [Link]<T>, qui T è Persona.
L’interfaccia Comparator<T> è un’interfaccia funzionale, il cui solo metodo (non default e non statico, e con
l’esclusione di equals ereditato da Object) è int compare(T t1,T t2), l’espressione lambda di sopra è
una sua semplice implementazione. Ecco il risultato:
pag. 177
Advanced Java rev. 1.0 Novembre 2012
L’ordinamento ha funzionato.
Se altero un oggetto Tk nell’ambito della collezione vx, lo ritrovo alterato accedendolo dallo stream sx, e
viceversa. Tuttavia l’ordinamento non è altrettanto intercambiabile.
Se aggiungo elementi al vettore vx, eseguendo un forEach su vx trovo gli elementi nell’ordine in cui li ho
aggiunti, e lo stesso ordine lo trovo se eseguo [Link]; ciò significa che, inizialmente, l’ordine di
comparsa degli oggetti (ossia, dei loro riferimenti) è lo stesso, nella collezione e nello stream.
Supponiamo che venga eseguito un sort su sx: facendo il forEach su sx trovo gli elementi ordinati, ma
eseguendo il forEach su vx ritrovo la successione iniziale, non quella ordinata: l’ordine degli oggetti in vx è
rimasto inalterato.
Ad esempio, creo vx di tipo Vector<Complex> e gli aggiungo, nell’ordine, 2+5i, i e 3+4i; eseguendo
l’istruzione [Link](c -> [Link](c)), ottengo la sequenza sopra elencata.
Non vale il viceversa, e lo mostro attraverso un esempio: creo vx e lo popolo con la sequenza 2+5i, i e 3+4i,
poi recupero il riferimento allo stream, Stream<Complex> sx = [Link]().
Se intendo usare l’ordinamento naturale del tipo T (posto che T sia ordinato), anziché passare un criterio
esterno di comparazione come espressione lambda, posso scrivere sort(null); così, essendo il tipo
[Link] ordinato, e stabilito che il suo naturale criterio d’ordinamento mi soddisfi, posso riscrivere il
codice precedente come segue: [Link](null);
pag. 178
Advanced Java rev. 1.0 Novembre 2012
Stream standalone.
Finora ho considerato solamente stream derivanti da collezioni, tuttavia uno stream può esistere di suo,
senza tirare in ballo alcuna collezione: un oggetto di tipo Stream<T> può essere creato, popolato (o
costruito), e quindi utilizzato, a prescindere da ogni collezione. Si parla in questo caso di stream standalone.
Allo scopo, l’interfaccia Stream dispone del metodo statico builder; volendo creare uno stream standalone,
come prima mossa creo il builder, ad esempio: Builder<Complex> b = [Link]().
Tramite il metodo add, posso quindi aggiungere elementi al builder: [Link](new Complex(3,4t)) etc.
Quando ritengo di aver terminato il popolamento dello stream builder (building phase), eseguo l’istruzione
Stream<Complex> sx = [Link](); l’istruzione [Link] fa evolvere lo stato dello stream builder a built,
e restituisce il riferimento allo stream: tutti gli elementi contenuti nello stream builder diventano elementi dello
stream appena generato, nello stesso ordine in cui sono stati aggiunti allo stream builder.
Lo stream che ne risulta è del tutto analogo a quelli già visti, ottenuti partendo da oggetti di tipo
Vector<Complex> e in generale da collezioni.
Notare: dopo la chiamata a [Link], lo stream builder passa nello stato built, e non è più possibile aggiungere
elementi.
I method reference.
I method reference sono in pratica una stenografia utilizzabile in alternativa ad alcune espressioni lambda;
come si vedrà nei prossimi paragrafi, esistono quattro diverse tipologie di method reference.
L’espressione lambda in filter consiste adesso di una chiamata di metodo sul parametro, e nient’altro. Al
posto dell’espressione lambda in questione, posso utilizzare un method reference:
public void filtraComplessi(int u)
{
...
.filter(Complex::isSquared)
...
}
Mostro un altro esempio, nel quale sono presenti parametri di passaggio (isSquared è parameterless): si
tratta del metodo ordinaPersone della classe Agente. In esso, l’ordinamento viene imposto dall’esterno,
poiché Persona non è un tipo ordinato:
[Link]()
pag. 179
Advanced Java rev. 1.0 Novembre 2012
.sorted((p1,p2) ->
{
if ([Link] < [Link]) return -1;
if ([Link] > [Link]) return 1;
return 0;
})
.forEach(v -> [Link](v));
Il tipo Persona continua a non essere ordinato, infatti non implementa l’interfaccia Comparable, ho
semplicemente aggiunto un nuovo metodo, e me ne servo per riscrivere il codice precedente:
[Link]()
.sorted((p1,p2) -> [Link](p2))
.forEach(v -> [Link](v));
Anche qui, l’espressione lambda in sorted consiste di una sola chiamata di metodo, e può essere sostituita
da un riferimento al metodo comparaMatricole della classe Persona:
[Link]()
.sorted(Persona::comparaMatricole)
.forEach(v -> [Link](v));
Questa è una delle forme più comuni di method reference: è il riferimento a un metodo d’istanza (cioè, non
statico) di un oggetto arbitrario avente un certo tipo.
Nel primo esempio, il metodo d’istanza è isSquared e il tipo è [Link]; nel secondo esempio, il metodo
d’istanza è comparaMatricole e il tipo è [Link]; in entrambi, si fa riferimento a un oggetto generico: nel
primo caso, v di tipo Complex – e il metodo è parameterless; nel secondo caso, p1 di tipo Persona – e il
metodo ha un parametro d’ingresso, p2 di tipo Persona.
Il metodo Comparatore della classe Agente è dichiarato come public String Comparatore(int n,
XFactor xf, int a, int b, int c, String o, XBino xb), perciò lo statement d’invocazione
nel metodo main di Maine può essere riscritto come segue:
pag. 180
Advanced Java rev. 1.0 Novembre 2012
[Link]([Link](q,[Link]::Fattoriale,
r,s,u,t,[Link]::Binomio));
Questa è un’altra forma abbastanza comune di method reference: è il riferimento a un metodo statico di un
certo tipo. A prima vista, sembra uguale al riferimento a un metodo non statico di un oggetto arbitrario del
tipo considerato, però si tratta di due espressioni concettualmente diverse.
Ne mostro un altro esempio di utilizzo, fornito dal metodo coeffBino della classe Agente:
public long coeffBino (int n,int k)
{
XFactor xf = [Link]::Fattoriale;
return [Link](n)/([Link](k)*[Link](n-k));
}
Qui l’approvvigionamento dell’algoritmo di calcolo del fattoriale è compiuto dal metodo coeffBino, il main si
limita a passare i soli valori. Con ciò, l’espressione lambda non è passata al metodo coeffBino dall’esterno,
ma è costruita e quindi usata dal metodo stesso; è come se nel metodo coeffBino avessi scritto l’istruzione
XFactor xf = n -> [Link](n), però al posto dell’espressione lambda esplicita
ho utilizzato la sua stenografia tramite un method reference.
Ecco la soluzione:
public long coeffBino (int n,int k)
{
[Link] fun = new [Link]();
XFactor xf = fun::Fattoriale;
return [Link](n)/([Link](k)*[Link](n-k));
}
Posso facilmente riscrivere anche la chiamata, da parte del metodo main, al metodo [Link]:
[Link] f = new [Link]();
[Link]([Link](q,f::Fattoriale,r,s,u,t,[Link]::Binomio))
;
qui viene ancora usato un method reference, ma si tratta del riferimento al metodo d’istanza di un oggetto
particolare, non arbitrario, e il tipo dell’oggetto (qui, [Link]) non compare.
Riferimento ad un costruttore.
Ci sono casi in cui un’espressione lambda si limita a restituire un oggetto. Come primo esempio, alquanto
accademico, considero il seguente scenario: il metodo main di Maine invoca un metodo (d’istanza) della
classe Agente, eseguendo l’output del risultato, senza sapere nulla delle caratteristiche dell’oggetto
restituito, senza nemmeno sapere il tipo dell’oggetto restituito:
[Link](agente.fetch2Double(q, r));
pag. 181
Advanced Java rev. 1.0 Novembre 2012
L’interfaccia funzionale XGet2Double ha il (solo) metodo public Object get(double a,double b);
con ciò, l’espressione lambda a destra è un’implementazione adeguata: dati due parametri di tipo double,
essa restituisce un oggetto di tipo Complex, e ciò va bene dato che tutto ciò che si chiede è che ci siano due
parametri d’ingresso di tipo double e che ci sia un elemento qualsiasi restituito.
Anche questa espressione lambda può essere sostituita da un method reference, di tipo particolare, cioè il
riferimento ad un costruttore:
public Object fetch2Double(double a,double b)
{
XGet2Double xg = Complex::new;
return [Link](a, b);
}
Nonostante la classe Complex abbia anche un altro costruttore, tutto funziona correttamente, perché il
compilatore Java è in grado di selezionare il costruttore corretto in base alla parametrizzazione del metodo
get dell’interfaccia funzionale.
Mostro un altro esempio, più elaborato, nel quale l’interfaccia funzionale ha natura veramente generale; si
tratta dell’interfaccia CatchAny, appartenente al package intfc, compresa nella libreria LibJar/[Link]:
public interface CatchAny
{
public Object gotcha(Object... pars);
}
Ciò che viene restituito è un oggetto generico, e la parameter list può essere qualsiasi, anche vuota.
Poi considero la classe [Link], appartenente alla libreria LibJar/[Link]; si tratta di una classe
puramente accademica:
public class StrangeClass
{
private long num;
private Complex cx;
private String str;
public StrangeClass(Object... pars)
{
switch([Link])
{
case 0:
num = 0;
cx = new Complex();
str = "";
break;
case 1:
num = (long) pars[0];
cx = new Complex(num,num);
str = [Link](num, true);
break;
case 2:
num = (long) pars[0];
cx = new Complex(num-1,num+1);
str = (String) pars[1];
if (num != 0) str += [Link](num, true);
break;
}
}
@Override
public String toString()
{
return "StrangeClass: the long is "+num+", the complex is
"+cx+
pag. 182
Advanced Java rev. 1.0 Novembre 2012
La classe ha un costruttore molto elastico, si comporta come tre costruttori diversi a seconda del numero di
parametri passati; è compreso il caso del costruttore parameterless.
ove u e t, parametri passati da linea di comando, sono rispettivamente un intero ed una stringa.
La prima chiamata del main si rivolge al costruttore parameterless, infatti il risultato non cambia al variare di
u e t: StrangeClass: the long is 0, the complex is 0 and the string is .
La seconda chiamata del main si rivolge al costruttore con un solo parametro, di tipo long; ecco il risultato
per u = 12000 e t qualsiasi:
StrangeClass: the long is 12000, the complex is 12000+12000i and the string is
2EE0.
La terza chiamata del main si rivolge al costruttore con due parametri, di tipo long e String (nell’ordine); ecco
il risultato per u = 12000 e t = 0x:
StrangeClass: the long is 12000, the complex is 11999+12001i and the string is
0x2EE0.
Anche in questo caso, il compilatore Java sa come chiamare correttamente il costruttore (cioè, con quanti e
quali parametri).
Per di più, essi coprono solamente un sottoinsieme delle espressioni lambda: un’espressione lambda può
essere costituita da un numero arbitrario di istruzioni, mentre un method reference corrisponde ad
un’espressione lambda costituita da una sola istruzione.
Ad esempio, riprendo il metodo fetch2Double della classe Agente: anziché limitarsi a restituire in ogni caso
un numero complesso usando i due parametri di passaggio come parti reale e immaginaria, il metodo deve
comportarsi ancora così solo nel caso in cui ambo i parametri siano strettamente positivi, altrimenti deve
restituire null. Questo nuovo comportamento è facilmente realizzabile mediante un’espressione lambda:
public Object fetch2Double(double a,double b)
{
XGet2Double xg = (d1,d2) ->
{
if ((d1 > 0) && (d2 > 0)) return new Complex(d1,d2);
else return null;
pag. 183
Advanced Java rev. 1.0 Novembre 2012
};
return [Link](a, b);
}
Tuttavia non posso tradurre quest’espressione in un method reference, a meno di non creare un metodo ad
hoc in una classe coinvolta, ad esempio Complex.
Infine, anche nel caso di espressioni lambda costituite da un’unica istruzione, questa dev’essere la chiamata
di un metodo o di un costruttore, e non altro.
Ad esempio, l’istruzione XGet2Double xg = (a,b) -> 2*a+b; è ben costruita e di immediato utilizzo;
ma non avrebbe senso cercare di riscriverla usando un reference method, proprio perché, nella sua
immediatezza, non è la chiamata di un metodo o di un costruttore.
pag. 184