JBoss Application Server e Sviluppo di Applicazioni Distribuite in

annuncio pubblicitario
SAPIENZA – Università di Roma
Facoltà di Ingegneria
Corso di Laurea in Ingegneria Gestionale e
Ingegneria Informatica
Dispensa didattica
JBoss Application Server e Sviluppo di
Applicazioni Distribuite in J2EE.
Concetti di Base ed Esempi.
M. Bartiromo, V. De Angelis, M. de Leoni, F. de Leoni, M.
Mecella, R. Russo, S. Saltarelli
Creative Commons License Deed
Attribuzione - Non commerciale - Non opere derivate
2.5 Italia
Tu sei libero:
• di riprodurre, distribuire, comunicare al pubblico, esporre in pubblico,
rappresentare, eseguire e recitare questa opera
Alle seguenti condizioni:
Attribuzione. Devi attribuire la paternità dell’opera nei modi indicati dall’autore o da chi ti ha dato l’opera in licenza.
Non commerciale. Non puoi usare questa opera per fini commerciali.
Non opere derivate. Non puoi alterare o trasformare quest’opera, nè usarla per crearne un’altra.
Ogni volta che usi o distribuisci questa opera, devi farlo secondo i termini di
questa licenza, che va comunicata con chiarezza.
In ogni caso, puoi concordare col titolare dei diritti d’autore utilizzi di quest’opera non consentiti da questa licenza.
Niente in questa licenza indebolisce o restringe i diritti degli autori.
Le utilizzazioni consentite dalla legge sul diritto d’autore e gli
altri diritti non sono in alcun modo limitati da quanto sopra.
Questo è un riassunto in linguaggio accessibile a tutti del Codice Legale
(la licenza integrale) disponibile all’indirizzo:
http://creativecommons.org/licenses/by-nc-nd/2.5/it/legalcode.
Indice
Introduzione
5
1 RMI
1.1 Dalle RPC a RMI (Object Broker) . . . . . . .
1.2 La serializzazione e la trasmissione degli oggetti
1.3 Architettura RMI . . . . . . . . . . . . . . . . .
1.4 Uso di RMI . . . . . . . . . . . . . . . . . . . .
1.5 Esempio: Sender e Receiver . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2 Swing
2.1 Il package Swing . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Top Level Container . . . . . . . . . . . . . . . . . . . . . . .
2.2.1 Uso di JFrame . . . . . . . . . . . . . . . . . . . . . . .
2.3 Paranoramica di alcuni widget . . . . . . . . . . . . . . . . . .
2.4 L’ereditarietà per personalizzare i frame . . . . . . . . . . . .
2.5 I Layout Manager e la gerarchia di contenimento . . . . . . . .
2.5.1 Layout Management . . . . . . . . . . . . . . . . . . .
2.5.2 Progettazione della GUI con le gerarchie di contenimento
2.5.3 Progettazione top down di interfacce grafiche . . . . . .
2.6 La Gestione degli Eventi . . . . . . . . . . . . . . . . . . . . .
2.6.1 Implementazione dell’event delegation . . . . . . . . . .
2.6.2 Un esempio: elaborare gli eventi del mouse . . . . . . .
2.6.3 Uso di adapter nella definizione degli ascoltatori . . . .
2.7 La gestione degli eventi Azione . . . . . . . . . . . . . . . . .
2.8 Accedere dall’ascoltatore agli oggetti di una finestra . . . . . .
2.9 Condividere gli ascoltatori per più oggetti . . . . . . . . . . .
2.10 Conclusioni . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
7
10
13
16
25
30
31
32
33
34
39
41
41
45
47
51
51
53
55
55
57
61
67
3 Servlet e Java Server Pages (JSP)
68
3.1 Introduzione alle Servlet . . . . . . . . . . . . . . . . . . . . . 68
3.2 Il Ciclo di Vita di una Servlet . . . . . . . . . . . . . . . . . . 69
2
INDICE
3.3 Come Interagire con una Servlet: Richieste
3.3.1 Esempio Data . . . . . . . . . . . .
3.3.2 Esempio Rubrica . . . . . . . . . .
3.4 Le Sessioni . . . . . . . . . . . . . . . . . .
3.4.1 Esempio Login . . . . . . . . . . .
3.5 Le Java Server Pages (JSP) . . . . . . . .
3.5.1 Esempio Lista Prodotti (carrello) .
3
e
.
.
.
.
.
.
Risposte
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
72
72
77
82
85
88
94
4 JMS: Enterprise Messaging (ed introduzione su MOM)
102
4.1 Introduzione al MOM (richiami) . . . . . . . . . . . . . . . . . 103
4.1.1 Modelli di Messaging . . . . . . . . . . . . . . . . . . . 103
4.2 Introduzione a JMS . . . . . . . . . . . . . . . . . . . . . . . . 105
5 Enterprise Java Bean (EJB)
114
5.1 Introduzione agli EJB . . . . . . . . . . . . . . . . . . . . . . 114
5.2 Fondamenti degli EJB . . . . . . . . . . . . . . . . . . . . . . 116
5.3 Scheletro di un Enterprise Bean . . . . . . . . . . . . . . . . . 119
5.3.1 Enterprise Bean Class . . . . . . . . . . . . . . . . . . 119
5.3.2 L’EJB Object . . . . . . . . . . . . . . . . . . . . . . . 120
5.3.3 L’Home Object . . . . . . . . . . . . . . . . . . . . . . 121
5.3.4 Le interfacce locali . . . . . . . . . . . . . . . . . . . . 122
5.3.5 Deployment Descriptor . . . . . . . . . . . . . . . . . . 123
5.4 Session Bean . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
5.4.1 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
5.4.2 Ciclo vita dei Session Bean . . . . . . . . . . . . . . . . 127
5.4.3 Creazione di un Session Bean Stateless . . . . . . . . . 129
5.4.4 Creazione di un Session Bean Stateful . . . . . . . . . . 136
5.5 Entity Bean . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
5.5.1 Caratteristiche . . . . . . . . . . . . . . . . . . . . . . 145
5.5.2 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
5.5.3 Creazione di un Entity Bean Bean-Managed persistent 148
5.5.4 Creazione di un Entity Bean Container-Managed persistent . . . . . . . . . . . . . . . . . . . . . . . . . . . 168
5.6 Message-driven Bean . . . . . . . . . . . . . . . . . . . . . . . 184
5.6.1 Utilizzo dei MDB . . . . . . . . . . . . . . . . . . . . . 184
6 Applicazione completa sugli EJB
6.1 Vendita di libri on-line . . . . . . . . . . .
6.2 Alcuni Diagrammi UML dell’Applicazione
6.3 Package BookOrder . . . . . . . . . . . . .
6.4 Package BookOrderMDB . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
186
186
191
193
194
INDICE
6.5
6.6
6.7
6.8
6.9
Package Archivio . . . .
Package BookStoreSwing
Modulo Accesso.war . .
Il deployment descriptor
Provare l’applicazione . .
4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
194
204
204
215
215
7 Web Service
229
7.1 Le tecnologie alla base dei Web Services . . . . . . . . . . . . 230
7.1.1 SOAP: Simple Object Access Protocol . . . . . . . . . 232
7.1.2 WSDL: Web Service Definition Language . . . . . . . . 235
7.1.3 UDDI: Universal Description Discovery and Integration 244
8 Applicazione completa sui Web Service
248
8.1 Il Web Service . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
8.2 Il Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Appendice
257
A.1
Servlet e JSP . . . . . . . . . . . . . . . . . . . . . . . 257
A.2
Uso di JMS con JBOSS . . . . . . . . . . . . . . . . . 258
Bibliografia
261
Introduzione
Oggigiorno, un numero sempre crescente di sviluppatori di sistemi software
per le imprese sta spostando il suo interesse verso applicazioni distribuite
transazionali con lo scopo di usufruire della maggiore velocità, sicurezza e affidabilità delle tecnologie server-side. Molti, infatti, sono i vantaggi derivanti dall’adozione di un modello distribuito: un sensibile miglioramento delle
prestazioni complessive, una maggiore semplicità nella gestione delle risorse
distribuite e un sostanziale incremento delle potenzialità operative. Ad esempio, si può pensare di suddividere un processo computazionalmente pesante
in sottoroutine più piccole ed eseguire tali “pezzi di applicazione” su macchine diverse, ottenendo una drastica diminuzione del tempo complessivo di
esecuzione. L’esigente mondo dell’e-commerce e dell’information technology,
in continuo cambiamento, richiede che le applicazioni aziendali siano progettate e costruite con uso di minori risorse economiche e maggiore velocità di
quanto non accadesse in precedenza. Un importante scenario in cui è utile
l’utilizzo del modello distribuito è quello in cui si devono realizzare applicazioni che si interfacciano con codice legacy: in tal caso si può pensare di
inglobare gli applicativi esistenti (legacy appunto) in oggetti remoti e pilotarne in tal modo le funzionalità da un client scritto con i nuovi paradigmi
di programmazione.
Per programmazione distribuita si intende tipicamente quel particolare
paradigma computazionale in cui una applicazione viene organizzata in moduli differenti, localizzati in spazi di indirizzamento diversi tra loro.
La programmazione distribuita consente di realizzare sistemi
service-oriented in cui un componente A è in grado di invocare i servizi di
un componente B dispiegato su un computer remoto: in tale scenario, per
semplicità, si definisce client il componente chiamante, mentre il remoto è
detto server.
I sistemi distribuiti sono generalmente costruiti al di sopra di un middleware. Esso fornisce astrazioni di programmazione che nascondono alcune
delle complessità che costituiscono la realizzazione di un’applicazione distribuita. I diversi componenti che cooperano a fornire un servizio composito
5
Introduzione
possono essere dispiegati su piattaforme diverse. I middleware hanno quindi
anche il compito di nascondere l’eterogeneità delle piattaforme dietro una
astrazione comune.
Il crescente uso del Web come canale di accesso ai sistemi informativi ha
forzato le piattaforme middleware a fornire il supporto per l’accesso al Web.
Questo supporto è tipicamente fornito sotto forma di Application Server.
Gli Application Server sono equivalenti alle piattaforme middleware. La
differenza principale è l’incorporazione del Web come canale di accesso ai
servizi implementati utilizzando il middleware.
Le tecnologie su cui possono basarsi gli Application Server sono due:
Microsoft .NET e Java di Sun. In termini di funzionalità, gli Application
Server basati su Java e quelli basati su .NET presentano molte similitudini.
Ciononostante, quella basata su Java è nettamente più diffusa e accettata,
ed annovera numerose implementazioni sia commerciali che open source.
Per ridurre i costi, il progetto e lo sviluppo di applicazioni, la Piattaforma
Java2 Enterprise Edition (J2EE) fornisce un approccio basato sui componenti per la progettazione, lo sviluppo, l’assemblamento e il dispiegamento
delle applicazioni d’impresa. La piattaforma J2EE offre un modello di applicazione distribuita multilivello, componenti riusabili, un modello di sicurezza
unificato, un controllo flessibile delle transazioni, e un supporto ai servizi web
mediante scambio di informazioni basato su XML.
Generalmente, gli Application Server di tipo open source vengono impiegati per la ricerca, la didattica e nelle piccole e medie imprese, che non
possono permettersi soluzioni commerciali. Spesso, questi prodotti “a basso
costo” non hanno nulla da invidiare alle soluzioni commerciali, sia nell’efficienza che nelle fnzionalità disponibili. Tuttavia, soffrono di una scarsa
documentazione.
Alla luce di queste considerazioni, questo lavoro ha come obbiettivo quello di fornire una valida documentazione di supporto all’uso delle tecnologie J2EE, con particolare attenzione a uno dei maggiori e più conosciuti
Application Server di tipo open source: JBOSS.
In particolare, questo materiale è utilizzato come supporto didattico dagli
studenti del corso di Progettazione del Software 2, tenuto dall’Ing. Massimo Mecella nei corsi di Laurea Specialistica in Ingegneria Informatica e
in Ingegneria Gestionale di SAPIENZA – Università di Roma, per l’anno
accademico 2006/2007 e 2007/2008.
6
Capitolo 1
RMI
1.1
Dalle RPC a RMI (Object Broker)
Uno dei requisiti fondamentali per implementare un sistema distribuito è disporre di un sistema di comunicazione fra macchine diverse, basato su standard e protocolli prestabiliti. In Java la gestione dei socket è un compito
relativamente semplice, tanto che si possono realizzare in maniera veloce
sistemi di comunicazione con i quali scambiare informazioni in rete o controllare sistemi remoti. L’implementazione di una connessione via socket
risolve però solo il problema di fondo (come instaurare la connessione) ma
lascia in sospeso tutta la parte di definizione delle modalità di invocazione e
dei vari protocolli per lo scambio delle informazioni. Prima dell’introduzione
di RMI, erano già disponibili strumenti per l’esecuzione di codice remoto,
basti pensare alla Remote Procedure Call (RPC). Con questa tecnologia
è possibile gestire procedure facenti parte di applicazioni remote rispetto al
chiamante. Le RPC furono introdotte all’inizio degli anni Ottanta e divennero immediatamente la base per costruire sistemi 2-tier, i quali ereditarono
molte delle notazioni e delle assunzioni usate a partire dalle RPC. In particolare, le RPC stabilirono la nozione di client (il programma che chiama una
procedura remota) e di server (il programma che implementa la chiamata di
procedura remota che viene invocata), nonchè il concetto di Interface Definition Language (IDL). La forza delle RPC era il fatto che esse si basavano
su un concetto che i programmatori di quel tempo conoscevano molto bene:
la procedura. Le RPC resero possibile cominciare a costruire applicazioni
distribuite senza dover cambiare il linguaggio o il paradigma di programmazione. Molti sono i vantaggi derivanti dall’adozione di tale modello: un
sensibile miglioramento delle prestazioni complessive, una maggiore semplicità nella gestione delle risorse distribuite, e un sostanziale incremento delle
7
1.1 Dalle RPC a RMI (Object Broker)
potenzialità operative. Ad esempio si può pensare di suddividere un processo
computazionalmente pesante in subroutine più piccole ed eseguire tali “pezzi
di applicazione” su macchine diverse ottenendo una drastica diminuzione del
tempo complessivo di esecuzione. Nel caso in cui invece l’efficienza non sia l’obiettivo principale, si può comunque trarre vantaggio da una organizzazione
distribuita, potendo gestire meglio e più semplicemente le varie risorse localizzate nei differenti spazi di indirizzamento. Si pensi per esempio a una
strutturazione a tre livelli (3-Tier) per la gestione di database relazionali in
Internet: dal punto di vista del client ci si deve preoccupare esclusivamente
dell’interfacciamento con l’utente e dello scambio con il server remoto delle
informazioni contenute nel database. Un altro importante scenario in cui è
utile l’utilizzo del modello distribuito è quello in cui si debbano realizzare
applicazioni che si interfacciano con codice legacy: in tal caso si può pensare
di inglobare gli applicativi esistenti (legacy appunto) in oggetti remoti e pilotarne in tal modo le funzionalità da un client. Tutto ciò che veniva richiesto
a un programma per diventare un componente di un sistema distribuito era
di essere compilato e collegato con il corretto set di librerie RPC. Lo sviluppo di una applicazione distribuita con le RPC è basato su una metodologia
ben definita. Assumiamo di voler sviluppare un server che implementi una
procedura che debba essere usata in remoto da un singolo client. Il primo
passo è quello di definire l’interfaccia della procedura. Questo viene fatto
con un IDL, che fornisce una rappresentazione astratta della procedura in
termini di quali parametri essa prende in ingresso e quali parametri restituisce. Tale descrizione IDL può essere considerata la specifica dei servizi
offerti dal server. Una volta ottenuta la descrizione IDL si può procedere
nello sviluppo del client e del server. Il secondo passo consiste nel compilare
la descrizione IDL. Ogni implementazione delle RPC e ogni middleware che
utilizza le RPC fornisce un compilatore di interfaccia. Osservando la Figura
1.1 possiamo osservare che la compilazione di una interfaccia IDL produce:
Client Stub: lo stub è un pezzo di codice da compilare e collegare con il
client. Quando il client chiama una procedura remota, la chiamata
che viene realmente eseguita è una chiamata locale alla procedura fornita dallo stub. Lo stub allora si preoccupa di localizzare il server,
formattare i dati in modo opportuno (il che implica marshaling e serializzazione1 , comunicare con il server, ottenere una risposta, e inoltrare
tale risposta come parametro di ritorno della procedura invocata dal
1
il marshaling è l’operazione di trasformazione dei dati in un formato intermedio sul
quale i diversi processi remoti si sono accordati o che è stato stabilito in una specifica
rilasciata da un consorzio internazionale (vedi XDR). Infatti, i vari processi remoti possono
girare eventualmente su differenti piattaforme. In questo modo il messaggio può essere
compreso da ogni ricevente; la serializzazione consiste nel trasformare il messaggio e i suoi
8
1.1 Dalle RPC a RMI (Object Broker)
codice
client
processo client
interfaccia nel
linguaggio client
client stub
ambiente di
sviluppo
IDL
sorgenti
IDL
compilatore IDL
9
processo server
codice
server
interfaccia nel
linguaggio server
server stub
moduli di
supporto
Figura 1.1: Sviluppo di applicazioni distribuite con RPC
client. In altre parole, lo stub è un proxy per la reale procedura implementata al server. Lo stub fa apparire la procedura come una normale
procedura locale (dal momento che è una parte di codice del client). Lo
stub, comunque, non implementa la procedura, ma tutti i meccanismi
necessari a interagire con il server remoto, allo scopo di eseguire quella
particolare procedura.
Server Stub: la natura del server stub è simile a quella del client stub eccetto che per il fatto che esso implementa il lato server dell’invocazione.
Esso cioè contiene il codice per ricevere l’invocazione dal client stub,
formattare i dati in modo opportuno (in modo speculare rispetto al
client stub, comporta deserializzazione e unmarshaling), invocare la
reale procedura implementata nel server, e inoltrare i valori di ritorno
della procedura al client stub. Cosı̀ come il client stub, anche il server
stub deve essere compilato e collegato con il codice del server.
Le RPC hanno visto il massimo del loro successo nei sistemi Unix e sono
strettamente legate al concetto di processo, ma male si inseriscono nel contesto del paradigma basato sugli oggetti. È questo il motivo principale alla
base dell’esigenza di una tecnologia apposita, come RMI, per la gestione di
oggetti distribuiti. La Remote Method Invocation (RMI) consente di
complessi parametri in una stringa di byte prima di mandare il messaggio in un canale di
comunicazione
1.2 La serializzazione e la trasmissione degli oggetti
realizzare applicazioni distribuite in cui un programma A è in grado di invocare i metodi di un oggetto B in esecuzione su un computer remoto. In realtà
il panorama della progettazione e gestione di oggetti distribuiti offre valide
alternative, come ad esempio CORBA: la scelta può ricadere su RMI nel caso
in cui si voglia implementare, in maniera semplice e veloce, una struttura a
oggetti distribuiti full-Java (sia il lato client che quello server devono essere
realizzati obbligatoriamente utilizzando tale linguaggio).
1.2
La serializzazione e la trasmissione degli
oggetti
Il meccanismo base utilizzato da RMI per la trasmissione dei dati fra client e
server è quello della serializzazione: è quindi sicuramente utile soffermarsi su
questo importante sistema di trasmissione prima di affrontare nello specifico
la Remote Method Invocation. Grazie all’estrema semplicità con la quale
essa permette il flusso di dati complessi all’interno di uno stream, la serializzazione spesso viene utilizzata anche indipendentemente da applicazioni
RMI, e quindi quanto verrà qui detto resta di utilità generale. L’obiettivo
principale della serializzazione è permettere la trasformazione in modo semplice di oggetti e strutture di oggetti in sequenze di byte manipolabili con
i vari stream del package java.io. Ad esempio, grazie alla serializzazione è
possibile inviare strutture dati di complessità arbitraria tramite un socket
(utilizzando gli stream associati al socket stesso), oppure salvarli su file al
fine di mantenere la persistenza. La scrittura su stream avviene mediante il
metodo writeObject() appartenente alla classe ObjectOutputStream. Ad esempio, volendo salvare su file un’istanza di una ipotetica classe Record, si
potrebbe scrivere:
Record record = new Record();
FileOutputStream fos = new FileOutputStream("data.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(record);
dove si è salvato su file binario (data.ser ) un oggetto di tipo Record. L’operazione, in questo caso, è stata fatta in due fasi: creazione di uno stream di
serializzazione prima, e associazione di tale stream a un comune FileOutputStream. In modo altrettanto semplice si può effettuare l’operazione opposta
che permette la trasformazione da stream a oggetto:
FileInputStream fis = new FileInputStream("data.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
10
1.2 La serializzazione e la trasmissione degli oggetti
public class Record implements Serializable {
private String firstName;
private String lastName;
private int phone;
public Record (String firstName,String lastName,int phone)
{
this.firstName = firstName;
this.lastName = lastName;
this.phone = phone;
}
}
Listato 1.1: Uso dell’interfaccia Serializable
record = (Record)ois.readObject();
ois.close();
In questo caso si utilizza la classe ObjectInputStream e il metodo readObject(), il quale restituisce un oggetto di tipo Object, rendendo necessaria una
operazione di conversione (cast esplicito). In entrambi i casi le operazioni di
lettura e scrittura devono essere inserite in appositi blocchi try-catch al fine
di prevenire possibili problemi di lettura scrittura o di conversione. Per poter
serializzare un oggetto, un gruppo di oggetti, una struttura di complessità
arbitraria, si utilizza sempre la medesima procedura e non ci sono particolari differenze di cui tener conto, a patto che l’oggetto sia serializzabile: per
rispettare questo vincolo, un oggetto deve implementare l’interfaccia Serializable. L’interfaccia Serializable è vuota e non definisce alcun metodo; il
suo scopo è esclusivamente quello di dichiarare la possibilità per l’oggetto
di essere serializzato. Per questo, ad esempio, l’oggetto Record visto nell’esempio di cui sopra potrebbe essere definito come nel Listato 1.1. La regola
della serializzazione è ricorsiva, per cui un oggetto, per essere serializzabile,
deve contenere esclusivamente riferimenti a oggetti serializzabili. La maggior parte delle classi contenute all’interno del JDK è serializzabile, fatta
eccezione per alcuni casi particolari: non sono serializzabili tutte le classi che
inglobano al loro interno strutture dati binarie dipendenti dalla piattaforma,
come ad esempio molti degli oggetti dell’API JDBC. In questo caso infatti
i vari oggetti contengono al loro interno puntatori a strutture dati o codice
nativo utilizzato per la comunicazione con lo strato driver del database. Per
sapere se un dato oggetto sia serializzabile o meno si può utilizzare il tool
Serial Version Inspector (comando serialver), messo a disposizione dal JDK,
passando ad esso il nome completo della classe da analizzare. Ad esempio,
11
1.2 La serializzazione e la trasmissione degli oggetti
12
per verificare che la classe java.lang.String sia serializzabile si può scrivere
da linea di comando la seguente istruzione:
serialver java.lang.String
che restituisce il serialVersionUID dell’oggetto:
java.lang.String:
static final long serialVersionUID = -6849794470754667710L;
Invece tramite:
serialver -show
si manda in esecuzione la versione con interfaccia grafica di tale strumento
(Figura 1.2).
Figura 1.2: Versione grafica
Benché il suo utilizzo sia relativamente semplice, la serializzazione nasconde
alcuni aspetti importanti relativamente alla trasmissione degli oggetti. Per
quanto visto finora si potrebbe immaginare che la serializzazione permetta
la trasmissione di oggetti per mezzo di stream: in realtà questa concezione è
quanto mai errata, dato che a spostarsi sono solamente le informazioni che
caratterizzano un’istanza di un particolare oggetto. Ad esempio, durante la
trasmissione in un socket l’oggetto non viene mai spostato fisicamente, ma ne
viene inviata solo la sua rappresentazione e, successivamente, viene ricreata
una copia identica dall’altra parte del socket: al momento della creazione di
questa copia, il runtime creerà un oggetto nuovo, riempiendo i suoi dati con
quelli ricevuti dal client. Risulta ovvio quindi che, al fine di consentire questo
spostamento virtuale, su entrambi i lati, sia server che client, debba essere
presente il codice relativo all’oggetto: il runtime quindi deve poter disporre
dei file “.class” necessari per istanziare l’oggetto, o deve poterli reperire in
qualche modo. Il serialVersionUID della classe serve proprio per identificare
di quale tipo di oggetto siano i dati prelevati dallo stream. Si tenga presente
che nella trasmissione delle informazioni relative all’oggetto sono inviati solamente quei dati realmente significativi della particolare istanza. Per questo
1.3 Architettura RMI
non vengono inviati i metodi (che non cambiano mai), le costanti, le variabili
con specificatore static, che formalmente sono associate alla classe e non alla
istanza, e quelle identificate con la parola chiave transient. Tale keyword
qualifica una variabile non persistente, ovvero una variabile il cui valore non
verrà inviato nello stream durante la serializzazione. Il valore assunto da una
variabile di questo tipo dipende da come essa è stata definita. Ad esempio,
supponendo di scrivere:
transient Integer Int = new Integer(10);
al momento della deserializzazione, alla variabile Int verrà impostato il valore
10. Se invece si fosse scritto:
transient Integer Int;
durante la fase di deserializzazione, Int assumerebbe il proprio valore di default che per tutte le variabili di tipo reference è null, mentre per i tipi
primitivi corrisponde al valore base (0 per gli int, false per i boolean, 0.0 per
i float e cosı̀ via).
Riconsiderando l’esempio visto in precedenza, la classe Record viene serializzata e deserializzata su uno stream. In questo caso, il processo di trasformazione da oggetto a sequenza di byte è effettuato utilizzando le procedure
standard di conversione della JVM. Anche gli oggetti contenuti all’interno di
Record sono trasformati in modo ricorsivo, utilizzando le medesime tecniche.
1.3
Architettura RMI
In Figura 1.3 è riportata la struttura tipica di una applicazione RMI: è possibile notare come essa sia organizzata orizzontalmente in strati sovrapposti,
e in due moduli verticali paralleli fra loro. Questa suddivisione verticale vede
da una parte il lato client, e dall’altra il server: il primo è quello che contiene
l’applicazione che richiede il servizio di un oggetto remoto, che a sua volta
diviene il servente del servizio RMI.
Lo strato più alto del grafico è costituito su entrambi i lati (client e server)
da una applicazione eseguita sulla Java Virtual Machine in esecuzione su quel
lato: nel caso del client si tratta di una applicazione che effettua le chiamate
ai metodi di oggetti remoti, i quali poi sono eseguiti dall’applicazione remota.
Questa ha quindi un ciclo di vita indipendente dal client che di fatto ignora
la sua presenza.
Subito sotto il livello applicazione troviamo i due elementi fondamentali
dell’architettura RMI, ovvero lo stub e lo skeleton. Questi due oggetti
forniscono una duplice rappresentazione dell’oggetto remoto: lo stub rappresenta una simulazione locale sul client dell’oggetto remoto che però, grazie
13
1.3 Architettura RMI
allo skeleton, vive e viene eseguito sul lato server; i due elementi quindi non
sono utilizzabili separatamente. Da un punto di vista funzionale, il client,
Figura 1.3: Architettura RMI
dopo aver ottenuto un reference dell’oggetto remoto (lo stub di tale oggetto),
ne esegue i metodi messi a disposizione per l’invocazione remota, in modo
del tutto analogo al caso in cui l’oggetto sia locale. Si può quindi scrivere:
OggettoRemoto.nomeMetodo();
Da un punto di vista sintattico non vi è quindi nessuna differenza fra un
oggetto locale e uno remoto.
In conclusione il client (intendendo sia il programma che il programmatore
della applicazione lato client) non ha che in minima parte la percezione di
utilizzare un oggetto remoto.
Uno dei grossi vantaggi nell’utilizzo di RMI consiste nella semplicità con
cui si possono passare parametri durante l’invocazione dei metodi remoti e
riceverne indietro i risultati; senza nessuna differenza rispetto al caso locale
si può scrivere:
Ris = OggettoRemoto.nomeMetodo(param_1, ..., param_n);
Riconsiderando lo schema riportato nella figura 1.3, si ha che i vari parametri
vengono serializzati dalla virtual machine del client, inviati sotto forma di
stream al server, il quale li utilizzerà in forma deserializzata per utilizzarli
all’interno del corpo del metodo invocato. Anche il durante percorso inverso, ovvero quello che restituisce un risultato al client, viene effettuata una
serializzazione e quindi una deserializzazione dei dati.
14
1.3 Architettura RMI
15
Il procedimento, che da un punto di vista teorico risulta essere piuttosto
complesso, è molto semplice da utilizzare. L’unico vincolo di cui si deve
tener conto è che i parametri passati e il risultato ricevuto siano oggetti
serializzabili, cioè devono implementare l’interfaccia Serializable.
Il lato server e client sono collegati col sottostante Remote Reference Layer
(RRL) che a sua volta si appoggia al Transport Layer (TL). Il primo dei due
ha il compito di instaurare un collegamento logico fra i due lati, di codificare
le richieste del client, inviarle al server, decodificare le richieste e inoltrarle
allo skeleton. Ovviamente nel caso in cui quest’ultimo fornisca dei risultati
per il particolare tipo di servizio richiesto, il meccanismo di restituzione di
tali valori avviene in maniera del tutto simile ma in senso opposto.
Al livello RRL viene instaurato un collegamento virtuale fra i due lati
client e server, mentre fisicamente la connessione avviene al livello sottostante,
il Transport Layer. Tale collegamento è di tipo sequenziale ed è per questo
che si richiede la serializzazione dei parametri da passare ai metodi. Il collegamento virtuale dello strato RRL si basa su un protocollo di comunicazione
generico e indipendente dal particolare tipo di stub o skeleton utilizzati:
questa genericità permette di mantenere la massima indipendenza dal livello
stub/skeleton, tanto che è possibile sostituire il RRL con versioni successive
più ottimizzate. Il protocollo di conversione delle invocazioni dei metodi,
l’impacchettamento dei riferimenti ai vari oggetti, e tutto quello che concerne
la gestione a basso livello sono operazioni a carico sia dello strato RRL, sia
e soprattutto dal TL, in cui si perde la concezione di oggetto remoto e/o
locale e i dati vengono semplicemente visti come sequenze di byte da inviare
o leggere verso certi indirizzi di memoria. Quando il TL riceve una richiesta
di connessione da parte del client, localizza il server RMI relativo all’oggetto
remoto richiesto: successivamente viene eseguita una connessione per mezzo
di un socket appositamente creato per il servizio.
Si tenga presente che, quando un client vuole chiamare un metodo, serializza i parametri e li invia sul canale. Il server deserializza i parametri
ricevuti con l’invocazione e ricostruisce una copia locale dell’oggetto. Proprio
per questo meccanismo, l’oggetto lato client e la sua copialato server sono indipendenti. Quindi una modifica sullo stato interno (ad esempio, le variabili
di istanza) di uno dei due non si ripercuote sull’altro. In questo contesto,
particolarmente importante è la presenza di un garbage collector apposito
per RMI, il quale provvede in modo automatico e trasparente a eseguire le
opportune operazioni di ripulitura delle aree di memoria non più utilizzate.
Per ogni oggetto RMI remoto, il reference layer del server mantiene la lista
dei riferimenti remoti che sono registrati dai client. Ogni riferimento remoto
di ogni client è stato ottenuto esplicitamente attraverso una “lookup” o implicitamente come risultato dell’invocazione di un metodo remoto. Il client
1.4 Uso di RMI
mantiene questi riferimenti nel suo Reference Layer.
Quando la Java Virtual Machine del client si accorge che un oggetto
remoto non è più referenziato localmente, questa notifica in modo asincrono
al server RMI l’evento “unreferenced” cosicchè il server può aggiornare la
lista dei riferimenti. Quando un oggetto RMI non ha più riferimenti remoti
nella lista, la possibilità di tale oggetto di essere “garbage collected” è basata
solamente sull’esistenza di riferimenti locali, come ogni altro oggetto java.
Il distributed garbage collector associa ad ogni riferimento remoto un
“lease”, cioè una sorta di timeout che può scadere. Quando il “lease” scade,
il riferimento è eliminato dalla lista e viene inviato una richiesta di “lease
renew” al reference layer del client che utilizzava tale riferimento. In questo
modo al client viene notificato che il riferimento remoto è scaduto. A questo
punto il client notifica al server RMI di mantenere ancora il riferimento
remoto nella lista perchè valido.
L’obiettivo del meccanismo di “lease renewal” è di permettere al server
di accorgersi della terminazione anormale dei client e quindi di eliminare i
riferimenti remoti non utili. Infatti, se non si facesse uso dei “lease”, un
client andato in “crash” non manderebbe mai il messaggio appropriato di
“unreferenced” prima di terminare l’esecuzione. In questo contesto, un client
che invoca una System.exit() è considerato terminato in maniere anormale
perchè tale operazione non permette al reference layer del client di mandare
l’appropriato messaggio “unreferenced” al server.
L’ultimo livello che però non viene incluso nella struttura RMI, è quello
che riguarda la gestione della connessione al livello di socket e protocolli
TCP/IP. Questo aspetto segue le specifiche standard di networking di Java
e non offre particolari interessanti in ottica RMI.
1.4
Uso di RMI
Si può ora procedere ad analizzare quali siano i passi necessari per realizzare
una applicazione RMI. Tutte le classi e i metodi che si analizzeranno e, in
generale, tutte le API necessarie per lavorare con RMI sono contenute nei
package java.rmi e java.rmi.server.
Anche se, dal punto di vista della programmazione a oggetti, sarebbe più
corretto parlare di classi, in questo caso si parlerà genericamente di oggetti
remoti e locali intendendo sia il tipo che la variabile. A tal proposito, in
base alla definizione ufficiale, si definisce remoto un oggetto che implementi
l’interfaccia Remote e i cui metodi possano essere eseguiti da una applicazione
client non residente sulla stessa macchina virtuale.
Un’interfaccia remota invece rende disponibile il set di metodi utilizzabili
16
1.4 Uso di RMI
per l’invocazione a distanza; ovviamente non è necessario definire nell’interfaccia quei metodi a solo uso interno della classe. Si immagini quindi di
definire MyServer un oggetto per il momento non remoto:
public class MyServer {
public void String concat(String a, String b) {
return a + b;
}
}
Il metodo concat() in questo caso esegue una concatenazione fra i due argomenti passati in input restituendo in uscita la stringa risultante.
A parte il vincolo della serializzabilità dei parametri, non ci sono limiti
alla complessità delle operazioni eseguibili all’interno di metodi remoti.
Dopo aver definito questa semplice classe, per trasformarla nella versione
remota si deve per prima cosa definire la sua interfaccia remota:
public interface MyServerInterface
extends Remote {
public String concat(String a, String b)
throws RemoteException;
}
Come si può osservare da queste poche righe di codice, per definire un’interfaccia remota è necessario estendere la java.rmi.Remote: questa è una
interfaccia vuota e serve solo per verificare durante l’esecuzione che le operazioni di invocazione remota siano plausibili. Un oggetto utilizzato come
parametro di input o di ritorno può implementare l’interfaccia Remote invece
che quella Serializable. La differenza principale tra oggetti remoti e serializzabili è che gli oggetti remoti sono inviati come riferimento, mentre gli
oggetti non remoti, serializzabili sono inviati come copia. Se l’oggetto è serializzabile ma non remoto esso viene serializzato e inviato al processo remoto
come flusso di byte.
È obbligatoria la gestione dell’eccezione java.rmi.RemoteException: infatti, a causa della distribuzione in rete, oltre alla gestione di eventuali problemi
derivanti dalla normale esecuzione del codice (bug o incongruenze di vario
tipo), si deve adesso proteggere tutta l’applicazione da anomalie derivanti
dall’utilizzo di risorse remote: ad esempio potrebbe venire a mancare improvvisamente la connessione fisica verso l’host dove è in esecuzione il server
RMI.
Definita l’interfaccia remota si deve modificare leggermente la classe di
partenza, in modo che implementi questa interfaccia, come mostrato nel Listato 1.2. Il nome della classe è stato cambiato a indicare l’implementazione
17
1.4 Uso di RMI
public class MyServerImpl implements MyServerInterface
extends UnicastRemoteObject {
public MyServerImpl() throws RemoteException {
...
}
public String concat(String a, String b)
throws RemoteException{
return a + b;
}
}
Listato 1.2: Uso dell’interfaccia MyServerInterface che estende Remote
dell’interfaccia remota; come si può notare, oltre a dichiarare di implementare
l’interfaccia precedentemente definita, si deve anche estendere la classe UnicastRemoteObject. Oltre a ciò, all’oggetto è stato aggiunto un costruttore di
default il quale dichiara di propagare eccezioni RemoteException: tale passaggio non ha una motivazione apparente, ma è necessario per permettere
al compilatore di creare correttamente tutte le parti che compongono il lato
server (stub e skeleton).
La classe UnicastRemoteObject deriva dalle due classi, RemoteServer e RemoteObject: la prima è una superclasse comune per tutte le implementazioni
di oggetti remoti (la parola Unicast ha importanti conseguenze come si avrà
modo di vedere in seguito), mentre l’altra semplicemente ridefinisce hashcode() ed equals() in modo da permettere correttamente il confronto tra
oggetti remoti.
L’uso della classe RemoteServer permette di utilizzare implementazioni
di oggetti remoti diverse da UnicastRemoteObject, anche se per il momento
quest’ultima è l’unica supportata.
L’organizzazione delle più importanti classi per RMI è raffigurata in Figura 1.4. Dopo questa trasformazione, l’oggetto è visibile dall’esterno, ma ancora non utilizzabile secondo la logica RMI: si devono infatti creare i cosiddetti
stub e skeleton. Tali oggetti sono ottenibili in maniera molto semplice per
mezzo del compilatore rmic, disponibile all’interno del JDK 1.1 e successivi:
partendo dal bytecode ottenuto dopo la compilazione dell’oggetto remoto,
questo tool produce stub e skeleton. Ad esempio, riconsiderando il caso della
18
1.4 Uso di RMI
Figura 1.4: Struttura gerarchica delle principali classi e interfacce RMI
classe MyServerImpl, con una operazione del tipo:
rmic MyServerImpl
si ottengono i due file MyServerImpl stub.class e MyServerImpl skel.class. A
questo punto si hanno a disposizione tutti i componenti per utilizzare l’oggetto remoto MyServerImpl: resta quindi da rendere possibile il collegamento tra
client e server per l’invocazione remota.
Si definisce server RMI l’applicazione che istanzia un oggetto remoto e
lo registra tramite una bind all’interno dell’RMI registry. Il server RMI
non è quindi l’oggetto che implementa la business logic, ma solamente una
applicazione di servizio necessaria per attivare il meccanismo di invocazione
remota.
Sul lato server, l’applicazione che gestisce lo skeleton deve notificare di
possedere al suo interno un oggetto abilitato all’invocazione remota. Per far
questo è necessario utilizzare il metodo statico java.rmi.Naming.bind() che
associa all’istanza dell’oggetto remoto un nome logico con cui tale oggetto
può essere identificato in rete.
Quindi, dopo aver creato una istanza dell’oggetto remoto tramite:
MyServerImpl server = new MyServerImpl();
si provvede a effettuarne la registrazione utilizzando un nome simbolico:
19
1.4 Uso di RMI
import java.rmi.*;
import java.rmi.server.*;
public class Registration
{
public static void main(String args[])
{
try
{
MyServerImpl server =new MyServerImpl();
Naming.bind("Paperino", server);
}
catch (Exception e)
{
System.out.println("Implerr:Ã" + e.getMessage());
e.printStackTrace();
}
}
}
Listato 1.3: Registrazione dell’oggetto remoto
Naming.bind("Paperino", server);
Questa operazione, detta registrazione, può fallire e in tal caso viene generata una eccezione in funzione del tipo di errore. In particolare si otterrà
una AlreadyBoundException nel caso in cui il nome logico sia già stato utilizzato per un’altra associazione, una MalformedURLException per errori nella
sintassi dell’URL, mentre il runtime produrrà RemoteException per tutti gli
altri tipi di errore legati alla gestione da remoto dell’oggetto.
Ogni associazione nome logico - oggetto remoto è memorizzata in un apposito registro detto RMI registry. In questo caso rmiregistry è anche il
comando che lancia l’applicazione per gestire tale archivio, applicazione che
deve essere lanciata sul lato server prima di ogni bind.
Una volta che l’RMI registry è attivo, è possibile registrare l’oggetto.
Il codice completo per registrare l’oggetto è mostrato nel Listato 1.3. Il
client a questo punto è in grado di ottenere un reference all’oggetto con una
ricerca presso l’host remoto utilizzando il nome logico con cui l’oggetto è
stato registrato. Ad esempio si potrebbe scrivere:
MyServerInterface server;
String url = "//" + serverhost + "/MyServer";
server = (MyServerInterface) Naming.lookup(url);
20
1.4 Uso di RMI
21
e quindi utilizzare il reference per effettuare le invocazioni ai metodi remoti:
System.out.println(server.concat("HelloÃ", "world!"));
Sul client per ottenere il reference si utilizza il metodo statico Naming.lookup(),
che può essere considerato il corrispettivo alla operazione di bind sul server.
L’URL passato come parametro al lookup() identifica il nome della macchina che ospita l’oggetto remoto e il nome con cui l’oggetto è stato registrato.
Entrambe le operazioni di registrazione e di ricerca accettano come parametro
un URL: il formato di tale stringa è la seguente:
rmi://host:port/name
dove host è il nome del server RMI, port la porta dove si mette in ascolto
il registry, name il nome logico. Sul server non è necessario specificare
l’host, dato che per default assume l’indirizzo della macchina stessa sulla
quale l’applicazione server RMI è mandata in esecuzione. In entrambi i casi
il numero della porta di default è la 1099 ma, se si specifica altrimenti, allora
tale informazione dovrà essere passata al rmiregistry con il seguente comando:
rmiregistry numero_porta
Ogni volta che un parametro viene passato ad un metodo remoto, o viceversa, ogni volta che si preleva un oggetto come risultato di una computazione
remota, si dà vita a un processo di serializzazione o deserializzazione dell’oggetto in questione. In realtà, come si è potuto vedere, l’oggetto serializzato non viene spostato dal client al server, ma vengono inviate nella rete
solamente le informazioni necessarie per ricreare una copia dell’oggetto dal
client al server (e viceversa). Questo significa che sia il client che il server
devono poter disporre dello stesso bytecode relativo all’oggetto serializzato
in modo da poterne ricreare l’istanza. La soluzione più semplice è copiare
fisicamente i vari file “.class” sia sul server che sul client: in questo caso si
potrà essere sicuri che le varie operazioni di serializzazione e deserializzazione
potranno essere effettuate correttamente. Lo svantaggio di questa organizzazione risiede nel dovere ridistribuire tutti i file per ogni modifica delle
varie classi. In alcuni casi questa soluzione è scomoda, se non addirittura
impraticabile.
RMI mette a disposizione un meccanismo molto potente che consente
di scaricare dalla rete, tramite un file server HTTP, i file necessari per il
funzionamento del client. Per ulteriori dettagli sul loading remoto delle classi
si faccia riferimento a [1].
Se si ripensa per un momento alla modalità di pubblicazione di un oggetto remoto da parte del server RMI, si potrà osservare come la funzione di
creazione e registrazione sia un compito totalmente a carico del server. Per
1.4 Uso di RMI
22
public interface Messaggio extends Remote
{
String getTesto() throws RemoteException;
Date getData() throws RemoteException;
String getMittente() throws RemoteException;
}
Listato 1.4: Interfaccia Messaggio
una precisa scelta progettuale quindi, visto che la registrazione dell’oggetto
avviene una volta sola, l’istanza dell’oggetto remoto sarà l’unica disponibile
e quindi condivisa fra tutti i client possibili. Pensata per semplificare al massimo il lavoro del programmatore, in molti casi però questa soluzione risulta
essere troppo rigida e non sufficiente per supportare architetture distribuite
complesse. Una soluzione semplicistica potrebbe essere quella di istanziare
un numero prefissato di oggetti per poter servire più client; ovviamente tale
soluzione, oltre ad essere poco flessibile e per niente elegante, non risolve il
problema della creazione “on demand” di oggetti remoti da parte del client.
Questo tipo di problema, che ricade nella sfera del pooling degli oggetti, può
essere risolto in RMI tramite due tecniche: una basata sull’utilizzo di particolari pattern progettuali (soluzione quindi non strettamente legata a RMI, ma
di valenza generale) e una basata sull’utilizzo di una particolare interfaccia
remota messa appositamente a disposizione per risolvere questo problema.
Il framework EJB che, per certi versi, può essere considerato come
l’evoluzione di RMI nasce per risolvere una serie di problemi presenti nei
modelli di programmazione distribuita come RMI, ma anche CORBA. Infatti in EJB il problema del pooling degli oggetti viene risolto in modo molto
potente ed elegante, demandando al container la gestione del numero degli
oggetti remoti in esecuzione in base alle esigenze di sistema e alle richieste
dei vari client.
Il problema del pooling di oggetti può essere risolto utilizzando una tecnica introdotta in RMI ad hoc, basata su Java Activation, ma risulta essere alquanto complessa(vedi [1]). Invece, utilizzando il pattern Factory
Method, si può ottenere lo stesso risultato in modo molto più semplice ed
elegante. si supponga di avere una classe remota che implementi l’interfaccia
mostrata nel Listato 1.4. L’implementazione di tale classe è mostrata nel
Listato 1.5
In uno scenario reale si potrebbe ipotizzare che ogni client debba o voglia
poter produrre messaggi propri, indipendentemente dagli altri, e che tali
messaggi siano gestiti da un server centrale. Si noti come le proprietà siano
1.4 Uso di RMI
23
public class MessaggioImpl extends UnicastRemoteObject
implements Messaggio{
String testo;
Date data;
String mittente;
public MessaggioImpl(String unMittente,String unTesto)
throws RemoteException{
super();
data=new Date();
testo=unTesto;
mittente=unMittente;
}
public String getTesto() throws RemoteException
{
return(testo);
}
public Date getData() throws RemoteException
{
return(data);
}
public String getMittente() throws RemoteException
{
return(mittente);
}
}
Listato 1.5: Classe MessaggioImpl
1.4 Uso di RMI
24
import java.rmi.*;
import java.util.*;
public interface Factory extends Remote
{
Messaggio creaMessaggio(String sndr, String text)
throws RemoteException;
}
public class FactoryImpl extends UnicastRemoteObject
implements Factory
{
public FactoryImpl() throws RemoteException
{
super();
}
public Messaggio creaMessaggio(String sndr, String text)
throws RemoteException
{
MessaggioImpl unMessaggio=new MessaggioImpl(sndr,text);
return (unMessaggio);
}
}
Listato 1.6: Interfaccia Factory
immutabili e impostate nel costruttore; si noti anche come, addirittura, la
proprietà Data sia in realtà impostata automaticamente alla data attuale del
Sistema.
Per fare questo, si deve predisporre un meccanismo che permetta la
creazione di tali oggetti anche da parte di client RMI. La classe MessaggioFactory che implementa il pattern Factory, è a tutti gli effetti un oggetto
remoto come quelli visti in precedenza: tramite la sua registrazione, ogni
client potrà ottenerne lo stub e invocarne il metodo remoto getLogger(), la
cui implementazione è riportata di seguito, nel Listato 1.6: Il client che vuole
creare una nuova instanza della classe MessaggioImpl (che implementa l’interfaccia Messaggio) deve semplicemente chiamare il metodo creaMessaggio
1.5 Esempio: Sender e Receiver
public class Application {
public static void main(String args[])
{
String sndr= ...
String text= ...
Factory fact= (Factory)Naming.lookup(...);
Messaggio mex = fact.creaMessaggio(sndr,text);
...
}
}
Listato 1.7: Classe Application
(vedi Listato 1.7). La chiamata al metodo Factory creaMessaggio in realtà
non fa nient’altro che creare un nuovo oggetto Messaggio e restituirne un
riferimento remoto al client richiedente. Tale messaggio avrà per le proprietà Mittente e Testo i valori passati al metodo Factory e la proprietà Data
impostato con la data del server (e non quella del client).
Il client invocando il metodo remoto creaMessaggio() riceverà lo stub dell’oggetto remoto, il quale verrà eseguito sul server remoto, in perfetto accordo
con il modello teorico di RMI.
L’utilizzo del pattern Factory risolve in maniera piuttosto agile alcune
delle limitazioni imposte dal modello RMI: con tale tecnica infatti è possibile
attivare oggetti al momento della effettiva necessità su richiesta del client ed
in modo esclusivo.
1.5
Esempio: Sender e Receiver
Vediamo ora un esempio pratico di quanto detto finora. In questo esempio si vuole gestire una applicazione client/server per fornire un servizio di
pubblicazione/lettura di messaggi di testo. L’interfaccia del servizio può essere definita con l’interfaccia Archivio che estende a sua volta l’interfaccia
Java Remote(Listato 1.8). Tale interfaccia definisce l’interfaccia del servizio,
espressa in linguaggio Java e quindi corrisponde all’IDL del servizio. In
Archivio vengono dichiarati due metodi, send e receive, che devono essere
definiti nella classe ArchivioImpl(implementazione del servizio), come mostrato nel Listato 1.8. Questa classe estende UnicastRemoteObject e implementa
Archivio. Per gestire la lista dei messaggi, viene utilizzato un oggetto (coda)
di tipo ArrayList. Il costruttore deve per prima cosa chiamare la superclasse.
I due metodi, send e receive, utilizzano la variabile x come semaforo (syn-
25
1.5 Esempio: Sender e Receiver
import java.rmi.*;
public interface Archivio extends Remote
{
void send(String mex) throws RemoteException;
String receive() throws RemoteException;
}
Listato 1.8: Definizione dell’interfaccia Archivio
chronized ), consentono di accedere alla lista (sia in lettura che in scrittura)
in modo consistente, evitando situazioni ambigue. Qualsiasi situazione di
accesso concorrente (due letture, due scritture, una lettura e una scrittura)
potrebbero generare situazioni non desiderate, come la perdita di messaggi.
Grazie all’istruzione synchronized(x), se un client sta pubblicando o leggendo
un messaggio, un altro client che vuole a sua volta leggere o pubblicarne uno
deve attendere che il primo client abbia terminato l’operazione per poterne
cominciare una.
Il client si implementa con due classi, la classe Sender e la classe Receiver.
Queste sono mostrate rispettivamente nel Listato 1.11 e nel Listato 1.12.
Nelle classi Sender e Receiver viene passato come parametro la stringa
args[0] che è il nome oppure l’indirizzo IP del server che fornisce il servizio.
La stringa args[1] utilizzata nella classe Sender è invece il messaggio che si
vuole inserire in coda.
RMI non fornisce alcun meccanismo di serializzazione delle invocazioni dei
metodi remoti. La serializzazione deve essere realizzata dal programmatore
mediante l’uso di codice opportuno (ad esempio, i blocchi synchronized ).
Questa limitazione è stata superata dalla naturale evoluzione di RMI, cioè
gli EJB.
26
1.5 Esempio: Sender e Receiver
import java.rmi.server.*;
public class ArchivioImpl extends UnicastRemoteObject
implements Archivio
{
ArrayList coda;
String x="";
public ArchivioImpl() throws RemoteException
{
super();
coda=new ArrayList();
}
public void send(String messaggio) throws RemoteException
{
synchronized(x)
{
coda.add(messaggio);
}
}
public String receive() throws RemoteException
{
if (coda.isEmpty())
return(null);
synchronized(x)
{
try
{
String messaggio=(String)coda.get(0);
coda.remove(0);
return(messaggio);
}
catch(NoSuchElementException err)
{
return(null);
}
}
}
}
Listato 1.9: Implementazione del servizio definito dall’Interfaccia Archivio
27
1.5 Esempio: Sender e Receiver
import java.rmi.*;
import java.rmi.server.*;
public class ArchivioServer
{
public static void main(String args[])
{
try
{
ArchivioImpl obj=new ArchivioImpl();
Naming.rebind("Coda", obj);
}
catch (Exception e)
{
System.out.println("Implerr:Ã" + e.getMessage());
e.printStackTrace();
}
}
}
Listato 1.10: Codice per la registrazione dell’oggetto nell’RMI Registry
import java.rmi.*;
public class Sender
{
public static void main(String args[]) throws Exception
{
if (args.length==0)
{
System.out.println("NessunÃmessaggioÃdaÃinviare");
return;
}
Archivio obj=
(Archivio)Naming.lookup("//"+args[0]+"/Coda");
obj.send(args[1]);
}
}
Listato 1.11: Classe Sender
28
1.5 Esempio: Sender e Receiver
import java.rmi.*;
public class Receiver
{
public static void main(String args[]) throws Exception
{
Archivio obj=
(Archivio)Naming.lookup("//"+args[0]+"/Coda");
String messaggio=obj.receive();
if (messaggio==null)
System.out.println("NessunÃmessaggioÃinÃcoda!");
else
System.out.println(messaggio);
}
}
Listato 1.12: Classe Receiver
29
Capitolo 2
Swing
Uno dei problemi più grossi emersi durante la progettazione di Java fu senza dubbio la realizzazione di un toolkit grafico capace di funzionare con
prestazioni di buon livello su piattaforme molto differenti tra loro. La soluzione
adottata nel 1996 fu AWT(Abstract Window Toolkit), un package grafico
che mappa i componenti del sistema ospite con apposite classi dette peer,
scritte in gran parte in codice nativo. In pratica, ogni volta che il programmatore crea un componente AWT e lo inserisce in un’interfaccia grafica, il
sistema AWT posiziona sullo schermo un oggetto grafico della piattaforma ospite, e si occupa di inoltrare ad esso tutte le chiamate a metodo effettuate sull’oggetto Java corrispondente, ricorrendo a procedure scritte in buona parte
in codice nativo; nel contempo, ogni volta che l’utente manipola un elemento
dell’interfaccia grafica, un’apposita routine (scritta sempre in codice nativo)
crea un apposito oggetto Event e lo inoltra al corrispondente oggetto Java,
in modo da permettere al programmatore di gestire il dialogo con il componente e le azioni dell’utente con una sintassi completamente Object Oriented
e indipendente dal sistema sottostante.
A causa di questa scelta progettuale, il set di componenti grafici AWT
comprende solamente quel limitato insieme di controlli grafici che costituiscono il minimo comune denominatore tra tutti i sistemi a finestre esistenti:
un grosso limite rispetto alle reali esigenze dei programmatori. In secondo luogo, questa architettura presenta un grave inconveniente: i programmi grafici
AWT assumono un aspetto ed un comportamento differente a seconda della
JVM su cui vengono eseguite, a causa delle macroscopiche differenze implementative esistenti tra le versioni di uno stesso componente presenti nelle
diverse piattaforme. Spesso le interfacce grafiche realizzate su una particolare piattaforma mostrano grossi difetti se eseguite su un sistema differente,
arrivando in casi estremi a risultare inutilizzabili.
Il motto della Sun per Java era “scrivi (il codice) una volta sola ed eseguilo
30
2.1 Il package Swing
31
Component
Container
JComponent
JPanel
Window
JApplet
JFrame
JDialog
java.awt
javax.swing
Figura 2.1: Diagramma UML di base del package Swing.
ovunque”; nel caso di AWT questo si era trasformato in “scrivi una volta sola
e correggilo ovunque”.
2.1
Il package Swing
Nel 1998, con l’uscita del JDK 1.2, venne introdotto il package Swing, i
cui componenti erano stati realizzati completamente in Java, ricorrendo unicamente alle primitive di disegno più semplici, tipo “traccia una linea” o
“disegna un cerchio”, accessibili attraverso i metodi dell’oggetto Graphics,
un oggetto AWT utilizzato dai componenti Swing per interfacciarsi con la
piattaforma ospite. Le primitive di disegno sono le stesse su tutti i sistemi
grafici, e il loro utilizzo non presenta sorprese: il codice java che disegna un
pulsante Swing sullo schermo di un PC produrrà lo stesso identico risultato
su un Mac o su un sistema Linux. Questa architettura risolve alla radice i
problemi di uniformità visuale, visto che la stessa identica libreria viene ora
utilizzata, senza alcuna modifca, su qualunque JVM. Liberi dal vincolo del
“minimo comune denominatore”, i progettisti di Swing hanno scelto di percorrere la via opposta, creando un package ricco di componenti e funzionalità
spesso non presenti nella piattaforma ospite. Il procedimento di disegno è
ovviamente più lento perché la JVM deve disegnare per proprio conto tutte
le linee degli oggetti grafici e gestirne direttamente il comportamento, però
è più coerente.
Le classi Swing sono definite nel pacchetto javax.swing, il cui nome javax
2.2 Top Level Container
indica estensione standard a Java. Inizialmente Swing venne rilasciato, infatti, come estensione per poi divenire un omponente standad di Java 2. Per
motivi di compatibilita il nome del pacchetto javax non venne corretto in
java. Gli oggetti grafici Swing derivano dai corrispettivi AWT; quindi è possibile utilizzare oggetti Swing, ove erano previsti i oggetti antenati. Swing
usa ancora alcuni elementi AWT per disegnare; anche la gestione degli eventi
è fatta per la maggior parte da classi AWT.
La Figura 2.1 riassume molto sinteticamente le classi base del package
Swing e come queste derivino da classi AWT. Ogni oggetto grafico (una
finestra, un bottone, un campo di testo, . . . ) è implementato come classe
del package javax.swing. Ogni classe che identifica un oggetto Swing deriva
per lo più dalla classe javax.swing.JComponent; si stima che esistono circa 70
o più oggetti diversi. Gli oggetti grafici utilizzati per disegnare le interfacce
vengono chiamati anche controlli oppure tecnicamente widget. JComponent
eredita da java.awt.Container, una sorta di controllo che di default è vuoto
e il cui scopo è offrire la possibilità di disporre altri componenti all’interno.
Non a caso la classe AWT Window e le sottoclassi Swing JFrame e JDialog, le
cui istanze rappresentano finestre, sono sottoclasse di Container. La cosa più
sorprendente è che, siccome JComponent deriva da Container, è possibile inserire all’interno di un qualsiasi widget qualsiasi altro. Ad esempio - sebbene
poco utile - è possibile aggiungere un campo di testo all’interno di un bottone
oppure - molto usato - un Container all’interno di un altro Container. La
classe Container (e ogni sottoclasse) definisce un metodo per aggiungere un
controllo ad un Container:
void add(Component);
Il metodo prende come parametro un oggetto Component che è la superclasse
di qualsiasi oggetto o container Swing o AWT.
2.2
Top Level Container
I top level container sono i componenti all’interno dei quali si creano le
interfacce grafiche: ogni programma grafico ne possiede almeno uno, di solito
un JFrame, che rappresenta la finestra principale. Ogni top level container
possiede un pannello (accessibile tramite il metodo getContentPane()) all’interno del quale vanno disposti i controlli dell’interfaccia grafca. Esistono
tre tipi principali di top level Container: JFrame, JApplet e JDialog. Il
primo viene solitamente usato come finestra principale per il programma, il
secondo è utilizzato per costruire Applet da visualizzare nella finestra di un
web browser mentre il terzo serve a creare le finestre di dialogo con l’utente.
32
2.2 Top Level Container
2.2.1
33
Uso di JFrame
Un oggetto della classe JFrame può essere creato usando i costruttori:
JFrame();
JFrame(String titoloFinestra);
Il primo costruisce un JFrame senza titolo; il secondo permette di specificarlo.
È sempre possibile impostare il titolo ricorrendo al metodo
setTitle(String s). Due importanti proprietà dell’oggetto sono la dimensione e la posizione, che possono essere impostate sia specificando le singole
componenti sia mediante oggetti Dimension e Point del package AWT:
public
public
public
public
void
void
void
void
setSize(Dimension d);
setSize(int width,int height);
setLocation(Point p);
setLocation(int x,int y);
Ricorrendo al metodo setResizable(boolean b) è possibile stabilire se
si vuole permettere all’utente di ridimensionare la finestra manualmente.
Infine, vi sono tre metodi piuttosto importanti:
public void pack();
public void setVisible(boolean b);
public void setDefaultCloseOperation(int operation);
Il primo ridimensiona la finestra tenendo conto delle dimensioni ottimali di
ciascuno dei componenti presenti all’interno. Il secondo permette di visualizzare o di nascondere la finestra. Il terzo imposta l’azione da eseguire alla pressione del bottone close, con quattro impostazioni disponibili:
JFrame.DO NOTHING ON CLOSE (nessun effetto), JFrame.HIDE ON CLOSE
(nasconde la finestra), JFrame.DISPOSE ON CLOSE (chiude la finestra e libera
le risorse di sistema) e JFrame.EXIT ON CLOSE (chiude la finestra e conclude
l’esecuzione del programma).
Per impostazione di default, un JFrame viene costruito non visibile e di
dimensione 0 x 0. Per questa ragione, affinchè la finestra sia visibile, è necessario chiamare i metodi setSize() o pack() per specificare la dimensione e
mettere a true la proprietà Visible chiamando il metodo: setVisible(true).
Per poter lavorare con i Frame Swing, è opportuno conoscere il linea
generale la struttura della superficie. La superficie di un frame Swing è
coperta da quattro lastre:
Glass Pane La lastra di vetro è nascosta di default ed ha il compito di catturare gli eventi di input sulla finestra. Normalmente è completamente
trasparente a meno che venga implementato il metodo paintComponent
2.3 Paranoramica di alcuni widget
Figura 2.2: Anatomia di un Frame Swing.
del GlassPane. Poichè è davanti a tutte le altre, qualsiasi oggetto
disegnato su questa lastra nasconde qualsiasi altro disegnato sulle altre
Content Pane La lastra dei contenuti è la più importante perchè è quella che ospita i componenti che volete visualizzare nella finestra e la
maggior parte dei programmatori Java lavora solo su questa
Layered Pane Contiene la lastra dei contenuti ed eventualmente i menu. I
menu, infatti, non vengono mai aggiunti al Content Pane ma a questa
lastra.
Root Pane La lastra radice ospita la lastra di vetro insieme con la lastra
dei contenuti e i bordi della finestra.
Per maggiori dettagli si faccia riferimento a [4].
Quindi, un componente, quale un pulsante, un’immagine o altro, non
viene aggiunto direttamente alla finestra ma alla lastra dei contenuti. Di
conseguenza, occorre per prima cosa procurarsi un riferimento all’oggetto
ContentPane, mediante la chiamata al metodo:
public Container getContentPane();
Come era da aspettarsi, il ContentPane “è un” Container perchè predisposto
a contenere altri componenti. A questo punto, come per ogni altro Container,
è possibile aggiungere ad esso un componente con il metodo add già descritto.
A questo punto è possibile disegnare la prima finestra. Il codice descritto
nel Listato 2.1 mostra la finestra in Figura 2.3.
2.3
Paranoramica di alcuni widget
I nomi delle classi per la maggior parte dei componenti dell’interfaccia utente
Swing iniziano con la J.
34
2.3 Paranoramica di alcuni widget
import javax.swing.*;
import java.awt.*;
public class Application {
public static void main(String args[])
{
JFrame win;
win = new JFrame("PrimaÃfinestra");
Container c = win.getContentPane();
c.add(new JLabel("BuonaÃLezione"));
win.setSize(200,200);
win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
win.setVisible(true);
}
}
Listato 2.1: La prima finestra
Figura 2.3: La prima finestra.
35
2.3 Paranoramica di alcuni widget
36
JTextField è un campo di testo Swing. Tale classe eredita da TextField,
l’obsoleto analogo del package AWT. Per costruire un campo di testo occorre
fornirne l’ampiezza, cioè il numero approssimato di caratteri che vi aspettate
verranno inseriti dall’utente.
JTextField xField=new JTextField(5);
Gli utenti possono digitare anche un numero maggiore di caratteri ma sempre
5 contemporaneamente saranno vicini: i 5 attorno alla posizione del cursore
nel campo.
Sarebbe opportuno etichettare ciascun campo di testo in modo che l’utente sappia cosa scriverci. Ogni etichetta è un oggetto di tipo JLabel
che viene costruito, sfruttando il costruttore con un parametro stringa; tale
parametro rappresenta il testo dell’etichetta:
JLabel xField=new JLabel("xÃ=Ã");
Figura 2.4: Alcuni Bottoni delle Swing.
Inoltre vorrete dare all’utente la possibilità di inserire informazione in
tutti i campi di testo prima di elaborarli per cui avete bisogno di un pulsante che l’utente possa premere per segnalare che i dati sono pronti per
essere elaborati. Un pulsante è un oggetto JButton che può essere costruito
fornendo una stringa che fungerò da etichetta, un’immagine come icona o
entrambe
JButton moveButton=new JButton("Move");
JButton moveButton=new JButton(new ImageIcon("hand.gif"));
JButton moveButton=new JButton("Move",new ImageIcon("hand.gif"));
JCheckBox è una sottoclasse di JButton che crea caselle di controllo,
con un aspetto simile a quello delle caselle di spunta dei questionari. Il
suo funzionamento è analogo a quello della superclasse, ma di fatto tende a
2.3 Paranoramica di alcuni widget
essere utilizzato in contesti in cui si offre all’utente la possibilità di scegliere
una o più opzioni tra un insieme, come avviene per esempio nei pannelli di
controllo. I costruttori disponibili sono gli stessi della superclasse, quindi non
sarà necessario ripetere quanto è stato già detto.
JCheckBox check1=new JCheckBox("JCheck");
JRadioButton è una sottoclasse di JButton, dotata dei medesimi costruttori. Questo tipo di controllo, chiamato pulsante di opzione, viene usato
tipicamente per fornire all’utente la possibilità di operare una scelta tra
un insieme di possibilità, in contesti nei quali un’opzione esclude l’altra. I
costruttori disponibili sono gli stessi della superclasse. Per implementare il
comportamento di mutua esclusione, è necessario registrare i JRadioButton
che costituiscono l’insieme presso un’istanza della classe ButtonGroup, come
viene mostrato nelle righe seguenti:
JRadioButton radioButton1=new JRadioButton("R1");
JRadioButton radioButton2=new JRadioButton("R2");
JRadioButton radioButton2=new JRadioButton("R3");
ButtonGroup group = new ButtonGroup();
group.add(radioButton1);
group.add(radioButton2);
group.add(radioButton3);
Ogni volta che l’utente attiva uno dei pulsanti registrati presso il ButtonGroup, gli altri vengono automaticamente messi a riposo.
I JComboBox offrono all’utente la possibilità di effettuare una scelta a
partire da un elenco elementi, anche molto lungo. A riposo il componente
si presenta come un pulsante, con l’etichetta corrispondente al valore attualmente selezionato. Un clic del mouse provoca la comparsa di un menu
provvisto di barra laterale di scorrimento, che mostra le opzioni disponibili.
Se si imposta la proprietà editable di un JComboBox a true esso si comporterà a riposo come un JTextField, permettendo all’utente di inserire valori
non presenti nella lista. È possibile creare un JComboBox usando i seguenti
costruttori:
JComboBox();
JComboBox(Object[] items);
Il secondo costruttore permette di inizializzare il componente con una
lista di elementi di qualsiasi tipo (ad esempio String). Se viene aggiunto
al ComboBox un oggetto generico (ad esempio un oggetto Libro), allora il
valore corrispondente visualizzato nella lista delle opzioni è quello ottenuto
chiamando sull’oggetto il metodo toString(). Quindi se si desidera aggiungere oggetti generici (magari definiti all’interno del programma), bisognerà
37
2.3 Paranoramica di alcuni widget
import javax.swing.*;
import java.awt.*;
public class Application {
public static void main(String args[])
{
JFrame win;
win = new JFrame("EsempioÃdiÃJComboBox");
String lista[]=new String[10];
for(int i=0;i<lista.length;i++)
lista[i]="ElementoÃnumeroÃ"+i;
JComboBox cBox=new JComboBox(lista);
Container c = win.getContentPane();
c.add(cBox);
win.setSize(200,200);
win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
win.setVisible(true);
}
}
Listato 2.2: Esempio di uso dei ComboBox
avere la cura di ridefinire tale metodo. Un gruppo di metodi permette di
aggiungere, togliere o manipolare gli elementi dell’elenco, cosı̀ come si fa con
un Vector:
public
public
public
public
public
public
public
void addItem(Object anObject);
void removeItem(Object anObject)
void removeItemAt(int anIndex);
void removeAllItems();
Object getItemAt(int index);
int getItemCount();
void insertItemAt(Object anObject, int index)
Per ottenere l’elemento correntemente selezionato, è disponibile il metodo:
public Object getSelectedItem();
La Figura 2.5 mostra un esempio di suo uso. Il Listato 2.2 rappresenta il
codice corrispondente:
38
2.4 L’ereditarietà per personalizzare i frame
Figura 2.5: Uso di ComboBox.
2.4
L’ereditarietà per personalizzare i frame
Aggiungendo ad un frame molti componenti dell’interfaccia utente, il frame
stesso può diventare abbastanza complesso: per frame che contengono molti
componenti è opportuno utilizzare l’ereditarietà. Infatti, se il software che si
sta sviluppando contiene molte finestre ricche di componenti ci si troverebbe
a dover fronteggiare una soluzione il cui caso limite è di un metodo main che
contiene la definizione di tutte le finestre del programma. Senza arrivare a
questa situazione, si otterrebbero moduli sono molto accoppiati e poco coesi; inoltre, gli stessi moduli sarebbero poco leggibili e non sarebbe facile
manutenerli. In parole povere, non si sfrutterebbe le potenzialità dell’Object Orientation: non sarebbe possibile permette l’information hiding dei
contenuti dei frame e dei controlli in essi contenuti; non sarebbe nemmeno
sfruttato l’incapsulamento delle implementazione, mettendo insieme concetti
eterogenei e finestre diverse tra loro.
Il Listato 2.3 produce la stessa finestra in Figura 2.3 sfruttando l’ereditarietà. La differenza rispetto agli esempi già visti non è da poco: a questo
punto ogni finestra diventa una nuova classe che, ereditando da JFrame, è
un modello per le finestre. É opportuno definire gli oggetti che mappano i
widgets come variabili di instanza (nel l’esempio l’unico widget è una etichetta (JLabel). La definizione della finestra e dei componenti contenuti viene
fatta direttamente nel costruttore (o in metodo chiamati da questo). La
prima istruzione chiama il costruttore della superclasse che prende come
parametro una stringa; in questo modo è possibile impostare il titolo della finestra. Infatti, se non fosse stato esplicitamente chiamato il costruttore
39
2.4 L’ereditarietà per personalizzare i frame
import javax.swing.*;
import java.awt.*;
class MyFrame extends JFrame
{
JLabel jl = new JLabel("BuonaÃLezione");
public MyFrame()
{
super("PrimaÃfinestra");
Container c = this.getContentPane();
c.add(jl);
this.setSize(200,200);
this.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
}
public class Application
{
public static void main(String args[])
{
MyFrame = new MyFrame();
}
}
Listato 2.3: Esempio con l’ereditarietà
40
2.5 I Layout Manager e la gerarchia di contenimento
ad un parametro String, sarebbe stato chiamato implicitamente il costruttore della superclasse di default (quello senza parametri) prima di continuare
l’esecuzione di quello della classe derivata. Il costruttore senza parametri
avrebbe impostato a vuoto il titolo della finestra.1 Il resto del costruttore
è simile alla tecnica di definizione delle finestre degli esempi precedenti con
l’unica differenza che i metodi sono chiamati su this (cioè se stesso) perchè
a questo punto i metodi da chiamare sono quelli ereditati dalla superclasse
JFrame.
2.5
2.5.1
I Layout Manager e la gerarchia di contenimento
Layout Management
Quando si dispongono i componenti all’interno di un Container sorge il problema di come gestire il posizionamento: infatti, sebbene sia possibile specificare le coordinate assolute di ogni elemento dell’interfaccia, queste possono
cambiare nel corso della vita del programma allorquando la finestra principale
venga ridimensionata. In molti Container i controlli sono inseriti da sinistra
verso destra con su una ipotetica riga: può non essere sempre la politica per
la GUI (Graphic User Interface) desiderata. Per semplificare il lavoro di impaginazione e risolvere questo tipo di problemi possibile ricorrere ai layout
manager, oggetti che si occupano di gestire la strategia di posizionamento
dei componenti all’interno di un contenitore.
Un gestore di layout è una qualsiasi classe che implementa l’interfaccia
LayoutManager; ogni container nasce con un certo Layout Manager ma è
possibile assegnare il più opportuno per quel Container con il metodo:
public void setLayout(LayoutManager m);
Il primo layout da essere citato è il gestore a scorrimento (Flow Layout)
che sono inseriti da sinistra verso destra con la loro Preferred Size, cioè la
dimensione minima necessaria a disegnarlo interamente. Usando il paragone
con un editor di testi, ogni controllo rappresenta una “parola” che ha una sua
propria dimensione. Come le parole in un editor vengono inseriti da sinistra
verso destra finchè entrano in una riga, cosı̀ viene fatto per i controlli inseriti
da sinistra verso destra. Quando un componente non entra in una “riga”
viene posizionato in quella successiva. I costruttori più importanti di oggetti
FlowLayout sono i seguenti:
1
A dire il vero, sarebbe stato possibile impostare il titolo della finestra in un secondo
momento modificando la proprietà Title: setTitle(‘‘Prima Finestra’’);
41
2.5 I Layout Manager e la gerarchia di contenimento
(a)
42
(b)
Figura 2.6: Aspetto grafico del frame definito nel Listato 2.4 e comportamento
del Flow Layout rispetto al ridimensionamento
public FlowLayout();
public FlowLayout(int allin);
Il secondo costruttore specifica l’allineamento dei controlli su una riga; il
parametro può essere una delle seguenti costanti che rispettivamente allineano
a sinistra, centro o destra:
FlowLayout.LEFT
FlowLayout.CENTER
FlowLayout.RIGHT
Il costruttore di default imposta l’allineamento centrale. Il Listato 2.4 mostra
un esempio di uso del Flow Layout. L’aspetto del frame risultato è mostrato
in Figura 2.6. É opportuno osservare come l’allineamento dei componenti del frame si adatti durante l’esecuzione quando la stessa finestra viene
ridimensionata (vedi situazione 2.6(b)). Ovviamente se non esiste nessun
allineamento per i componenti del frame tale che tutti siano contemporaneamente visibile, allora alcuni di questi risulteranno in parte o del tutto non
visibili; per esempio, la finestra è troppo piccola.
Il gestore a griglia (GridLayout) suddivide il contenitore in una griglia
di celle di uguali dimensioni. Le dimensioni della griglia vengono definite
mediante il costruttore:
public GridLayout(int rows, int columns)
in cui i parametri rows e columns specificano rispettivamente le righe e le
colonne della griglia. A differenza di quanto avviene con FlowLayout, i componenti all’interno della griglia assumono automaticamente la stessa dimensione, dividendo equamente lo spazio disponibile. L’esempio descritto nel
Listato 2.5 permette di illustrare il funzionamento di questo pratico layout
manager; il risultato è in Figura 2.7.
Il gestore a bordi (BorderLayout) suddivide il contenitore esattamente
in cinque aree, disposte a croce, come nella Figura 2.8. Ogni zona può
2.5 I Layout Manager e la gerarchia di contenimento
import javax.swing.*;
import java.awt.*;
public class MyFrame extends JFrame
{
JButton uno=new JButton("Uno");
JButton due=new JButton("Due");
JButton tre=new JButton("Tre");
JButton quattro=new JButton("Quattro");
JButton cinque = new JButton("Cinque");
public MyFrame()
{
super("FlowÃLayout");
Container c = this.getContentPane();
c.setLayout(new FlowLayout());
c.add(uno);
c.add(due);
c.add(tre);
c.add(quattro);
c.add(cinque);
setSize(300,100);
setVisible(true);
}
}
public class Application
{
public static void main(String args[])
{
MyFrame = new MyFrame();
}
}
Listato 2.4: Uso del Flow Layout
43
2.5 I Layout Manager e la gerarchia di contenimento
public class MyFrame extends JFrame
{
public MyFrame()
{
super("GridÃLayout");
Container c = this.getContentPane();
c.setLayout(new GridLayout(4,4));
for(int i = 0; i<15; i++)
c.add(new JButton(String.valueOf(i));
setSize(300,300);
setVisible(true);
}
}
Listato 2.5: Uso del Grid Layout
Figura 2.7: Aspetto grafico del frame definito nel Listato 2.5.
44
2.5 I Layout Manager e la gerarchia di contenimento
Figura 2.8: Le aree di una disposizione a bordi
contenere uno ed un solo widget (o Container): un secondo widget inserito
in una zona sostituisce il precedente. Se una o più zone non vengono riempite,
allora i componenti nelle altre zone sono estesi a riempire le zone vuote. Il
BorderLayout è il gestore di layout di default per la Lastra dei Contenuti di
un JFrame.
Il programmatore può decidere in quale posizione aggiungere un controllo
utilizzando la variante presente nei Container:
public void add(Component c,String s);
dove il primo parametro specifica il componente da aggiungere e il secondo indica la posizione. I valori validi per il secondo parametro sono le
costanti BorderLayout.NORTH, BorderLayout.SOUTH, BorderLayout.CENTER,
BorderLayout.EAST e BorderLayout.WEST. Si osservi l’esempio nel Listato 2.6.
2.5.2
Progettazione della GUI con le gerarchie di contenimento
I gestori di layout permettono maggiore versatilità nell’inserimento dei controlli nelle finestre. Tuttavia nella stragrande maggioranza delle situazioni
un’intera finestra non può seguire un unico layout. Si prenda, ad esempio,
in considerazione il frame in Figura 2.9. Non è difficile convincersi che la
parte in alto contenente una sorta di “tastierino numerico” segue un GridLayout mentre i bottoni in basso seguono un FlowLayout centrato. Poichè
ogni Container può seguire uno ed un unico layout, occorre predisporre più
Container, uno per ogni zona che ha un layout differente. Ovviamente ad
ogni Container possono essere aggiunti direttamente i controlli oppure altri
Container. La lastra dei contenuti è il Container “radice” ed altri si possono
aggiungere all’interno. I Container (insieme con i componenti contenuti) aggiunti in un altro Container si comporteranno come ogni altro widget la cui
45
2.5 I Layout Manager e la gerarchia di contenimento
public class MyFrame extends JFrame
{
JButton nord = new JButton("Nord");
JButton centro = new JButton("Centro");
JButton ovest=new JButton("Ovest");
public MyFrame()
{
super("BorderÃLayout");
Container c = this.getContentPane();
c.setLayout(new BorderLayout());
c.add(nord,BorderLayout.NORTH);
c.add(centro,BorderLayout.CENTER);
c.add(ovest,BorderLayout.WEST);
setSize(300,300);
setVisible(true);
}
}
Listato 2.6: Uso del Border Layout
Figura 2.9: Un frame più complesso
dimensione preferita è quella minima necessaria a visualizzare tutti i componenti all’interno. Per creare nuovi Container conviene utilizzare la classe
46
2.5 I Layout Manager e la gerarchia di contenimento
javax.swing.JPanel che eredita da java.awt.Container.
La forza di questa soluzione è data dall’alta modularità: è possibile usare
un layout per il pannello interno e un altro layout per il ContentPane. Il
pannello interno verrà inserito nella finestra coerentemente con il layout della
lastra dei contenuti. Inoltre all’interno pannelli interni se ne possono inserire
altri con loro layout e cosı̀ via, come nel gioco delle scatole cinesi. Il numero
di soluzioni diverse sono praticamente infinite.
Ad esempio per il frame in Figura 2.9 è possibile creare due contenitoripannelli JPanel: uno per contenere il “tastierino numerico” organizzato il
GridLayout ed un altro il FlowLayout per i tre bottoni in basso. La lastra
dei Contenuti viene lasciata in BorderLayout: al centro viene aggiunto il
pannello tastierino ed a sud il pannello con i tre bottoni.
2.5.3
Progettazione top down di interfacce grafiche
Durante la progettazione delle interfacce grafiche, può essere utile ricorrere
a un approccio top down, descrivendo l’insieme dei componenti a partire
dal componente più esterno per poi procedere a mano a mano verso quelli
più interni. Si può sviluppare una GUI come quella dell’esempio precedente
seguendo questa procedura:
1. Si definisce il tipo di top level container su cui si vuole lavorare (tipicamente un JFrame).
2. Si assegna un layout manager al content pane del JFrame, in modo da
suddividerne la superficie in aree più piccole.
3. Per ogni area messa a disposizione dal layout manager è possibile
definire un nuovo JPanel. Ogni sotto pannello può utilizzare un layout
manager differente.
4. Ogni pannello identificato nel terzo passaggio può essere sviluppato
ulteriormente, creando al suo interno ulteriori pannelli o disponendo
dei controlli.
Il risultato della progettazione può essere rappresentato con un albero
della GUI. L’obiettivo di tale albero è mostrare come i Container (a partire
dalla lastra dei contenuti ) sono stati suddivisi per inserire i componenti o
altri Container.
Ogni componente (o Container) è rappresentato da un nodo i cui figli sono
i componenti (o i Container) contenuti all’interno e il padre è il componente
che lo contiene.
47
2.5 I Layout Manager e la gerarchia di contenimento
Figura 2.10: Un frame d’esempio
Una volta conclusa la fase progettuale, si può passare a scrivere il codice
relativo all’interfaccia: in questo secondo momento, è opportuno adottare un
approccio bottom up, realizzando dapprima il codice relativo ai componenti
atomici, quindi quello dei contenitori e infine quello del JFrame.
Esempio di Progettazione Top-Down
JFrame
BorderLayout
nordPnl
FlowLayout
infoLbl
centroPnl
GridLayout (2,1)
opz1chk opz2chk
sudPnl
FlowLayout
okBtn
cancBtn
Figura 2.11: Un frame più complesso
Un esempio finale viene mostrato per riassumere quanto è stato finora
detto. Si inizierà mostrando la progettazione top-down dell’interfaccia e si
concluderá mostrandone la realizzazione bottom-up. Si consideri la semplice
finestra in Figura 2.10. L’albero della GUI corrispondente è in Figura 2.11.
Il codice per mostrare tale finestra è nel Listato 2.7. Si osservi l’invocazione del metodo pack() che sostituisce il metodo setSize(x,y) e che
imposta la dimensione della finestra alla minima necessaria a visualizzare
tutti i controlli. Inoltre si osservi come le due istruzioni successive hanno
48
2.5 I Layout Manager e la gerarchia di contenimento
public class MyFrame extends JFrame
{
JPanel nordPnl = new JPanel();
JPanel centroPnl = new JPanel();
JPanel sudPnl = new JPanel();
JLabel infoLbl = new Label("Selezionare:");
JCheckBox opz1Chk = new JCheckBox("Opz1");
JCheckBox opz2Chk = new JCheckBox("Opz2");
JButton okBtn=new JButton("OK");
JButton cancBtn=new JButton("Annulla");
public MyFrame() {
super("Esempio");
centroPnl.setLayout(new GridLayout(2,1));
centroPnl.add(opz1Chk);
centroPnl.add(opz2Chk);
nordPnl.add(infoLbl);
sudPnl.add(okBtn);
sudPnl.add(cancBtn);
getContentPane().add(nordPnl,BorderLayout.NORTH);
getContentPane().add(centroPnl,BorderLayout.CENTER);
getContentPane().add(sudPnl,BorderLayout.SOUTH);
pack();
Dimension dim=
Toolkit.getDefaultToolkit().getScreenSize();
setLocation((dim.getWidth()-this.getWidth())/2,
(dim.getHeight()-this.getHeight())/2);
setVisible(true);
}
}
Listato 2.7: Il codice della figura 2.10
49
2.5 I Layout Manager e la gerarchia di contenimento
lo scopo di centrare la finestra sullo schermo. Il metodo getScreenSize()
chiamato sulla classe Singleton 2 Toolkit del package java.awt restituisce un
riferimento ad un oggetto java.awt.Dimension. Questo possiede due proprietà
Width e Height che, nel caso specifico, conterranno la dimensione dello schermo in pixel. L’istruzione successiva sposta la finestra al centro dello schermo con il metodo setLocation(int x,int y) dove x, y sono le coordinate
dell’angolo in alto a sinistra.
2
Una classe Singleton è tale che di essa esista sempre al più una instanza. Per maggiori
dettagli si faccia riferimento a [5] (in ogni modo sarà anche argomento del corso).
50
2.6 La Gestione degli Eventi
2.6
La Gestione degli Eventi
Figura 2.12: La gestione ad event delegation
La gestione degli eventi grafici in Java segue il paradigma event delegation
(conosciuto anche come event forwarding). Ogni oggetto grafico è predisposto ad essere sollecitato in qualche modo dall’utente e ad ogni sollecitazione
genera eventi che vengono inoltrati ad appositi ascoltatori, che reagiscono
agli eventi secondo i desideri del programmatore. L’event delegation presenta il vantaggio di separare la sorgente degli eventi dal comportamento a essi
associato: un componente non sa (e non è interessato a sapere) cosa avverrà
al momento della sua sollecitazione: esso si limita a notificare ai propri ascoltatori che l’evento che essi attendevano è avvenuto, e questi provvederanno
a produrre l’effetto desiderato.
Ogni componente può avere più ascoltatori per un determinato evento
o per eventi differenti, come anche è possibile installare uno stesso ascoltatore su più componenti anche diversi a patto che entrambi possono essere
sollecitati per generare l’evento. L’operazione di installare lo stesso ascoltatore su più controlli (che quindi si comporteranno nello stesso modo se sollecitati dallo stesso evento) è frequente: si pensi alle voci di un menu che vengono replicate su una toolbar per un più facile accesso ad alcune funzionalità
frequenti
2.6.1
Implementazione dell’event delegation
Il gestore delle finestre può generare un numero enorme di eventi (sono eventi
muovere il mouse nella finestra, spostare o ridimensionare una finestra, scri-
51
2.6 La Gestione degli Eventi
vere o abbandonare un campo di testo, . . . ). Molte volte si è interessati a
pochi di questi eventi. Per fortuna, il programmatore può considerare solo gli
eventi di cui ha interesse, gestendoli con un ascoltatore opportuno, ignorando completamente tutti gli altri che di default verranno gestiti come eventi
vuoti.
Quando accade un evento per il quale esiste un ricevitore, la sorgente dell’evento chiama i metodi da voi forniti nell ricevitore, dando, in un oggetto di
una classe evento, informazioni più dettagliate sull’evento stesso. In generale
sono coinvolte tre classi:
La classe del ricevitore. Implementa una particolare interfaccia del tipo
XXXListener tipica degli eventi di una certa classe. I metodi dell’interfaccia che la classe dell’ascoltatore implementa contengono il codice
eseguito allo scatenarsi degli eventi di una classe che l’ascoltatore intercetta. Grazie al fatto che ogni ascoltatore è caratterizzato da una particolare interfaccia Java, qualsiasi classe può comportarsi come un ascoltatore, a patto che fornisca un’implementazione per i metodi definiti
dalla corrispondente interfaccia listener
L’origine dell’evento. É il componente che genera l’evento che si vuole gestire su cui si vuole “installare” l’ascoltatore. Ogni componente dispone
per ogni evento supportato di due metodi:
void addXXXListener(XXXListener ascoltatore);
void removeXXXListener(XXXListener ascoltatore);
La classe evento. Contiene le informazioni riguardanti le caratteristiche
dell’evento generato. Gli oggetti di questa classe sono istanziati direttamente dai componenti che notificano eventi agli ascoltatori. Formalmente sono parametri di input dei metodi dell’interfaccia implementata dall’ascoltatore. Nel momento in cui il componente viene
sollecitato, esso chiama gli ascoltatori in modalità callback, passando come parametro della chiamata un’apposita sottoclasse di Event.
Queste classi contengono tutte le informazioni significative per l’evento stesso e possono essere utilizzate per modificare il comportamento
dell’ascoltatore in base alle informazioni sull’evento scatenato
Le classi che permettono di gestire gli eventi generati dai componenti
Swing sono in gran parte gli stessi utilizzati dai corrispondenti componenti
AWT, e si trovano nel package java.awt.event. Alcuni componenti Swing,
tuttavia, non hanno un omologo componente AWT: in questo caso le classi
necessarie a gestirne gli eventi sono presenti nel package javax.swing.event.
52
2.6 La Gestione degli Eventi
2.6.2
Un esempio: elaborare gli eventi del mouse
Per chiarire il funzionamento, a prima vista complesso, della gestione degli
eventi Swing, verra fatto un esempio. In particolare, vogliamo realizzare un
meccanismo per “spiare” gli eventi del mouse su una finestra e stamparli.
Una classe che desidera catturare gli eventi del mouse deve implementare
l’interfaccia
MouseListener definita nel package java.awt.event che è cosı̀ definita:
public interface MouseListener
{
void mouseClicked(MouseEvent e);
void mouseEntered(MouseEvent e);
void mouseExited (MouseEvent e);
void mousePressed(MouseEvent e);
void mouseReleased(MouseEvent e);
}
dove MouseEvent è la corrispondente classe evento il cui scopo è dare informazioni aggiuntive sull’evento del mouse. Infatti, questa definisce due
metodi che permettono di conoscere le coordinate del mouse allo scatenarsi dell’evento (i primi due) e un terzo metodo che permette di determinare
quale bottone del mouse è stato premuto:
int getX();
int getY();
int getModifiers()
Nel codice del Listato 2.9 viene mostrata l’implementazione della “spia”.
Si noti come la classe ascoltatore MouseSpy implementi l’interfaccia
MouseListener e come la classe definisca due metodi vuoti: mouseEntered
e mouseExited. La definizione dei due metodi vuoti ha lo scopo di dichiarare
che gli eventi legati all’entrata e all’uscita del mouse dall’area della finestra
devono essere ignorati. In effetti, ignorare gli eventi è già il comportamento
di default e quindi tale dichiarazione è superflua. La ragione per cui è stata
comunque dichiarata la gestione di tali eventi è legata al linguaggio Java:
affinché una classe implementi una interfaccia, tale classe deve implentare
tutti i metodi dell’interfaccia. E questo vale anche in questo caso: la classe
MouseSpy deve implementare tutti i metodi dell’interfaccia MouseListener
per poter dichiarare di implementarla.
Nella Tabella 2.1 sono riassunti gli ascoltatori più importanti. Per ulteriori dettagli si consiglia di consultare la documentazione Java Swing [6].
53
2.6 La Gestione degli Eventi
import java.awt.event.*;
import javax.swing.*;
public class MouseSpy implements MouseListener
{
public void mouseClicked(MouseEvent e) {
System.out.println
("ClickÃsuÃ("+e.getX()+","+e.getY()+")");
}
public void mousePressed(MouseEvent e) {
System.out.println
("PremutoÃsuÃ("+e.getX()+","+e.getY()+")");
}
public void mouseReleased(MouseEvent e) {
System.out.println
("RilasciatoÃsuÃ("+e.getX()+","+e.getY()+")")
}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}
public class MyFrame extends JFrame
{
public MyFrame()
{
super("MouseTest");
this.addMouseListener(new MouseSpy());
setSize(200,200);
setVisible(true);
}
}
Listato 2.8: Primo esempio della gestione degli eventi
54
2.7 La gestione degli eventi Azione
ActionListener
ComponentListener
FocusListener
KeyListener
MouseMotionListener
MouseListener
TextListener
WindowListener
Definisce 1 metodo per ricevere eventi-azione
Definisce 4 metodi per riconoscere quando un
componente viene nascosto, spostato, mostrato o
ridimensionato
Definisce 2 metodi per riconoscere quando un
componente ottiene o perde il focus
Definisce 3 metodi per riconoscere quando viene
premuto, rilasciato o battuto un tasto
Definisce 2 metodi per riconoscere quando il mouse
e trascinato o spostato
Se ne è già parlato. . .
Definisce 1 metodo per riconoscere quando cambia
il valore di un campo testo
Definisce 7 metodi per riconoscere quando un finestra viene attivata, chiusa, disattivata, ripristinata,
ridotta a icona, ecc.
Tabella 2.1: Gli ascoltatori più importanti
2.6.3
Uso di adapter nella definizione degli ascoltatori
Alcuni componenti grafici generano eventi cosı̀ articolati da richiedere, per
la loro gestione, ascoltatori caratterizzati da più di un metodo. Si è già visto che MouseListener, ne definisce tre. L’interfaccia WindowListener (vedi
Listato 2.9) ne dichiara addirittura sette, ossia uno per ogni possibile cambiamento di stato della finestra. Si è detto che se si desidera creare un ascoltatore interessato a un solo evento (per esempio uno che intervenga quando la
finestra viene chiusa), esso dovrà comunque fornire un’implementazione vuota anche dei metodi cui non è interessato. Nei casi come questo è possibile
ricorrere agli adapter: classi di libreria che forniscono un’implementazione
vuota di un determinato ascoltatore. Per esempio, il package java.awt.event
contiene la classe WindowAdapter, che fornisce un’implementazione vuota di
tutti i metodi previsti dall’interfaccia. Una sottoclasse di WindowAdapter,
pertanto, è un valido WindowListener, con in più il vantaggio di una maggior
concisione. Si veda l’esempio 2.10
2.7
55
La gestione degli eventi Azione
La maggior parte dei componenti Swing generano eventi, noti come eventi
azione, che possono essere catturati da un ActionListener. Essa definisce un
2.7 La gestione degli eventi Azione
public interface WindowListener
{
public void windowOpened(WindowEvent e);
public void windowClosing(WindowEvent e);
public void windowClosed(WindowEvent e);
public void windowIconified(WindowEvent e);
public void windowDeiconified(WindowEvent e);
public void windowActivated(WindowEvent e);
public void windowDeactivated(WindowEvent e);
}
Listato 2.9: L’interfaccia MouseListener
class WindowClosedListener extends WindowAdapter {
public void windowClosed(WindowEvent e)
{
// codice da eseguire alla chiusura della finestra
}
}
Listato 2.10: Esempio dell’uso degli Adapter
unico metodo:
public interface ActionListener
{
public void actionPerformed(ActionEvent ae);
}
L’oggetto ActionEvent da informazioni aggiuntive sull’evento generato. Ad
esempio, definisce il metodo Object getSource() che restituisce l’oggetto
che ha generato l’evento.
La maggior parte dei widget java sono predisposti per generare eventi
azione. Ad esempio quando si clicca su un bottone, si preme INVIO in un
campo di testo, si seleziona una voce di un menu oppure si seleziona/deseleziona una voce di una checkBox o un radioButton, viene generato un
evento azione. Si potrebbe obiettare che, la gestione del click di un bottone,
potrebbe essere fatta usando un mouseListener. Il vantaggio di utilizzare un
evento azione risiede nel fatto che è possibile cliccare su un bottone anche con
la tastiera (anche se in quel caso non è un vero click!): l’utente con il tasto
TAB potrebbe spostarsi sulla schermata, mettere il focus su un bottone e
qui premere la barra spaziatrice per voler “cliccare” sul bottone.
56
2.8 Accedere dall’ascoltatore agli oggetti di una finestra
Figura 2.13: La finestra ottenuta dal Listato 2.11
É possibile installare uno stesso ascoltatore, ad esempio, per un bottone
della toolbar e una voce di menu. Il vantaggio risiede nel fatto che sia la voce
del menu che il bottone hanno lo stesso ascoltatore invece che due ascoltatori
separati che fanno la stessa cosa.
Un primo esempio di uso degli eventi azione è nel Listato 2.11. Il comportamento è quello in Figura 2.13: quando l’utente seleziona uno dei bottoni
della finestra MyFrame viene aperta una finestra di dialogo con il testo del
bottone premuto. Su ogni bottone è installato lo stesso ascoltatore per gli
eventi azione. Alla pressione di un bottone viene eseguito il metodo actionPerformed, il quale si fa restituire con il metodo getSource() il bottone
premuto (si noti il cast giacchè il metodo lo restituisce come riferimento ad
Object). A partire dal bottone premuto, con il metodo getText() viene
restituito il testo visualizzato nel bottone.
2.8
Accedere dall’ascoltatore agli oggetti di
una finestra
L’approccio finora descritto funziona abbastanza bene anche se presenta ancora alcune problematiche. Il primo problema è legato al fatto che la finestra
e i suoi ascoltatori sono divisi in classi separate. Supponiamo di avere ascoltatori per gestire gli eventi scatenati da alcuni componenti installati in una
data finestra. Essendo le due classi formalmente indipendenti tra loro, per
quanto detto, non è possibile accedere dagli ascoltatori a tutti i componenti
della finestra (tranne il componente che ha generato l’evento).
Supponiamo di voler realizzare una finestra come quella in Figura 2.14.
Alla pressione del bottone OK il contenuto del campo di testo deve essere
visualizzato in un message dialog box. Si consideri il codice nel Listato 2.12.
Nell’ascoltatore definiamo un nuovo JTextField text. Quest’approccio
non funziona perchè il campo di testo definito è un nuovo campo di testo e
non quello della finestra. Quindi la pressione del bottone mostrerà sempre
57
2.8 Accedere dall’ascoltatore agli oggetti di una finestra
public class MyFrame extends JFrame
{
private JButton uno = new JButton("Uno");
private JButton due = new JButton("Due");
private JButton tre = new JButton("Tre");
private JButton quattro = new JButton("Quattro");
private JButton cinque = new JButton("Cinque");
Ascoltatore listener = new Ascoltatore();
public MyFrame()
{
...
Container c = this.getContentPane();
c.add(uno);
uno.addActionListener(listener);
...
c.add(cinque);
cinque.addActionListener(listener);
}
}
public class Ascoltatore implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
JButton b = (JButton)event.getSource();
JOptionPane.showMessageDialog(null,
"ÈÃstatoÃpremuto"+b.getText());
}
}
Listato 2.11: Esempio uso di ActionPerformed
Figura 2.14: La finestra ottenuta dal Listato 2.12
58
2.8 Accedere dall’ascoltatore agli oggetti di una finestra
public class MyFrame extends JFrame {
JPanel centro = new JPanel();
JPanel sud = new JPanel();
JTextField txt = new JTextField(20);
JButton button = new JButton("Premi");
public MyFrame() {
super("Esempio");
centro.add(txt);
sud.add(button);
getContentPane().add(centro,BorderLayout.CENTER);
getContentPane().add(sud,BorderLayout.SOUTH);
button.addActionListener(new Listen());
...
}
}
class Listen implements ActionListener {
public void actionPerformed(ActionEvent e) {
JTextField text = new JTextField();
JOptionPane.showMessageDialog(
null,text.getText());
}
}
Listato 2.12: Un esempio che non funziona
una finestra dal contenuto vuoto perchè di default un campo di testo viene
inizializzato con il contenuto vuoto.
Un primo approccio è utilizzare una classe interna. Una classe interna
è definita all’interno di un’altra classe e può accedere a tutte le variabili e
i metodi di istanza della classe esterna (anche a quelli privati). Si consideri
il Listato 2.13 che fa uso della classi interne: l’istruzione text = txt nell’ascoltatore fa sı̀ che il riferimento text “punti” allo stesso campo di testo
riferito da txt. Quindi quando nel codice successivo, l’ascoltatore chiede
text.getText() ottiene effettivamente il codice richiesto.
Questa tecnica è accettabile se l’ascoltatore esegue poche operazioni e
pochi ascoltatori esistono. Altrimenti la classe MyFrame diventa eccessivamente grande. Inoltre il codice ottenuto diventa poco leggibile perchè si
tende ad accorpare classi che rappresentano concetti diversi.
La soluzione preferibile è quella in cui l’ascoltatore definisce un costruttore che prende come parametro un riferimento alla finestra con i componenti
59
2.8 Accedere dall’ascoltatore agli oggetti di una finestra
public class MyFrame extends JFrame {
JPanel centro = new JPanel();
JPanel sud = new JPanel();
JTextField txt = new JTextField(20);
JButton button = new JButton("Premi");
public MyFrame() {
super("Esempio");
centro.add(txt);
...
button.addActionListener(new Listen());
...
}
class Listen implements ActionListener
{
public void actionPerformed(ActionEvent e) {
JTextField text = txt;
JOptionPane.showMessageDialog(
null,text.getText());
}
}
}
Listato 2.13: Una soluzione che funziona con le classi interne
60
2.9 Condividere gli ascoltatori per più oggetti
richiesti. Il costruttore memorizza tale riferimento come variabile di classe.
La classe ascoltatore resta comunque esterna: essa accede ai widget della
finestra tramite tale riferimento e l’operatore punto. Ovviamente i widget
non devono essere memorizzati privati. La soluzione è descritta nel Listato 2.14. Nel codice viene anche mostrato una grande potenzialità degli eventi
azione: l’evento azione del campo di testo è gestito nello stesso modo della finestra. Ne risulta, di conseguenza, che la pressione del bottone e la
pressione del tasto ENTER sul campo di testo vengono gestiti dallo stesso
ActionListener e quindi gestiti nello stesso modo.
2.9
Condividere gli ascoltatori per più oggetti
La tecnica più naive per gestire più eventi associati ad un controllo è quello di
creare una classe “ascoltatore” per ogni oggetto e per ogni classe di eventi da
gestire per quello oggetto. Ciò vorrebbe dire che se dovessimo gestire l’evento
di pressione di X bottoni, dovrebbe realizzare X classi che implementano
ActionListener.
Ovviamente è nella pratica improponibile prevedere una classe per ogni
bottone, voce del menu e cosı̀ via. Infatti, consideriamo per esempio un’applicazione con 5 finestre, ognuna con 5 bottoni. Supponiamo che una di tali
finestre abbia anche 3 voci del menu di 4 opzioni ciascuno. Ciò significherebbe
che avremmo 37 classi solo per gestire la pressione dei bottoni o della voci
del menu!
L’idea più elegante è raggruppare gli oggetti su cui ascoltare in classi,
prevedendo un ascoltatore condiviso per tutti gli oggetti della stessa classe.
Da questo punto in poi consideriamo solo gli eventi actionListener senza
perdere di generalità. Lo stesso approccio si applica in tutti gli altri casi. I
componenti della stessa classe, ovviamente, condivideranno lo stesso metodo
actionPerformed. A meno che tutti i componenti della stessa classe si devo
comportare nello stesso modo allo scatenarsi dell’evento azione, occore essere
in grado di “capire” quale oggetto ha generato l’evento. Questo al fine di
capire quale comportamente adottare.
Esistono due modi per capire “chi” ha generato l’evento:
1. Attraverso il metodo getSource() della classe ActionEvent che restituisce un riferimento all’oggetto che ha scatenato l’evento. In questo
modo è possibile utilizzare un approccio di tipo switch/case: se il riferimento “punta” a X, allora chiama un metodo privato che corrisponde
alla gestione di pressione del bottone X; se il riferimento “punta” a Y,
61
2.9 Condividere gli ascoltatori per più oggetti
public class MyFrame extends JFrame {
JPanel centro = new JPanel();
JPanel sud = new JPanel();
JTextField txt = new JTextField(20);
JButton button = new JButton("Premi");
Listen ascolt=new Listen(this);
public MyFrame() {
super("Esempio");
centro.add(txt);
...
button.addActionListener(ascolt);
txt.addActionListener(ascolt);
...
}
}
class Listen implements ActionListener {
MyFrame frame;
public Listen(MyFrame aFrame)
{ frame = aFrame; }
public void actionPerformed(ActionEvent e) {
JTextField text = frame.txt;
JOptionPane.showMessageDialog(
null,text.getText());
}
}
Listato 2.14: La soluzione migliore
62
2.9 Condividere gli ascoltatori per più oggetti
allora chiama il metodo per Y. E cosi via. ’E ovviamente chiaro se questa approccio è fattibile se è possibile confrontare i riferimenti ottenuti
con il metodo getSource() con i riferimenti memorizzati internamente
alla classe che estende JFrame e che disegna la finestra con i bottoni
X, Y e gli altri. Questo approccio è generalmente fattibile, quindi, con
le classi interne.
2. Utilizzando la proprietà actionCommand, implementata per ogni componente, che permette di associare una stringa identificativa univoca
ad ogni componente che scatena un evento azione. A questo punto è
possibile definire all’interno del metodo actionPerformed un approccio di tipo switch/case sugli actionCommand. Se lo actionCommand
dell’oggetto è “ACTX” (che è stato associato ad X) allora l’ascoltatore
esegue una certa porzione di codice, se lo actionCommand è “ACTY” fa
un’altra cosa. E cosı̀ via. In questo nuovo contesto non è più necessario
per l’ascoltatore di avere accesso ai riferimenti.
Si consideri, ad esempio, la porzione di condice nel Listato 2.15 dove ci
sono tre voci di menu UpOpt, DownOpt, RandomOpt. Le tre voci del menu
hanno associati lo stesso ascoltatore con lo stesso metodo actionPerformed.
All’interno del metodo la discriminazione su come l’ascoltatore si deve comportare in funzione del metodo invocato è fatto analizzando i riferimenti.
L’istruzione src=e.getSource() restituisce un riferimento all’oggetto che
ha generato l’evento. Se tale riferimento è uguale a UpOpt (entrambi puntano allo stesso oggetto corrispondente alla voce del menu “Up”, allora la
voce del menu scelta è Up. Quindi viene eseguita la porzione di codice relativa a tale voce. Lo stesso per le altre voci. L’ascoltatore è realizzato come
classe interna in modo tale da poter realizzare l’uguaglianza tra i puntato e
verificare se coincidono.
Come si è detto, l’approccio con le classi interne ha molti svantaggi. la
metodologia che vogliamo qui introdurre per capire “chi” ha generato l’evento
non deve assumere classi interne e, quindi, non può basarsi sull’uguaglianza
tra puntatori.
Quando si usano classi esterne, è possibile “capire” il componente che ha
notificato l’evento associando ai diversi componenti un diverso valore della
proprietà actionCommand.
Nell’esempio nel Listato 2.16 i possibili valori per le proprietà actionCommand sono memorizzate come costanti stringa, cioè public final static,
della classe ascoltatore Listener. Ad ogni oggetto da predisporre per gestire
gli eventi azione viene associato un valore per la actionCommand presi tra
tali costanti stringa. Questo viene fatto grazie al metodo
63
2.9 Condividere gli ascoltatori per più oggetti
public class MyFrame extends JFrame
{
...
JMenuItem UpOpt = new JMenuItem("Up");
JMenuItem DownOpt = new JMenuItem("Down");
JMenuItem RandomOpt = new JMenuItem("Random");
Listener ascoltatore = new Listener();
public MyFrame()
{
...
UpOpt.addActionListener(ascoltatore);
DownOpt.addActionListener(ascoltatore);
RandomOpt.addActionListener(ascoltatore);
...
}
class Listener implements ActionListener
{
public void actionPerformed(ActionEvent e)
{
Object src = e.getSource();
if (src == UpOpt)
{ codice della voce del menu Up }
else if (src == DownOpt)
{ codice della voce del menu Down }
else if (src == RandomOpt)
{ codice della voce del menu Random }
}
}
}
Listato 2.15: Ascoltatore condiviso come classe Interna
64
2.9 Condividere gli ascoltatori per più oggetti
public class MyFrame extends JFrame {
...
JMenuItem UpOpt = new JMenuItem("Up");
JMenuItem DownOpt = new JMenuItem("Down");
JMenuItem RandomOpt = new JMenuItem("Random");
Listener ascolt = new Listener();
public MyFrame() {
...
UpOpt.addActionListener(ascolt);
UpOpt.setActionCommand(ascolt.UPOPT);
DownOpt.addActionListener(ascolt);
DownOpt.setActionCommand(ascolt.DOWNOPT);
RandomOpt.addActionListener(ascolt);
RandomOpt.setActionCommand(ascolt.RANDOMOPT)
...
}
}
Listato 2.16: Il JFrame per usare ascoltatori esterni
void setActionCommand(String);
Il metodo actionPerformed dell’actionListener legge il valore della proprietà actionCommand del componente che ha notificato l’evento e, in funzione
del valore letto, sceglie la porzione di codice da eseguire. Eventualmente è
possibile associare lo stesso actionCommand a componenti gestiti dallo stesso ascoltatore se si desidera che questi gestiscano l’evento azione nello stesso
modo. Questo cosa è molto utile quando si desidera replicare la voce di un
dato menu con un bottone da inserire nella toolbar della finestra: per fare sı̀
che sia la voce del menu che il bottone nella toolbar gestiscano l’evento nello
stesso modo è sufficiente associare ad entrambi lo stesso actionCommand.
Concludiamo il capitolo con l’ascoltatore relativo all’esempio nel Listato 2.16. Il suo scheletro è nel Listato 2.17. Si possono osservare sempre tre
parti in questo tipo di ascoltatori:
• La parte che definisce le costanti stringa (le tre dichiarazioni di oggetti
pubblici, costanti3 e statici)
3
In Java è possibile definire che il valore di un riferimento non può cambiare dichiarandolo final. Questo ovviamente non è sufficiente per dichiarare un oggetto costante. Tuttavia nel caso di oggetti String le due cose coincidono dato che gli oggetti stringa sono
immutabili: non può cambiare il riferimento, nè può cambiare il valore dell’oggetto puntato
65
2.9 Condividere gli ascoltatori per più oggetti
public class Listener implements ActionListener
{
public final static String UPOPT = "up";
public final static String DOWNOPT = "down";
public final static String RANDOMOPT = "random";
public void actionPerformed(ActionEvent e)
{
String com = e.getActionCommand();
if (com == UPOPT)
upOpt();
else if (src == DOWNOPT)
downOpt();
else if (src == RANDOMOPT)
randomOpt();
}
private void upOpt()
{ ... }
private void randomOpt()
{ ... }
private void downOpt()
{ ... }
}
Listato 2.17: Ascoltatore condiviso come classe Interna
• Il metodo actionPerformed che gestisce l’evento. Questo metodo in
questi casi non fa nient’altro che leggere il valore della proprietà actionCommand dell’oggetto che ha generato l’evento. In funzione del
valore della proprietà (e quindi dell’oggetto scatenante), viene attivata
una gestione separata che consiste nell’invocare metodi diversi.
• I diversi metodi privati che eseguono effettivamente le operazioni associate all’attivazione dei diversi controlli/widget.
66
2.10 Conclusioni
2.10
Conclusioni
Questo capitolo ha introdotto la progettazione delle Graphical User Interface
(GUI) attraverso il package Swing. Swing garantisce alta portabilità tra le
piattaforme grazie al fatto che la JVM stessa disegna le linee e colora per
creare i controlli. Questo è anche uno degli svantaggi delle Swing. Disegnare e
colorare a livello di JVM è fatto parzialmente in “modo utente” del Sistema
Operativo. Quindi, risulta meno efficiente e veloce rispetto a delegare al
Sistema operativo con delle chiamate di sistema che vengono eseguite in
“modo superuser”. Questo aspetto, per fortuna, in molti casi va in secondo
piano grazie al fatto che le moderne schede video, progettate per videogiochi,
risultano molto potenti.
67
Capitolo 3
Servlet e Java Server Pages
(JSP)
3.1
Introduzione alle Servlet
Uno dei settori sui quali si focalizza maggiormente l’attenzione dello scenario
della Information Technology è quello della programmazione “web oriented”, ovvero quella in cui la parte client è costituita da un semplice browser
che interagisce con la parte server per mezzo del protocollo HTTP. Questa
tipologia di programmi, il cui modello di funzionamento viene tipicamente
denominato Common Gateway Interface (CGI), ha l’obiettivo di permettere
l’interfacciamento da parte di un client web con una serie di risorse e di servizi
residenti sul server.
Poco prima dell’uscita definitiva del JDK 1.2, per la realizzazione di applicazioni CGI in Java, Sun ha introdotto la Servlet API, diventata in poco
tempo una delle più importanti di tutta la piattaforma Java2.
Le servlet hanno introdotto alcune importanti innovazioni nel modello
operativo del CGI, innovazioni che poi sono state adottate di riflesso sia
nelle tecnologie più obsolete, sia in quelle direttamente concorrenti.
Secondo la definizione ufficiale, una servlet è un “componente lato server
per l’estensione di web server Java enabled”, cioè un qualcosa di più generale
del solo WWW. Una servlet è quindi un programma Java in esecuzione sul
server e in grado di colloquiare con il client per mezzo del protocollo HTTP.
Tipicamente questo si traduce nella possibilità di generare dinamicamente
contenuti web da visualizzare nella finestra del client-browser. I package che
contengono tutte le classi necessarie per la programmazione delle servlet sono
il javax.servlet e il javax.servlet.http.
Quando si lavora con le servlet, è importante specificare quale versione
68
3.2 Il Ciclo di Vita di una Servlet
della API si utilizza, dato che il servlet engine utilizzato potrebbe non supportare l’ultima versione delle API.
Con la versione 2.2, le Servlet API sono entrate a far parte formalmente
della cosiddetta Java 2 Enterprise Edition, al fianco di altre importanti
tecnologie come JDBC, JNDI, JSP, EJB e RMI.
Questa nuova organizzazione logica, anche se non ha alterato l’architettura di base, ha introdotto alcune nuove definizioni e formalismi. L’innovazione più importante da questo punto di vista è l’introduzione del concetto
di “container” al posto di server: un container è un oggetto all’interno del
quale una servlet vive e nel quale trova un contesto di esecuzione personalizzato. Il concetto di container ha poi una visione più ampia dato che viene
adottato anche nel caso di JSP ed EJB.
Essendo composta al 100% da classi Java, una servlet è integrabile con il
resto della tecnologia Java dando luogo a soluzioni scalabili in maniera molto
semplice.
Ogni volta che il client esegue una richiesta di un servizio basato su CGI,
il web server deve mandare in esecuzione un processo dedicato per quel client.
Se n client effettuano la stessa richiesta, allora n processi devono essere prima
istanziati e poi eseguiti. Questo comporta un notevole dispendio di tempo e
di risorse di macchina. Il modello multithread permette a una sola servlet,
dopo la fase di inizializzazione, di servire un numero n di richieste senza la
necessità di ulteriori esecuzioni. Tutto questo porta a notevoli vantaggi dal
punto di vista della efficienza e delle potenzialità operative.
Va sottolineato il fatto che utilizzare una applicazione web basata su
servlet, pagine JSP o comunemente Java, garantisce portabilità dell’applicazione stessa, in quanto essa potrà essere spostata da una piattaforma a
un’altra in funzione delle esigenze e del carico di lavoro: tutti i sistemi
operativi moderni, infatti, hanno una JVM di ultima generazione.
3.2
Il Ciclo di Vita di una Servlet
Quando una servlet è invocata per la prima volta, il servlet container
(Tomcat) chiama il metodo init(). Tale metodo serve per inizializzare la
servlet e per il corretto funzionamento successivo, ed è qui che tipicamente si
eseguono le operazioni computazionalmente costose da effettuare una volta
per tutte (come ad esempio la connessione a un DB). Dal punto di vista dell’implementazione, per realizzare una servlet HTTP è sufficiente estendere
la classe HttpServlet, appartenente al package javax.servlet.http, e ridefinire
alcuni metodi fondamentali, uno per personalizzare l’inizializzazione della
servlet (init(), appunto!) e altri per definirne invece il comportamento in
69
3.2 Il Ciclo di Vita di una Servlet
funzione delle invocazioni del client. In Figura 3.1 è mostrata la gerarchia
delle principali classi per la gestione delle servlet. Il metodo init(), derivato
Figura 3.1: Gerarchia delle classi principali per la gestione delle Servlet
dalla classe GenericServlet, ha la seguente firma:
public void init() throws ServletException
Se durante il processo di inizializzazione si verifica un errore tale da compromettere il corretto funzionamento successivo, allora la servlet potrà segnalare
tale evento generando una eccezione di tipo ServletException. In questo caso, la servlet non verrà resa disponibile per l’invocazione e l’istanza appena
creata verrà immediatamente rilasciata. Dopo il rilascio dell’istanza fallita,
il server procederà immediatamente, alla istanziazione e inizializzazione di
una nuova servlet; la generazione di una UnavailableException permette di
specificare il tempo minimo necessario da attendere prima di intraprendere
nuovamente il processo di inizializzazione.
Dopo l’inizializzazione, una servlet si mette in attesa di una eventuale
chiamata da parte del client, che potrà essere indistintamente una GET o una
POST HTTP. La differenza fondamentale tra questi due tipi di chiamata è
che con una chiamata di tipo GET i parametri sono passati nella URL della
richiesta, separati da una “&”, ad esempio:
70
3.2 Il Ciclo di Vita di una Servlet
www.unsito.it\MyServer?a=4\&b=2
Con una chiamata di tipo POST invece i parametri vengono specificati nello
header del pacchetto HTTP. Quindi, poichè una URL è composta al più da
255 byte, c’è un limite al numero di parametri e alla loro lunghezza quando
si usa una GET. Un vantaggio invece nell’utilizzo di questo tipo di chiamata
sta nel fatto che trattandosi di una URL, una chiamata di tipo GET potrà
essere memorizzata dal browser come un bookmark (tra i preferiti).
L’interfaccia Servlet mette a disposizione il metodo service() che viene
invocato direttamente dal server in modalità multithread (il server, per ogni
invocazione del client, manda in esecuzione un metodo service in un thread
separato). In questo modo, il programmatore dovrà solo preoccuparsi di scrivere il codice relativo alle operazioni da effettuare in funzione della richiesta
di un client. La firma del metodo è la seguente:
public void service(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException
Il service viene invocato indistintamente sia nel caso di una invocazione tipo
GET che di una POST. La ridefinizione del metodo service permette di definire
il comportamento della servlet stessa. Se nella servlet è definito il metodo service(), esso è eseguito al posto dei metodi doGet() e doPost(). I due
parametri passati sono ServletRequest e ServletResponse, che permettono di
interagire con la richiesta effettuata dal client e di inviare risposta tramite
pacchetti HTTP. In seguito saranno analizzati in dettaglio gli aspetti legati
a queste due interfacce. Nel caso in cui invece si desideri implementare un
controllo più fine si possono utilizzare i due metodi doGet() e doPost(), che
risponderanno rispettivamente a una chiamata di tipo GET e a una di tipo
POST. Ad esempio, se si vuole evitare che una servlet sia invocata con una
POST, si può implementare soltanto il metodo doGet(); oppure, nel caso in cui
si voglia gestire in modo differente i due tipi di chiamata, basta implementare
in maniera appropriata i due metodi corrispondenti.
A completamento del ciclo di vita, vi è la fase di “distruzione” della
servlet. Il metodo che viene utilizzato per questo scopo è destroy(), che consente altresı̀ la terminazione del processo e il log dello status. La ridefinizione
di tale metodo, derivato dalla interfaccia Servlet, permette di specificare tutte
le operazioni simmetriche alla inizializzazione, oltre a sincronizzare e a rendere persistente lo stati della memoria. Anche la chiamata della destroy()
come quella della init() è a carico del server che ha in gestione la servlet.
La distruzione di una servlet e il relativo scaricamento dalla memoria
avvengono solamente nel momento in cui tutte le chiamate a service() da
71
3.3 Come Interagire con una Servlet: Richieste e Risposte
72
parte dei client sono state eseguite. Quando invece si verifica un errore durante il processo di inizializzazione e la servlet non può essere resa disponibile, l’istanza appena creata verrà immediatamente rilasciata, ma il metodo
destroy() in tale situazione non verrà invocato dal server.
3.3
Come Interagire con una Servlet: Richieste
e Risposte
Una servlet può comunicare con il client in maniera bidirezionale per mezzo
delle due interfacce HttpServletRequest e HttpServletResponse: la prima rappresenta la richiesta e contiene i dati provenienti dal client, mentre la seconda
rappresenta la risposta e permette di incapsulare tutto ciò che deve essere
inviato indietro al client stesso.
La Risposta: HttpServletResponse
La risposta di una servlet può essere inviata al client per mezzo di un
oggetto di tipo HttpServletResponse, che offre due metodi per inviare dati
al client: il getWriter() che restituisce un oggetto di tipo java.io.Writer, e
il getOutputStream() che, come lascia intuire il nome, restituisce un oggetto di tipo ServletOutputStream (Figura 3.2). Si deve utilizzare il primo tipo
per inviare dati di tipo testuale al client, mentre un OutputStream può essere utile per inviare anche dati in forma binaria. Da notare che la chiusura
di un Writer o di un ServletOutputStream dopo l’invio dei dati permette al
server di conoscere quando la risposta è completa. Con i metodi setContentType() e setHeader() si può stabilire il tipo MIME della pagina di risposta e,
nel caso sia HTML, avere un controllo fine su alcuni parametri. Ad esempio, il content-type può essere impostato a “text/html”, a indicare l’invio di
una pagina HTML al browser, mentre “pragma no-cache” serve per forzare il
browser a non memorizzare la pagina dalla cache.
3.3.1
Esempio Data
Vediamo un semplice esempio, in cui la servlet deve restituire una pagina
HTML in output con la data e l’ora corrente (che ovviamente cambierà ad
ogni invocazione!) e in più la data e l’ora in cui è stato invocato il metodo
init() (che sarà la stessa per ogni invocazione!). Per l’installazione e le operazioni preliminari da effettuare per poter utilizzare JBOSS, fare riferimento
all’Appendice A.1.
I Listati 3.1 e 3.2 contengono il codice della classe GeneraDataInit.
3.3 Come Interagire con una Servlet: Richieste e Risposte
import
import
import
import
javax.servlet.http.*;
javax.servlet.*;
java.io.*;
java.util.*;
public class GeneraDataInit extends HttpServlet {
Calendar dataInit;
private String convertToString (Calendar data)
{
String giornoSettimana = null;
String ilMese = null;
String giornoMese, lAnno, lOra;
String iMinuti, iSecondi, dataFinale;
int ggSettimana = data.get(Calendar.DAY_OF_WEEK);
switch (ggSettimana)
{
case 1: giornoSettimana = "domenica,Ã"; break;
case 2: giornoSettimana = "lunedi’,Ã"; break;
case 3: giornoSettimana = "martedi’,Ã"; break;
case 4: giornoSettimana = "mercoledi’,Ã"; break;
case 5: giornoSettimana = "giovedi’,Ã"; break;
case 6: giornoSettimana = "venerdi’,Ã"; break;
case 7: giornoSettimana = "sabato,Ã"; break;
}
int ggMese = data.get(Calendar.DAY_OF_MONTH);
giornoMese = ggMese+"Ã";
int mese = data.get(Calendar.MONTH);
switch (mese)
{
case 0: ilMese = "GennaioÃ"; break;
case 1: ilMese = "FebbraioÃ"; break;
case 2: ilMese = "MarzoÃ"; break;
case 3: ilMese = "AprileÃ"; break;
case 4: ilMese = "MaggioÃ"; break;
case 5: ilMese = "GiugnoÃ"; break;
case 6: ilMese = "LuglioÃ"; break;
case 7: ilMese = "AgostoÃ"; break;
case 8: ilMese = "SettembreÃ"; break;
case 9: ilMese = "OttobreÃ"; break;
case 10: ilMese = "NovembreÃ"; break;
case 11: ilMese = "DicembreÃ"; break;
}
Listato 3.1: Classe GeneraDataInit
73
3.3 Come Interagire con una Servlet: Richieste e Risposte
int anno = data.get(Calendar.YEAR);
lAnno = anno+",Ã";
int ore = data.get(Calendar.HOUR_OF_DAY);
lOra = ore+":";
int minuti = data.get(Calendar.MINUTE);
if(minuti<10)
iMinuti = "0"+minuti+":";
else
iMinuti = minuti+":";
int secondi = data.get(Calendar.SECOND);
if(secondi<10)
iSecondi = "0"+secondi+".Ã";
else
iSecondi = secondi+".Ã";
dataFinale = giornoSettimana+giornoMese
+ilMese+lAnno+lOra+iMinuti+iSecondi;
return dataFinale;
}
public void init() throws ServletException
{
dataInit = Calendar.getInstance();
}
public void service (HttpServletRequest req,
HttpServletResponse res)
throws IOException, ServletException
{
OutputStream os = res.getOutputStream();
PrintWriter out = new PrintWriter(os, true);
out.println ("<HTML><HEAD><TITLE>GeneraDataMigliore
</TITLE></HEAD>");
out.println ("<BODY>");
out.println ("Ciao,ÃlaÃdataÃeÃl’oraÃcorrentiÃsono:Ã");
Calendar rightNow = Calendar.getInstance();
out.println (convertToString(rightNow));
out.println ("<BR>");
out.println ("LaÃdataÃeÃl’oraÃinÃcuiÃe’ÃstatoÃinvocato
il metodo init sono le seguenti: ");
out.println (convertToString(dataInit));
out.println ("</BODY>");
out.println ("</HTML>");
}
}
Listato 3.2: Classe GeneraDataInit (continuo)
74
3.3 Come Interagire con una Servlet: Richieste e Risposte
Figura 3.2:
Classi per la gestione delle risposte verso il client:
ServletResponse e HTTPServletResponse
Il file web.xml relativo all’esempio è nel Listato 3.3. Si supponga di
impacchettare l’intero esempio in un archivio datainit.war. Per invocare la
servlet sarà necessario scrivere nella barra degli indirizzi del browser
http://localhost:8080/datainit/Init
La Richiesta: HttpServletRequest
La HttpServletRequest, oltre a permettere l’accesso a tutte le informazioni
relative all’header HTTP (come ad esempio i vari cookies memorizzati nella
cache del browser), permette di ricavare i parametri passati insieme all’invocazione del client. Tali parametri sono inviati come coppie nome-valore, sia
che la richiesta sia di tipo GET che POST. Ogni parametro può assumere valori multipli. L’interfaccia ServletRequest dalla quale discende la HttpServletRequest mette a disposizione alcuni metodi per ottenere i valori dei parametri
passati alla servlet: ad esempio, per ottenere il valore di un parametro dato
il nome, si può utilizzare il metodo fornito dalla interfaccia
public String getParameter(String name)
75
3.3 Come Interagire con una Servlet: Richieste e Risposte
<?xml version="1.0" encoding="UTF-8"?>
<!-- per invocarla http://localhost:8080/datainit/Init -->
<servlet>
<servlet-name>servletDataInit</servlet-name>
<servlet-class>GeneraDataInit</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDataInit</servlet-name>
<url-pattern>/Init</url-pattern>
</servlet-mapping>
</web-app>
Listato 3.3: File web.xml
Nel caso di valori multipli per la stessa variabile - come quando l’invocazione
viene fatta tramite un form HTML - a partire dalla API 2.1 si ottiene sempre
un array di stringhe, risultato analogo a quello fornito dal metodo
public String[] getParameterValues(String name)
Per ottenere invece tutti i nomi o tutti i valori dei parametri si possono
utilizzare i metodi
public Enumeration getParameterNames()
public String[] getParameterValues(String name)
Infine, nel caso di una GET http, il metodo getQueryString() restituisce una
stringa con tutti i parametri concatenati. Nel caso in cui si attendano dati
non strutturati, in forma testuale, e l’invocazione sia una POST, una PUT
o una DELETE, si può utilizzare il metodo getReader(), che restituisce un
BufferedReader. Se invece i dati inviati sono in formato binario, allora è
indicato utilizzare il metodo getInputStream(), il quale a sua volta restituisce
un oggetto di tipo ServletInputStream. Ecco di seguito un semplice esempio
public class MyServlet extends HttpServlet
{
public void service(HttpServletRequest req,
HttpServletResponse res)
throws ServletException
{
res.setContentType("text/html");
res.setHeader("Pragma", "no-cache");
res.writeHeaders();
String param = req.getParameter("parametro");
76
3.3 Come Interagire con una Servlet: Richieste e Risposte
// esegue programma di servizio
OutputStream out = res.getOutputStream();
// usa out per scrivere sulla pagina di risposta
}
}
Da notare l’utilizzo dello statement
OutputStream out = res.getOutputStream();
impiegato per ricavare lo stream di output con il quale inviare dati (ad
esempio codice HTML) al client.
3.3.2
Esempio Rubrica
In questo esempio vediamo come, a partire da una form HTML, possiamo richiedere a un server il numero di telefono corrispondete al nome
della persona specificata (attraverso la form, appunto). La form è quella
in Figura 3.3 e il codice HTML relativo è nel Listato 3.4. La pressione
del bottone di submit della form deve invocare una servlet codificata nella classe Rubrica (vedi Listati 3.6 e 3.7). L’ultimo punto da analizzare
è come configurare l’applicazione in modo tale che la pressione del bottone di submit invochi direttamente la servlet rubrica. Analizzando il file
web.xml(Listato 3.8) si osserva che la servlet è invocabile mediante l’URL
http://localhost:8080/nome war/rubrica. Ciò vuol dire che l’elemento form della pagina HTML dovrà specificare come action rubrica, ovvero
l’URL relativo della servlet. Si osservi inoltre il nome dei campi di input
della form: il campo di testo si chiama “Nome” e quindi alla servlet Rubrica verrà passato un parametro Nome il cui valore è il contenuto del campo
di testo. Inoltre esistono tre checkbox. Il comportamento delle checkbox
nel passaggio di parametri alla servlet è leggermente diverso: solo se una
checkbox è selezionata allora viene passato come parametro il nome della
checkbox (e valore quello specificato nell’attributo value). Nel caso specifico,
ad esempio, se la checkbox relativa al numero di casa è selezionata, allora
alla servlet verrà passato il parametro Casa con valore “ON”. Se la stessa non è selezionata, il parametro non verrà passato e quindi l’invocazione
dell’istruzione getParameter(‘‘Casa’’) restituirà null.
La classe fa uso dei servigi della classe Numero descritta nel Listato 3.5
che permette l’accesso al repository dei numeri di telefoni. Si osservi come
l’unico costruttore della classe sia privato a sottolineare che gli oggetti di tale
classe possono essere creati solamente attraverso il metodo static ottieniNumero(String abbonato). Per semplicità il repository è implementato come un
file di testo Rubrica.txt una cui instanza è rappresentata in Figura 3.3.
77
3.3 Come Interagire con una Servlet: Richieste e Risposte
<html>
<head>
<title>Nome</title>
</head>
<body>
<form method="GET" action="rubrica">
<p>Nome:
<input type="text" name="Nome" size="20">
</p>
<p>Tipo: <input type="checkbox" name="Casa" value="ON">
Casa
<input type="checkbox" name="Cellulare" value="ON">
Cellulare
<input type="checkbox" name="Ufficio" value="ON">
Ufficio
</p>
<p><input type="submit" value="Invia">
<input type="reset" value="Reimposta"></p>
</form>
</body>
</html>
Listato 3.4: Il file HTML della form
78
3.3 Come Interagire con una Servlet: Richieste e Risposte
import java.io.*;
public class Numero {
public static String nome;
public String casa;
public String ufficio;
public String cellulare;
private Numero() {}
public static Numero ottieniNumeri (String unNome)
throws IOException
{
FileReader in = new FileReader ("C:\\Rubrica.txt");
BufferedReader riga = new BufferedReader (in);
nome = riga.readLine();
while(!nome.equals(unNome) && nome!=null)
{
riga.readLine();
riga.readLine();
riga.readLine();
nome = riga.readLine();
}
if (nome.equals(unNome))
{
Numero n = new Numero();
n.casa = riga.readLine();
n.ufficio = riga.readLine();
n.cell = riga.readLine();
return (n);
}
else
return null;
}
}
Listato 3.5: Classe Numero
79
3.3 Come Interagire con una Servlet: Richieste e Risposte
import javax.servlet.http.*;
import javax.servlet.*;
import java.io.*;
public class Rubrica extends HttpServlet {
public void service (HttpServletRequest req,
HttpServletResponse resp)
throws ServletException,IOException
{
String nome=req.getParameter("Nome");
boolean casa=(req.getParameter("Casa")==null);
boolean ufficio=(req.getParameter("Ufficio")==null);
boolean cell=(req.getParameter("Cellulare")==null);
OutputStream os = resp.getOutputStream();
PrintWriter out = new PrintWriter(os, true);
out.println ("<HTML><HEAD><TITLE>Esempio2</TITLE>
</HEAD>");
out.println ("<BODY>");
out.println ("Ciao,ÃsonoÃstatiÃrichiestiÃi
seguenti numeri di telefono di ");
out.println ("<B>");
out.println (nome);
out.println ("</B>");
out.println (":");
out.println ("<BR>");
Numero unNumero = Numero.ottieniNumeri(nome);
Listato 3.6: Classe Rubrica
80
3.3 Come Interagire con una Servlet: Richieste e Risposte
if(!casa)
{
out.println ("<P>");
out.println ("<I>");
out.println ("Casa:");
out.println ("</I>");
out.println (unNumero.casa);
out.println ("</P>");
}
if(!ufficio)
{
out.println ("<P>");
out.println ("<I>");
out.println ("Ufficio:");
out.println ("</I>");
out.println (unNumero.ufficio);
out.println ("</P>");
}
if(!cell)
{
out.println ("<P>");
out.println ("<I>");
out.println ("Cellulare:");
out.println ("</I>");
out.println (unNumero.cell);
out.println ("</P>");
}
out.println ("</BODY>");
out.println ("</HTML>");
}
}
Listato 3.7: Classe Rubrica
81
3.4 Le Sessioni
82
Figura 3.3: La Form HTML
I numeri di telefono vanno memorizzati su un documento di testo, come
mostrato in Figura 3.4.
3.4
Le Sessioni
Il fatto che più esecuzioni di una stessa servlet facciano capo allo stesso oggetto permette di condividere proprietà e metodi della classe e, in definitiva,
l’informazione. In pratica ciò si traduce nell’istanziare nel metodo init delle
variabili di classe, che saranno successivamente visibili nelle varie esecuzioni
dei metodi doPost e doGet (o service) della nostra servlet.
La necessità di passare informazioni tra le pagine Web è particolarmente
sentita tra chi sviluppa procedure Intranet / Extranet, nelle quali, una volta
identificato l’utente, si permette che esso compia una serie di operazioni che
non si concludono necessariamente in una pagina; è cosı̀ necessario tenere
traccia delle operazioni, fornire un ambiente che si adatti all’utente stesso
secondo le sue preferenze e diritti di accesso; questo si traduce tecnicamente
nel mantenimento dell’indentità dell’utente e dello stato attraverso la richiesta di più pagine. Viene cosı̀ ad identificarsi un concetto analogo a quanto
noto da anni nei mainframe e nei DBMS, cioè la sessione. L’uso di variabili
3.4 Le Sessioni
83
Figura 3.4: Rubrica.txt
<?xml version="1.0" encoding="UTF-8"?>
<servlet>
<servlet-name>servletRubrica</servlet-name>
<servlet-class>Rubrica</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletRubrica</servlet-name>
<url-pattern>/rubrica</url-pattern>
</servlet-mapping>
</web-app>
Listato 3.8: File web.xml
di classe in questa ottica non è più sufficiente, in quanto esse sono visibili
da qualunque client invochi quella servlet, noi invece vogliamo tenere traccia
di una informazione specifica per quella servlet chiamata da quel particolare
client. Le Servlet API forniscono una serie di strumenti per fare tutto ciò in
maniera efficace e rapida, dunque in pieno stile Java.
Cosa accade quando si crea una nuova sessione? Il server deve predisporre
l’area della memoria che conterrà l’informazione e fare in modo di “marcare”
il browser dell’utente in modo da poter successivamente associare ogni richiesta di quel client con quella determinata sessione; in pratica viene registrato
un cookie sul browser con un numero di identificazione della sessione, il Session Id. Molte persone si preoccupano per i cookie e per la scarsa sicurezza
che offrono in quanto l’utente un po’ smaliziato può divertirsi a modificarli
3.4 Le Sessioni
e creare problemi all’applicazione; nel caso delle sessioni in Java ciò non è
possibile in quanto l’informazione continua a risiedere là dove è più al sicuro,
cioè sul server.
Per gestire le sessioni in Java, il package javax.servlet.http mette a disposizione tre classi: HttpSession, HttpSessionBindingListener e HttpSessionContext. Una volta inizializzata correttamente la servlet, sarà possibile ottenere
nel metodo doGet, doPost o service un riferimento all’oggetto HttpSession
tramite una chiamata al metodo getSession di HttpServletRequest:
HttpSession session = request.getSession();
La servlet cerca di identificare il client (nel nostro caso il browser) che
la chiama; se esiste già un oggetto HttpSession per quel client session, allora
riferirà a quello, altrimenti ne verrà creato uno nuovo.
Riassumendo, ecco cosa accade:
1. Il browser richiede la servlet (ad es.: localhost/servlet/MyServlet)
2. La servlet controlla se il browser richiedente possiede un cookie, se lo
possiede verifica se esso punta ad una sessione valida. Altrimenti crea
una nuova sessione e ne registra l’identificativo in un cookie (generato
da una Servlet che gira col web server)
3. Una volta ottenuto l’oggetto session è possibile usarlo per memorizzare qualsiasi tipo di informazione, basta usare gli opportuni metodi
di HttpSession. Ad esempio, per tenere un semplice contatore delle
volte che viene chiamata quella servlet da quel determinato client, sarà
sufficiente:
// Tento di ricavare dalla sessione
// il valore di sessiontest.counter
Integer ival =
(Integer) session.getValue("sessiontest.counter");
if (ival==null)
// ival nonè definito...
ival = new Integer(1);
else
// il valore di ivalè definito, lo
// incremento di uno
ival = new Integer(ival.intValue() + 1);
// Memorizzo nella sessione il nuovo valore
session.putValue("sessiontest.counter", ival);
84
3.4 Le Sessioni
Questo esempio è tratto dal tutorial del JSDK 2.0, per il codice completo
potete fare riferimento a quello [3].
Una sessione rimane valida finché non viene invalidata esplicitamente mediante il metodo invalidate, oppure supera un determinato tempo di inattività. Il time-out può essere settato a proprio piacimento, attraverso il metodo
setMaxInactiveInterval.
Le sessioni sono un meccanismo intrinseco del web server; ciò significa
anche che non tutti i Web Server che permettono di usare servlet permettono
anche di usare le sessioni, inoltre si potrebbero sperimentare alcune differenze
di comportamento tra un Web Server e l’altro.
Il cookie presenta numerosi vantaggi: è molto elegante perché il tutto
avviene di nascosto, senza annoiare l’utente con URL complesse, inoltre, anche se il browser viene chiuso, il cookie viene mantenuto in modo da poter
riprendere il lavoro da dove l’avevamo lasciato. Se però in una logica Intranet
è abbastanza plausibile aspettarsi che tutti i browser supportino i cookie o
comunque siano configurati per usarli, una volta nel mare di Internet questa
certezza non c’è più; in effetti molti utenti preferiscono disabilitare i cookie per motivi di sicurezza. Un aspetto particolarmente interessante delle
HttpSession è che queste si preoccupano per noi di verificare se sono uitilizzabili i cookie sul client (browser), e di eseguire le opportune contromosse in
caso negativo. In pratica, invece di usare i cookie, viene aggiunto in coda
alla query string il Session ID, cosicché l’URL prende questo forma:
http://localhost/servlet/MyServlet?jrunsessionid
=912773795120311538
L’unica accortezza che dobbiamo usare nello scrivere il codice è di generare le URL attraverso il metodo HttpServletResponse.encodeUrl, in modo che
venga inserita nella pagina HTML la chiamata opportuna per entrambe le
situazioni.
3.4.1
Esempio Login
Il seguente esempio mostra l’uso delle sessioni per realizzare il login. Il client
inizia invocando la servlet codificata nella classe HomeServlet (Listato 3.9) che
è accedibile dal client inserendo nella barra degli indirizzi del browser l’indirizzo http://localhost:8080/sessioni/start (ovviamente supponendo che
il deploy sia stato fatto mediante il file sessioni.war). Tale servlet legge
dalla sessione l’attributo “login” che, nell’esempio, è predisposto per memorizzare l’account dell’utente che si è loggato. Se non esiste nella tabella delle
sessioni nessun valore per quello attributo, allora significa che l’utente non si
è ancora loggato. In tal caso la servlet ridirezione il browser dell’utente sulla
85
3.4 Le Sessioni
86
import javax.servlet.http.*;
import javax.servlet.*;
import java.io.*;
public class HomeServlet extends HttpServlet {
public void service (HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
HttpSession s = req.getSession(true);
Object value = s.getAttribute("login");
if(value==null)
{
resp.sendRedirect("login.htm");
}
else
resp.sendRedirect("homepage.htm");
}
}
Listato 3.9: Classe HomeServlet
pagina login, altrimenti il browser viene ridirezionato alla pagina principale.
La ridirezione viene fatta con il metodo HttpServletResponse.sendRedict(String
url).
La Figura 3.5 contiene la pagina del Login con un unico campo di testo
il cui nome è Nome. La pressione del bottone di Submit invoca la servlet
LoginServlet, passando ovviamente come parametro il nome dell’utente che
desidera loggarsi. La servlet è molto semplice: prende il parametro Nome ed
aggiunge l’attributo “login” alla tabella delle sessioni. Quindi effettua la redirezione alla homepage. La successiva volta in cui l’utente invoca la servlet
all’indirizzo http://localhost:8080/sessioni/start, l’output verrà automaticamente redirezionato alla pagina homepage. Questo perchè troverà
nella tabella delle sessioni un valore impostato per l’attributo “login”.
3.4 Le Sessioni
87
Figura 3.5: La pagina per il login
import javax.servlet.http.*;
import javax.servlet.*;
import java.io.*;
public class LoginServlet extends HttpServlet
{
public void service (HttpServletRequest req,
HttpServletResponse resp)
throws ServletException, IOException
{
String nome = req.getParameter("Nome");
HttpSession s = req.getSession(true);
s.setAttribute("login", nome);
resp.sendRedirect("homepage.htm");
}
}
Listato 3.10: Classe LoginServlet
3.5 Le Java Server Pages (JSP)
<?xml version="1.0" encoding="UTF-8"?>
<servlet>
<servlet-name>homesessioni</servlet-name>
<servlet-class>HomeServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>homesessioni</servlet-name>
<url-pattern>/start</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>loginsessioni</servlet-name>
<servlet-class>LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>loginsessioni</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
</web-app>
Listato 3.11: File web.xml
3.5
Le Java Server Pages (JSP)
Per venire in contro all’esigenza di una maggiore interazione tra client e server
e per fornire all’utente contenuti più vari e meno statici sono state introdotte
svariate tecnologie, che introducono elementi dinamici nei siti web.
Il supporto di queste tecnologie ha comportato modifiche ai vari elementi coinvolti nell’interazione client/server: estensione delle funzionalità del
client (web browser), estensione delle funzionalità del server ed estensione
del linguaggio HTML.
Nuove versioni dei browser e dei server si sono unite alla realizzazione
di moduli aggiuntivi, detti plugin, che vengono collegati, tramite opportune
API, con il browser o con il server web.
É possibile stilare una classificazione sulla base di dove viene eseguito il
codice che compie le elaborazioni che rendono l funzionamento dinamico.
Tecnologie lato client (client side) la dinamicità e l’interazione con l’utente sono gestite direttamente da codice eseguito dal client o da un
suo plugin. É il caso delle Applet Java.
Tecnologie lato server (server side) il client ha un ruolo passivo ed è il
server, o un suo plugin, a gestire a parte dinamica. Il caso più tipico
88
3.5 Le Java Server Pages (JSP)
è la comunicazione client/server per mezzo del protocollo HTTP, supportata in HTML dai form e dai suoi componenti. Attraverso questi,
l’utente può inserire dati, mandarli al server e ottenerne una risposta.
Il browser manda questi dati sotto forma di una richiesta HTTP, utilizzando uno dei metodi previsti dal protocollo (in genere get o post)
a cui il server fa seguire una appropriata risposta. Queste tecnologie
devono innanzitutto fornire al programmatore i mezzi per scrivere sul
lato server il codice che elabora le risposte: in Java, troviamo i Servlet
e le Java Server Pages.
Approcci misti presentano elementi dinamici sia sul lato server che sul lato
client, realizzati mediante l’interazione dei rispettivi plugin (lato server
e lato client). Rientra in questa categoria l’uso congiunto di Applet e
Servlet Java.
Tra questi diversi approcci, assume particolare rilevanza quelo delle tecnologie server side, basate sul protocollo HTTP. Sono infatti quelle che più
si prestano allo sviluppo di applicazioni web centrate sulla gestione di basi
di dati.
Le tecnologie Java, nate per ultime, hanno usufruito delle precedenti idee
ed esperienze, apportando però anche significativi miglioramenti. L’uso di
Servlet infatti ha apportato il vantaggio di avere un minore impiego di risorse,
dato che tutti i Servlet sono eseguiti dalla stessa Java virtual machine, messa
in esecuzione dal server, come thread distinti e non come processi distinti. E proprio come i Servlet, JSP regala gli stessi vantaggi legati all’uso di
Java, e una reale portabilità in tutte le piattaforme in cui esista una implementazione della macchina virtuale Java (quindi, praticamente tutte quelle
comunemente usate). In tutte le tecnologie server side, si è detto, si ha
la generazione di pagine web come risultato di una determinata richiesta da
parte del client. La pagina non esiste come risorsa statica, ma viene generata
dinamicamente. Il problema sta nel fatto che chiamare funzioni o metodi di
output che scrivano letteralmente codice HTML può portare alla generazione
di continui errori. Con le Java Server Pages l’intero processo di generazione
del codice, compilazione ed esecuzione è gestito automaticamente in modo
trasparente. JSP ha anche la peculiarità di permettere l’inserimento diretto
di codice Java nella pagina. Ma entriamo più nel dettaglio.
Da un punto di vista funzionale, una pagina JSP si può considerare come
un file di testo scritto secondo le regole di un markup language in base al
quale il contenuto del file viene elaborato da un JSP container (ovvero, un
software per l’elaborazione di pagine JSP), per restituire il risultato di una
trasformazione del testo originale, secondo le istruzioni inserite nel testo. Si
89
3.5 Le Java Server Pages (JSP)
tratta quindi di pagine web a contenuto dinamico generato al momento in
cui la pagina viene richiesta dal client.
Una pagina JSP contiene essenzialmente due tipi di testo:
template text ovvero testo “letterale” destinato a rimanere tale dopo l’elaborazione della pagina;
JSP text porzioni di testo che vengono interpretate ed elaborate dal JSP
container (Tomcat).
Nella maggior parte dei casi, il template text è in formato HTML (o a
volte XML), dato che le JSP sono pagine web.
Dal punto di vista del programmatore Java, una JSP viene vista come un
modo particolare per interfacciarsi a oggetti Java. Il JSP container infatti
converte la pagina JSP in un Servlet, generando prima il codice sorgente, poi
compilandolo.
Vediamo un semplice (anzi, banale!) esempio di pagina JSP:
<html>
<head>
<title>Data e ora</title>
</head>
<body>
<p>
Data e ora corrente:<br>
<b><%= new java.util.Date()%></b>
</p>
</body>
</html>
Si tratta, come si può vedere, di una normale pagina HTML, con un
solo elemento estraneo a questo linguaggio <%= new java.util.Date()%>.
Si tratta di una espressione JSP in linguaggio Java che verrà interpretata
e valutata dal JSP container e sostituita dalla data e ora corrente. Quindi
con new java.util.Date() viene creato un oggetto rappresentante l’istante
attuale. Giacchè è richiesta una rappresentazione stringa, allora l’oggetto è
convertito in una stringa1
Finora si sono visti soltanto alcuni degli elementi di una pagina JSP: il
template text e le espressioni. Ora verrà fatta una breve descrizione di tutti
gli elementi:
1
Per essere precisi, la conversione in Stringa è ottenuta automaticamente invocando per l’oggetto il metodo ereditato da Object toString(), in questo caso sovrascritto per
rappresentare la data in forma di stringa.
90
3.5 Le Java Server Pages (JSP)
91
Template Text tutte le parti di testo che non sono definite come elementi
JSP; vengono copiate tali e quali nella pagina di risposta.
Comment con sintassi: <%- - comment - -%>; sono commenti che riguardano
la pagina JSP in quanto tale, e pertanto vengono eliminati dal JSP
container nella fase di traduzione-compilazione; da non confondere con
i commenti HTML e XML, che vengono inclusi nella risposta come
normale template text.
Directive con sintassi: <%@ directive ...%>; sono direttive di carattere generale, indipendenti dal contenuto specifico della pagina, relative alla
fase di traduzione-compilazione.
Action seguono la sintassi dei tag XML ossia <tag attributes> body </tag>
oppure <tag attributes/> dove attributes sono nella forma attr1 = value1
attr2 = value2 ...; le azioni sono eseguite nella fase di processing, e danno
origine a codice Java specifico per la loro esecuzione.
Sripting element sono porzioni di codice in uno scripting language specificato nelle direttive; il linguaggio di default è lo stesso Java; si dividono
in tre sottotipi:
Scriplet con sintassi: <% code %>; sono porzioni di codice nello
scripting language che danno origine a porzioni di codice Java;
generalmente inserite nel metodo service() del Servlet; se contengono dichiarazioni di variabile, queste saranno variabili locali
valide solo nell’ambito di una singola esecuzione del Servlet; se il
codice contiene istruzioni che scrivono sullo stream di output, il
contenuto mandato in output sarà inserito nela pagina di risposta
nella stessa posizione in cui si trova lo scriptlet.
Declaration con sintassi <%! declaration [declaration] ...%>; sono
dichiarazioni che vengono inserite nel Servlet come elementi della
classe, al di fuori di qualunque metodo. Possono essere sia variabili
di classe che metodi.
Expression con sintassi <%= expression %>; contengono un’espressione che segue le regole dele espressioni dello scripting language;
l’espressione viene valutata e scritta nella pagina di risposta nella
posizione corrispondente a quella dell’espressione JSP.
Torniamo all’esempio della pagina JSP per vedere come essa viene elaborata dal JSP container. In primo luogo, viene generato il codice che definisce
una classe Java; questa classe è una Servlet, e il codice generato sarà il
seguente:
3.5 Le Java Server Pages (JSP)
class JSPDataOra extends HttpJspServlet
{
public void _jspService (HttpServletRequest request,
HttpServletResponse response)
{
...
PrintWriter out = response.getWriter();
out.println("<html>");
out.println();
out.println("<head>");
out.println("<title>DataÃeÃOra</title>");
out.println("</head>");
out.println();
out.println("<body>");
out.println("<p>");
out.println("DataÃeÃoraÃcorrente:<br>");
out.println("<b>");
out.println(new java.util.Date());
out.println("</b>");
out.println("</p>");
out.println("</body>");
out.println();
out.println("</html>");
...
}
}
La classe è derivata da una ipotetica classe HttpJspServlet, che potrà essere una sottoclasse di HttpServlet e dovrà implementare l’interfaccia
HttpJspPage, definita nella JSP API. Questa interfaccia contiene il metodo
jspService(), corrispondente al metodo service() del Servlet generico.
Il Servlet non fa altro che rimandare in output le parti del file che non
contengono tag JSP come string literal mentre l’espressione JSP viene eseguita
e valutata prima di essere scritta nello stream.
Prescindendo dai dettagli implementativi, l’esecuzione di una pagina JSP
prevede due fasi distinte:
1. fase di traduzione-compilazione, durante la quale viene costruito un
oggetto costruibile dalla VM (in genere un Servlet) in grado di elaborare
una risposta alle richieste implicite o esplicite contenute nella pagina;
92
3.5 Le Java Server Pages (JSP)
2. fase di processing, in cui viene mandato in esecuzione il codice generato
e viene effettivamente elaborata e restituita la risposta.
Il Servlet manipola una serie di oggetti per svolgere il suo lavoro, principalmente i due oggetti ServletRequest e ServletResponse, su cui si basa tutto
il meccanismo di funzionamento. Quindi, per poter usufruire dei servizi del
Servlet nella pagina JSP, occorre avere un modo per accedere a questi oggetti: a questo scopo esistono gli oggetti impliciti JSP, utilizzabili in tutti gli
scriptlet. Eccone un elenco.
Request Corrisponde generalmente all’oggetto ServletRequest passato come
parametro al metodo service(); può essere una qualunque sottoclasse di
ServletRequest; generalmente si tratta di una sottoclasse di
HttpServletRequest.
Response Corrisponde all’oggetto ServletResponse passato come parametro
al metodo service() del Servlet; può essere una qualunque sottoclasse di
ServletResponse; generalmente si tratta di una sottoclasse di
HttpServletResponse.
Out Poichè il container deve fornire un meccanismo di buffering, non è dato
accesso direttamente all’oggetto PrintWriter
restituito da
response.getWriter(). L’oggetto out è invece uno stream bufferizzato
di tipo javax.servlet.jsp.JspWriter, restituito da un metodo del PageContext. Il trasferimento sullo output stream del ServletResponse avviene
in un secondo tempo, dopo che tutti i dati sono stati scritti sull’oggetto
out.
Page É un riferimento all’oggetto che gestisce la richiesta corrente, che si
è vista essere generalmente un Servlet. In Java corrisponde al this
dell’oggetto, pertanto è di scarsa utilità. Si presume risulti utile con
altri linguaggi di script.
PageContext Si tratta di un oggetto della classe javax.servlet.jsp.PageContext
utilizzata prevalentemente per incapsulare oggetti e features particolari di ciascuna implementazione del container. Il PageContext viene
utilizzato anche per condividere oggetti tra diversi elementi.
Session Corrisponde all’oggetto HttpSession del Servlet, viene restituito da
un metodo del PageContext.
93
3.5 Le Java Server Pages (JSP)
3.5.1
Esempio Lista Prodotti (carrello)
Questo esempio mostra l’utilizzo di JSP e i vantaggi rispetto alle servlet
quando occorre creare dinamicamente pagine Web complesse in cui la maggior parte del contenuto è statico. L’esempio mostra una bozza di un possibile
sito per l’acquisto di prodotti on-line. Tutte le pagine JSP fanno uso della
classe Prodotto che forniscono due metodi statici per ottenere la lista di tutti
i prodotti acquistabili e per ottenere le informazioni di un prodotto attraverso il nome. Per semplicità l’esempio assume che non possano esistere due
prodotti con lo stesso nome e quindi il nome può essere preso come “chiave”.
Il repository è implementato come un file di testo.
L’interazione inizia dalla pagina JSP nel Listato 3.14 (vedi Figura 3.6)
dove vengono visualizzate la lista dei prodotti esistenti. Si osservi come la
tabella inserita in una form sia generata dinamicamente. Anche lo stesso
campo di testo è generato dinamicamente con il nome uguale al prodotto che
rappresenta.
L’utente inserisce nei campi di testo le quantità desiderate dei singoli
prodotti e preme il bottone di submit. A questo i parametri delle form
(cioè il nome dei prodotti con le loro quantità) sono passati alla pagina JSP
addCarrello.jsp (vedi Listato 3.15 e Figura 3.7). La seconda pagina JSP
utilizza l’attributo “Carrello” della sessione per memorizzare una Hashtable
che contiene le associazioni tra le chiavi (i nomi dei prodotti) e i valori (le
quantità nel carrello). La pagina JSP carrello.jsp mostra il resoconto dei
prodotti nel carrello. Essa fa uso dell’attributo “Carrello” della sessione per
ottenere le informazioni sui prodotti nel carrello.
94
3.5 Le Java Server Pages (JSP)
package exampleJSP;
import java.io.*;
import java.util.*;
public class Prodotto
{
private String nome;
private float prezzo;
private String marca;
private Prodotto () {}
public String getNome ()
{
return (nome);
}
public float getPrezzo ()
{
return (prezzo);
}
public String getMarca ()
{
return (marca);
}
public static Vector listaProdotti()
{/*Per ogni prodotto crea un oggetto
Prodotto ed aggiungilo al vector*/
Vector lista = new Vector();
String nome;
try
{
BufferedReader riga = new BufferedReader
(new FileReader ("C:\\ListaProdotti.txt"));
nome = riga.readLine();
while(nome!=null)
{
Prodotto p = new Prodotto();
p.nome = nome;
p.marca = riga.readLine();
p.prezzo = Float.parseFloat(riga.readLine());
lista.addElement(p);
nome = riga.readLine();
}
}
Listato 3.12: Classe Prodotto
95
3.5 Le Java Server Pages (JSP)
catch (IOException err)
{
return null;
}
return(lista);
}
public static Prodotto getProdottoByName(String unNome)
{
try
{
String nome;
FileReader in =
new FileReader ("C:\\ListaProdotti.txt");
BufferedReader riga = new BufferedReader (in);
nome = riga.readLine();
while(!nome.equals(unNome) && nome!=null)
{
riga.readLine();
riga.readLine();
nome = riga.readLine();
}
if (nome.equals(unNome))
{
Prodotto p = new Prodotto();
p.marca = riga.readLine();
p.prezzo = Float.parseFloat(riga.readLine());
p.nome = nome;
return (p);
}
else
return null;
}
catch (IOException err)
{
return null;
}
}
}
Listato 3.13: Classe Prodotto (continuo)
96
3.5 Le Java Server Pages (JSP)
<html>
<%@ page import = "java.util.*,ÃexampleJSP.*" %>
<%
Vector listaProdotti=Prodotto.listaProdotti();
%>
...
<body>...<form method="GET" action="addCarrello.jsp">
<table border="1" width="100%" id="table1" height="172">
<tr>
<td align="center" height="20">
<b><font size="2">Nome</font></b>
</td>
<td align="center" height="20">
<b><font size="2">Marca</font></b>
</td>
<td align="center" height="20">
<b><font size="2">Prezzo</font></b>
</td>
<td align="center" width="148" height="20">
<b><font size="2">àQuantit</font></b>
</td>
</tr>
<% int size=listaProdotti.size();
for (int i=0;i<size;i++)
{
Prodotto p=(Prodotto)listaProdotti.get(i);
%>
<tr>
<td><%= p.getNome() %></td>
<td><% out.print(p.getMarca()); %></td>
<td><% out.print("EuroÃ"+p.getPrezzo()); %></td>
<td>
<p><input type="text"
name="<%=Ãp.getNome()Ã%>"
size="20" value="0"></p>
</td>
</tr><% } %>
</table>
<p align="center"><button>AGGIUNGI</button></p>
</form>
...
</body></html>
Listato 3.14: homepage.jsp
97
3.5 Le Java Server Pages (JSP)
Figura 3.6: La Homepage
Figura 3.7: La pagina Aggiunti al Carrello
98
3.5 Le Java Server Pages (JSP)
<html>
<%@ page import = "java.util.*,ÃexampleJSP.*" %>
...
<body>
<p align="center"><i>
<font size="4">
Sono stati aggiunti al carrello i seguenti prodotti:
</font></i></p>
<% Enumeration e = request.getParameterNames();
int counter = 0;
Hashtable carrello =
(Hashtable)session.getAttribute("Carrello");
if(carrello==null)
{
carrello = new Hashtable();
session.setAttribute("Carrello", carrello);
}
while(e.hasMoreElements())
{
String nome = (String)e.nextElement();
int quantita =
Integer.parseInt(request.getParameter(nome));
%>
<ul>
<%
if(quantita!=0)
{
out.println("<li>"+quantita+"Ã"+nome+"</li>");
counter = counter+1;
Integer q = (Integer)carrello.get(nome);
int v=0;
if (q!=null)
v=q.intValue();
Integer vquantita = new Integer(v+quantita);
carrello.put(nome, vquantita);
}
%>
</ul>
<% }
if(counter==0)
out.println("NonÃe’ÃstatoÃselezionatoÃalcunÃprodotto!");
%>
...</body></html>
Listato 3.15: addCarrello.jsp
99
3.5 Le Java Server Pages (JSP)
<html>
<%@ page import = "java.util.*,ÃexampleJSP.*" %>
...
<body>
<font size="4">
Il carrello contiene attualmente i seguenti prodotti:</font>
<table border="1" width="100%" id="table1">
<tr>
<td align="center">
<b><font size="2">Nome</font></b>
</td>
<td align="center">
<b><font size="2">Marca</font></b>
</td>
<td align="center">
<b><font size="2">Prezzo unitario</font></b>
</td>
<td align="center">
<b><font size="2">Quantita</font></b>
</td>
</tr>
<%
Prodotto prodotto;
Hashtable carrello =
(Hashtable)session.getAttribute("Carrello");
Enumeration enum = carrello.keys();
while(enum.hasMoreElements())
{
String nome = (String)enum.nextElement();
prodotto = Prodotto.getProdottoByName(nome);
out.println("<tr><td>");
out.println(nome);
out.println(prodotto.getMarca());
out.println("</td><td>");
out.println(prodotto.getPrezzo());
out.println("</td><td>");
out.println(carrello.get(nome));
out.println("</td>");
out.println("</tr>");
} %>
</table>
...</body></html>
Listato 3.16: carrello.jsp
100
3.5 Le Java Server Pages (JSP)
Figura 3.8: La pagina del Carrello
101
Capitolo 4
JMS: Enterprise Messaging (ed
introduzione su MOM)
Figura 4.1: Schematizzazione di un Message Oriented Middleware
102
4.1 Introduzione al MOM (richiami)
4.1
103
Introduzione al MOM (richiami)
Con il termine messaging si definisce un meccanismo che permette la comunicazione asincrona tra client (detti peer-element) mediante scambio di
messaggi caratterizzato da uno scarso grado di accoppiamento. Data questa definizione si può considerare messaging system un qualsiasi sistema che
scambi pacchetti TCP/IP tra programmi client. Piuttosto che creare un meccanismo di messaging specifico per ogni situazione che richieda lo scambio di
dati, diversi produttori hanno creato un sistema che permette di scambiare
messaggi tra applicazioni diverse in modo generico. Questi sistemi sono chiamati Message Oriented Middleware (MOM). Tramite l’infrastruttura middleware del MOM (Figura 4.1), processi residenti su macchine diverse possono
interagire tra loro mediante l’invio e ricezione di messaggi tipicamente asincroni. È importante notare che si è in presenza di un sistema di messaging
peer-to-peer dove i client interagiscono tra loro non in modo diretto, bensı̀
mediante un messaging server, che fa le veci del “postino”: riceve i messaggi
dai mittenti (“produttori”) e li recapita ai relativi destinatari (“consumatori”). Grazie alla mediazione del messaging server, i client risultano essere
disaccoppiati, cioè non devono sapere nulla l’uno dell’altro. Il disaccoppiamento nei MOM è una caratteristica molto importante ed è maggiore rispetto
ai sistemi middleware basati su architettura ORB (Object Request Broker).
Si pensi ad esempio a RMI o CORBA: per poter comunicare con un
oggetto remoto, un’applicazione deve essere a conoscenza dei suoi metodi,
in modo da poterli invocare. Nei sistemi MOM, i client non interagiscono
direttamente tra di loro. Inoltre né il produttore né il ricevitore devono essere
a conoscenza l’uno dell’altro. L’unica cosa che entrambi devono sapere è il
formato del messaggio e la “casella di posta” (destination) da usare. Altra
importante caratteristica offerta dai sistemi MOM è il concetto di Guaranteed
Message Delivery (GMD) che garantisce la consegna del messaggio. Al fine
di prevenire una perdita di informazioni in caso di crash del message server, i
messaggi devono essere resi persistenti (per esempio mediante JDBC) prima
di essere recapitati ai consumatori.
4.1.1
Modelli di Messaging
I Message-oriented middleware defiscono due diversi paradigmi di scambio
di messaggi (Figura 4.2): point-to-point e publish and subscribe. Il modello
di messaging point-to-point (p2p) è caratterizzato dal fatto che più produttori e più consumatori possono condividere la stessa destinazione (queue) e
ogni messaggio ha un solo consumatore; questo permette di fatto una comunicazione uno-a-uno. Il modello publish/subscribe (pub/sub) permette
4.1 Introduzione al MOM (richiami)
104
Figura 4.2: I Modelli di Messaging supportati da JMS
una comunicazione uno-a-molti dal momento che ogni messaggio inserito nella destinazione (topic) può essere inoltrato a tutti i destinatari che si sono
dichiarati (subscription) interessati.
Point-to-Point Nel modello p2p un client può mandare uno o più messaggi a un altro client.La comunicazione non avviene in modo diretto,
bensı̀ condividendo una stessa destinazione verso cui si inviano e da
cui si prelevano i messaggi. La destinazione nel modello point-to-point
prende il nome di queue (“coda”). Più produttori e più consumatori possono condividere la stessa coda ma ogni messaggio ha un solo
consumatore; questo permette di fatto una comunicazione uno a uno.
Non ci sono dipendenze temporali tra sender e receiver del messaggio e
quindi il ricevente può ricevere il messaggio anche se non era in ascolto
sulla coda al momento dell’invio. Il messaggio viene tolto dalla coda
una volta che il ricevente l’ha prelevato confermando (acknowledge) la
ricezione al produttore. L’utilizzo del messaging p2p è da utilizzare nel
caso si abbia l’esigenza che ogni messaggio spedito sia ricevuto da un
solo consumatore che ne confermi l’avvenuta ricezione.
PUB/SUB Nel modello publish/subscribe (detto anche sistema di messag-
4.2 Introduzione a JMS
105
ing event-driven), i client inviano messaggi a un topic, una coda “speciale” in grado di inoltrare i messaggi a più destinatari. Il sistema si
prende carico di distribuire i messaggi inviati (publish) da più publishers: tali messaggi sono automaticamente ricevuti da tutti i client che
hanno effettuato la sottoscrizione (subscribe), cioè che si sono dichiarati
interessati alla ricezione. Il modello pub/sub permette quindi una comunicazione uno-a-molti visto che ogni messaggio può avere più consumatori. È il MOM che si occupa di recapitare una copia di ogni
messaggio pubblicato agli appropriati destinatari. Il messaggio una
volta consumato da tutti i subscriber interessati viene tolto dal topic.
I client possono essere dinamicamente publisher oppure subscriber o
anche entrambi. Esiste una dipendenza temporale tra publisher e subscriber, visto che il receiver può “consumare” il messaggio solo dopo
essersi “sottoscritto” (subscription) al topic d’interesse, una volta che
il client abbia effettivamente inviato il messaggio. Inoltre il receiver deve rimanere attivo, se vuole continuare a ricevere. Molti MOM
(tra cui JBOSS) rilassano tale vincolo temporale mediante il concetto
di durable subscriptions. Le durable subscriptions infatti permettono
di ricevere i messaggi anche se i subscribers non erano in ascolto al
momento dell’invio del messaggio. L’utilizzo di messaging publish/subscribe è da preferire quando si ha la necessità che ogni messaggio
sia ricevuto da più consumatori. Le destinazioni sono accessibili in
modo concorrente e si possono definire anche dei filtri per esprimere le
condizioni da rispettare affinché un messaggio venga recapitato a una
certa destinazione.
4.2
Introduzione a JMS
Java Message Service (JMS) è un insieme di API Java che permette di creare,
spedire, ricevere e leggere messaggi. JMS è stato sviluppato da Sun insieme ai
maggior produttori di sistemi per definire un’interfaccia comune indipendente
dalla specifica implementazione del sistema di messaging, in modo analogo
a quanto avviene con JDBC, JNDI, JCA e cosı̀ via. L’applicazione Java JMS
risulta cosı̀ essere indipendente, oltre che dal sistema operativo, anche dalla
specifica implementazione del sistema di messaging. Mediante JMS è quindi
possibile interagire con sistemi MOM esistenti quali MQSeries di IBM, Fiorano MQ, Microsoft MQ. Si capisce come lo scarso disaccoppiamento rispetto
ai sistemi RPC, l’intrinseca asincronia delle operazioni di messaging, la sua
integrazione in ambito J2EE (i Message Driven Bean) e l’indipendenza dello
specifico provider rendono JMS una tecnologia potente, flessibile e fondamen-
4.2 Introduzione a JMS
106
Figura 4.3: Gli Attori JMS
tale soprattutto per interazioni business-to-business (B2B) e integrazioni di
sistemi eterogenei in ambito EAI (Enterprise Application Integration). In
un’applicazione JMS gli attori coinvolti sono ConnectionFactory, Destination, Connection, Session, MessageProducer, Message e MessageConsumer,
come rappresentato in Figura 4.3. Di seguito viene descritto brevemente il
compito di ciascun attore JMS.
Administered Objects
Le Destination (queue/topic) e le ConnectionFactory sono dette
administered objects perché incapsulano le specifiche implementazioni dei
provider JMS. Gli administered objects sono vendor-dependent, cioè la loro
specifica implementazione varia da provider a provider e per questo motivo
non sono gestiti all’interno del programma Java. Le interfacce JMS permettono allo sviluppatore Java di astrarsi dai dettagli implementativi della
specifica versione di MOM, permettendo il riutilizzo del codice al variare dell’implementazione JMS, rispettando appieno la filosofia Java. La creazione
di ConnectionFactory e di Destination è compito dell’amministratore JMS
che deve inserirli all’interno di un contesto JNDI in un opportuno namespace in modo che possano essere recuperati da qualsiasi client mediante le
normali API JNDI. Per inviare messaggi (sender/publisher) o per riceverli
(receiver/subscriber), un’applicazione JMS client utilizza i servizi JNDI per
4.2 Introduzione a JMS
107
ottenere un oggetto di tipo ConnectionFactory e uno o più oggetti di tipo
Destination (Queue o Topic).
Connection Factory
È l’administered object utilizzato dal client per ottenere una connessione con
il message server. Nel caso point-to-point si utilizza l’interfaccia QueueConnectionFactory:
Context ctx = new InitialContext();
QueueConnectionFactoryqueueConnectionFactory =
(QueueConnectionFactory)ctx.
lookup("MyQueueConnectionFactory");
mentre nel caso di publish/subscribe si utilizza l’interfaccia TopicConnectionFactory:
TopicConnectionFactory topicConnectionFactory =
(TopicConnectionFactory)ctx.
lookup("MyTopicConnectionFactory");
Destination
È l’administered object che rappresenta l’astrazione di una particolare destinazione. Anche in questo caso il client identifica la destinazione mediante l’utilizzo delle API JNDI. Nel caso point-to-point, la destinazione è
rappresentata dall’interfaccia Queue:
Queue queue = (Queue) jndiContext.lookup("MyQueue");
mentre, nel caso di publish/subscribe, si usa l’interfaccia Topic:
Topic topic = (Topic) jndiContext.lookup("MyTopic");
Connection
Rappresenta l’astrazione di una connessione attiva con un particolare JMS
provider e si ottiene dall’oggetto ConnectionFactory. Nel caso point-to-point,
si ottiene un reference d’interfaccia QueueConnection invocando il metodo
createQueueConnection sull’oggetto Connection Factory:
Analogamente, nel caso di publish/subscribe, si ottiene un reference d’interfaccia TopicConnection invocando il metodo createTopicConnection() sull’oggetto ConnectionFactory:
4.2 Introduzione a JMS
108
TopicConnection topicConnection =
topicConnectionFactory.createTopicConnection();
Session
L’interfaccia Session si ottiene dall’oggetto Connection e rappresenta il canale
di comunicazione con una destinazione: permette la creazione sia di produttori che di consumatori di messaggi. La creazione della sessione permette di specificare il controllo della transazione e la modalità di acknowledge. Nel caso point-to-point, la Session è identificata mediante l’interfaccia
QueueSession:
QueueSession queueSession =
queueConnection.
createQueueSession(false,Session.AUTO_ACKNOWLEDGE);
Nel caso di publish/subscribe, la Session viene identificata tramite interfaccia TopicSession:
TopicSession topicSession =
topicConnection.
createTopicSession(false,Session.AUTO_ACKNOWLEDGE);
Messages
I messaggi JMS sono costituiti da tre parti: un’intestazione (message header ),
una serie di campi contenenti proprietà (property fields) e il corpo (message
body).
Il message header è costituito esattamente da 10 campi obbligatoriamente
presenti. Tali campi sono valorizzati dal provider o dal client al fine di identificare, configurare e inoltrare il messaggio. Tra questi campi è presente l’identificatore univoco del pacchetto (JMSMessageID), il nome della Queue o
Topic a cui il pacchetto è destinato(JMSDestination),la priorità (JMSPriority),
il timestamp del tempo d’inoltro(JMSTimestamp), il flag indicante se il messaggio è stato re-inoltrato(JMSRedelivered), l’ID per correlare i messaggi JMS
tra di loro (JMSCorrelationID), la modalità di invio(JMSdeliveryMode), la destinazione a cui inoltrare il messaggio di risposta(JMSReplyTo). I campi JMSReplyTo e JMSCorrelationID e il campo opzionale JMSType sono assegnati
dall’applicazione JMS in modo esplicito sull’oggetto Message; tutti gli altri, invece, dal provider JMS. I property fields sono coppie di nome e valore e
sono utili per costruire “filtri” a livello applicativo. La caratteristica di questi
campi è che possono essere esaminati mediante i message selectors. Quando
4.2 Introduzione a JMS
109
un consumatore si connette al server può definire un message selector il quale
esamina l’header e i property fields (non il body message) del pacchetto JMS
di una specifica destinazione sia essa una Queue o un Topic. Questo permette alle applicazioni JMS di utilizzare i message selector per specificare
quali messaggi è interessata a ricevere utilizzando una sintassi condizionale
sottoinsieme del linguaggio SQL-92. Il filtraggio avviene a livello di server e
permette di inoltrare ai client lungo la rete i messaggi strettamente necessari
o utili, risparmiando cosı̀ la banda del canale. Un message selector non è altro che un oggetto di classe String che può contenere boolean literals(TRUE,
true, FALSE e false), operatori logici (NOT, AND, OR), aritmetici (+, -, *,
/), di comparazione( =, >, >=, <, <=, < >) di ricerca (IS, LIKE, IN, NOT
IN) e cosı̀ via. Esempi di message selector:
"squadra = ’Pittsburgh Steelers’"
"squadra <> ’Dallas Cowboys’"
"age NOT BETWEEN 20 and 32"
"type = ’FIAT’ AND color = ’blue’ AND weight >= 100"
"NewsType = ’Opinion’ OR NewsType = ’Sports’"
I property field possono essere impostati mediante la classe di metodi
setXXXProperty(String name, XXX value) dove XXX possono essere Boolean,
Byte, Short, Integer, Long, Float, Double e String e rappresenta il tipo del campo proprietà.
Ovviamente esistono i getter corrispondenti
XXX getXXXProperty(String name) che restituiscono i valori delle proprietà
a partire dal loro nome. Infine le proprietà possono essere cancellate mediante il metodo clearProperties(), lasciando il messaggio JMS senza campi
proprietà.
Body Message
I messaggi JMS possono essere di varie tipi, ognuno dei quali mette a disposizione gli appositi metodi get e set. JMS non supporta uno specifico body
per messaggi XML; in questi casi è utilizzabile il text message. I messaggi
vengono creati mediante i metodi dell’interfaccia Session dalla quale estendono sia l’interfaccia QueueSession che TopicSession. L’interfaccia mette a
disposizione i metodi di creazione relativi a ogni tipologia di message: createTextMessage, createObjectMessage, createMapMessage, createBytesMessage e
createStreamMessage, prevedendo per ognuno di essi la versione overloaded
che contempla come parametro in ingresso un oggetto di inizializzazione.
Per creare messaggi, nel caso di tipologia point-to-point è sufficiente invocare il metodo create opportuno sulla QueueSession, mentre nel caso di pub-
4.2 Introduzione a JMS
110
lish/subscribe sull’oggetto TopicConnection come mostrato negli esempi che
seguono:
TextMessage txtMsg = queueSession.createTextMessage();
txtMsg.setText("HelloÃJMSÃPTPÃWorld!");
TextMessage txtMsg2 =
queueSession.createTextMessage("HelloÃJMSÃPTPÃWorld!");
ObjectMessage objMsg = queueSession.createObjectMessage();
objMsg.setObject(my_ptp_order);
ObjectMessage objMsg2 = topicSession.createObjectMessage();
ObjMsg2.setObject(my_pubSub_chat_msg);
MessageProducer
Il MessageProducer è l’oggetto che permette l’invio dei messaggi verso una
particolare destinazione. Utilizzando gli oggetti Session e Destination creati,
si possono inizializzare uno o più message producer. Nel caso point-to-point,
il MessageProducer per inviare messaggi sulla Queue è referenziato mediante
l’interfaccia QueueSender:
QueueSender queueSender = queueSession.createSender(queue);
mentre nel caso di publish/subscribe l’invio dei messaggi verso il Topic può
avvenire mediante l’interfaccia TopicPublisher:
TopicPublisher topicPublisher =
topicSession.createPublisher(topic);
Una volta creato il produttore, basta invocare il metodo send sull’oggetto QueueSender nel caso PTP, mentre nel caso pub/sub, il metodo publish(String message) va invocato sull’oggetto topicPublisher; entrambi i metodi
send richiedono in ingresso un parametro d’interfaccia Message e presentano
firme overloaded.
queueSender.send(message);
topicPublisher.publish(message);
MessageConsumer
Il MessageConsumer rappresenta un oggetto in grado di ricevere messaggi.
Analogamente a quanto avviene per il MessageProducer, bisogna indicare
l’oggetto Session e la Destination d’interesse per inizializzare correttamente il
MessageConsumer.
4.2 Introduzione a JMS
111
public class TextListener implements MessageListener
{
public void onMessage(Message message)
{
try
{
if (message instanceof TextMessage)
{
TextMessage txtMsg = (TextMessage) message;
System.out.
println("Ricevuto:Ã" + txtMsg.getText());
}
}
catch(JMSException err)
{
System.err.println("ErroreÃJMS:"+err.getMessage());
}
}
}
Listato 4.1: Esempio di Consumatore Asincrono
QueueReceiver queueReceiver = queueSession.createReceiver(queue);
TopicSubscriber topicSubscriber =
topicSession.createSubscriber(topic);
Nella modalità asincrona, il MessageProducer (sender o publisher) non
è tenuto ad attendere che il messaggio sia ricevuto per proseguire nel suo
funzionamento, e l’elaborazione del messaggio stesso non è necessariamente
sequenziale. Il MessageConsumer è in grado di ricevere i messaggi in modo
asincrono definendo un message listener : si tratta di una classe che implementa l’interfaccia javax.jms.MessageListener. Ogni qualvolta un messaggio
arriva alla destinazione d’interesse viene automaticamente invocato il metodo
di callback onMessage().
Il Listato 4.1 mostra l’implementazione di un consumatore asincrono:
TextListener è una normale classe Java che implementa l’interfaccia JMS
MessageListener ridefinendo perciò il metodo onMessage(). In tale metodo
si mette la logica applicativa per gestire il contenuto del messaggio ricevuto;
si effettua il controllo del tipo del messaggio ricevuto e, se è del tipo corretto,
se ne elabora il contenuto.
4.2 Introduzione a JMS
112
Per associare il consumatore alla coda opportuna occorre creare un oggetto d’interfaccia javax.jms.MessageListener:
TopicListener topicListener = new TextListener();
QueueListener queueListener = new TextListener();
e passarlo come argomento al metodi setMessageListener dell’oggetto Receiver
topicSubscriber.setMessageListener(topicListener);
queueReceiver.setMessageListener(queueListener);
Connesso il listener, ci si pone in ascolto, in attesa di ricevere ed elaborare i
messaggi, invocando il metodo start sull’oggetto Connection:
queueConnection.start();
topicConnection.start();
I prodotti di messaging sono intrinsecamente asincroni ma è bene specificare che un MessageConsumer può ricevere i messaggi anche in modo sincrono. Nella ricezione dei messaggi in modo sincrono il consumatore richiede
esplicitamente alla destinazione di prelevare il messaggio (fetch) invocando il
metodo receive.
Il metodo receive() appartiene all’interfaccia
javax.jms.MessageConsumer (dalla quale estendono sia QueueReceiver che TopicReceiver) ed è sospensivo, cioè rimane bloccato fino alla ricezione del messaggio, a meno che non si espliciti un timeout finito il quale il metodo
termina:
public Message receive() throws JMSException
public Message receive(long timeout) throws JMSException
Esempio di ricezione sincrona:
while(<condition>)
{
Message m = queueReceiver.receive(1000);
if( (m!=null)&&(m instanceof TextMessage)
{
message = (TextMessage) m;
System.out.println("Rx:" + message.getText());
}
}
Finite le operazioni di gestione del messaggio ricevuto, si esegue il cosiddetto codice di “pulizia” chiudendo la connessione JMS mediante il metodo
close.
4.2 Introduzione a JMS
queueConnection.close();
topicConnection.close();
113
Capitolo 5
Enterprise Java Bean (EJB)
Gli Enterprise Java Bean (EJB) sono componenti di un’architettura lato
server utilizzati per semplificare il processo di sviluppo delle applicazioni a
componenti in Java. Infatti, usando gli EJB è possibile produrre facilmente
e velocemente applicazioni server-side e applicazioni scalabili per sistemi
distribuiti.
Il vantaggio di poter disporre di tali componenti risiede nel fatto che
in questo modo è possibile concentrarsi meglio nella scrittura del codice
necessario per la propria applicazione e quindi nella realizzazione di una
applicazione server robusta piuttosto che sullo sviluppo del middleware.
5.1
Introduzione agli EJB
Essendo gli Enterprise JavaBeans (EJB) componenti standard della tecnologia server-side in Java è possibile utilizzarli su qualsiasi tipologia di applicazione server. Per tale motivo c’è stata una notevole diffusione nell’utilizzo
degli EJB, si è cercato quindi di facilitarne la comprensione e l’apprendimento, ma soprattutto si è resa particolarmente efficiente la loro portabilità a
seguito della pubblicazione delle specifiche.
Una critica che si può fare verso tali componenti è che questi devono
essere scritti esclusivamente in linguaggio java. Questa, che apparentemente
potrebbe sembrare una restrizione, in realtà è compensata dal fatto che Java
è un linguaggio ideale nella costruzione di componenti, per diverse ragioni
tra cui:
Separazione interfaccia/implementazione: Secondo il paradigma MVC,
la separazione tra interfaccia e implementazione consente maggiore
modularità. Lo strato di presentation è separato, dunque posso aggiornare il mio portale senza aggiornare la logica o viceversa. Gli EJB
114
5.1 Introduzione agli EJB
115
non sono come i componenti GUI (Graphical User Interface), cioè non
sono visibili agli utenti, anzi agiscono dietro di essi ed eseguono il vero
lavoro. La differenza principale tra GUI ed EJB sta nel loro dominio
applicativo. I GUI sono componenti lato client, cioè agiscono direttamente per il client creando interfacce grafiche o piccole operazioni
logiche. Gli EJB, d’altra parte, sono componenti lato server, effettuano
operazioni server come eseguire complessi algoritmi o eseguire un gran
numero di transazioni; i loro componenti hanno bisogno di essere sempre in esecuzione e forniscono inoltre un sicuro ambiente multiutente
transazionale. In un paradigma MVC (Model View Controller), attraverso gli EJB viene implementato lo strato di logica applicativa, cioè
le applicazioni vere e proprie che realizzano le funzionalità del sistema.
Per questo gli EJB non prevedono interazione diretta con i client. Per
accedere agli EJB vengono utilizzati i GUI (che implementano la parte
del Controller).
Sicurezza: L’architettura Java è molto più sicura dei tradizionali linguaggi
di programmazione sotto diversi punti di vista. Infatti con il linguaggio
Java si hanno perdite di dati in memoria meno frequenti, maggior robustezza. Per esempio, se durante una comunicazione cade un Thread
realizzato in linguaggio Java, l’applicazione continua la sua esecuzione.
Inoltre Java comprende una ricca serie di librerie, che permette agli
sviluppatori di non reinventare codice riducendo il rischio di creare
errori.
Portabilità: Java gira su qualsiasi piattaforma. Questo significa che gli EJB
non hanno problemi di portabilità, e quindi possono essere utilizzati
senza alcun problema anche da coloro che hanno investito su diverse
piattaforme (UNIX, win32 e mainframes).
Generalmente gli EJB sono utilizzati in modo specifico per aiutare a risolvere business problems. Per tale ragione essi possono eseguire qualunque
dei seguenti task:
Eseguire business logic: Cioè implementare la logica di un’applicazione
server side, per esempio attraverso l’utilizzo della JavaMail API, gli
EJB possono essere utilizzati ad esempio per elaborare il pagamento
delle tasse di un’attività, per verificare i permessi degli utenti che approvano degli ordini di acquisto o mandano una conferma di un’ordine..
Accedere ad un database: L’accesso al database avviene attraverso l’uso
delle Java Database Connectivity (JDBC) API. Tale accesso può essere
5.2 Fondamenti degli EJB
116
richiesto per esempio per immettere un ordine di alcuni libri, trasferire
del denaro tra due conti bancari.
Accedere ad un altro sistema: Gli EJB ottengono questa integrazione
con il Java Connector Architecture (JCA). Un esempio di tale integrazione può essere il collegamento ad un sistema COBOL che elabora
il fattore di rischio per una nuova assicurazione.
5.2
Fondamenti degli EJB
Un Enterprise Bean può essere composto da uno o più oggetti Java, in quanto
un componente potrebbe essere composto da più di un oggetto semplice. La
tecnologia EJB si basa su altre due tecnologie: java RMI-IIOP e JNDI. I
client di un EJB possono riferirsi ad esso senza preoccuparsi di come sia
composto, cioè può essere utilizzato come una scatola nera. In particolare
il funzionamento avviene nel seguente modo: il client del bean utilizza una
semplice interfaccia pubblica la quale deve contenere le stesse specifiche del
bean; tali specifiche obbligano il bean ad avere alcuni metodi definiti, i quali
permettono all’EJB Container di maneggiare i bean senza preoccuparsi di
quale sia in esecuzione. Un EJB è sempre contenuto in un EJB Container. I
Container amministrano i componenti in modo da ottimizzare l’uso di risorse
(es. pooling). Un contenitore EJB è responsabile per mediare l’accesso di un
EJB alle risorse.
Chiunque può chiamare un enterprise bean, una servlet, un’applet o un’altro bean. Quest’ultimo caso può dar vita ad una lunga catena di chiamate.
Tale compito, notevolmente complesso se realizzato manualmente dal programma seguendo tutte le chiamate, può essere suddiviso utilizzando una
varietà di bean già scritti che gestiscono i sotto compiti.
Esistono 3 tipi diversi di EJB:
Session Beans. Rappresentano le sessioni di interazione con i client. Possono eseguire qualunque tipo di azione, dal sommare i numeri ad accedere ad un database o chiamare un altro Bean. I Session Bean hanno
la caratteristica di essere transienti: il loro stato non viene memorizzato
in modo persistente in un database o simili.
Entity Beans. Sono i dati di un’applicazione, ne rappresentano gli oggetti
di business, (es. prodotti agli ordini, numeri di carta di credito di un’applicazione di eCommerce). Tipicamente gli entity bean sono gestiti dai
session bean per raggiungere un obiettivo. Sono memorizzati in modo
5.2 Fondamenti degli EJB
117
Figura 5.1: Catena di invocazioni da applicazioni Swing
persistente spesso, un’istanza di entity Bean corrisponde ad una tupla
di un DB.
Message-Driven Beans. I Message-Driven Beans sono simili ai session
bean. La differenza principale riguarda l’attivazione, in particolare
essi possono essere attivati solo tramite l’invio di messaggi, infatti i
Message-Driven Beans restano in ascolto su un topic/coda JMS e si attivano solo quando un messaggio è inviato a quel topic/coda. Possono
essere utilizzati per chiamare un altro enterprise beans.
Un aspetto rilevante di tale tecnologia è dato dalla possibilità di incrociare le chiamate tra bean (Figura 5.1) ovvero quando un client chiama in
modo sincrono un bean, quest’ultimo potrebbe aver bisogno di alcuni dati,
per eseguire i propri metodi, che sono reperibili solo attraverso un’ulteriore
chiamata ad un entity bean. La stessa modalità di incrociare le chiamate
può avvenire anche con l’uso dei message-driven bean, i quali possono essere
attivati a seguito della ricezione di un messaggio da un client su una coda/topic JMS, e come i session bean possono invocare successivamente gli entity
bean per eseguire l’applicazione.
Queste azioni possono essere eseguite anche attraverso browser internet
grazie alle JSP/Servlet, i quali possono invocare i session bean o i message
bean. Queste azioni sono mostrate in Figura 5.2
La tecnologia EJB, come l’RMI, è basata su oggetti distribuiti. La Figura 5.3 mostra come avviene il collegamento tra un client ed un oggetto
distribuito, in particolare, essa mostra come:
1. Il client chiama lo stub, che è un oggetto proxy client-side. Lo stub è
responsabile di mascherare la comunicazione sulla rete dal client, attraverso l’uso delle sockets, e quando necessario, attraverso la conver-
5.2 Fondamenti degli EJB
118
Figura 5.2: Catena di invocazioni da pagine Internet tramite JSP/Servlet
Figura 5.3: Chiamata di un oggetto distribuito
5.3 Scheletro di un Enterprise Bean
119
sione dei parametri nella rappresentazione della rete, lo stub comunica
attraverso la rete.
2. Lo stub chiama lo skeleton, il quale è un oggetto proxy server-side.
Lo skeleton maschera le comunicazioni sulla rete dell’oggetto distribuito, riceve una chiamata dalla socket e converte i parametri ricevuti in
un’opportuna rappresentazione Java.
3. Lo skeleton passa la chiamata all’oggetto distribuito, il quale esegue dei
compiti. Una volta eseguiti tali compiti, tale oggetto passa di nuovo
il controllo allo skeleton, il quale successivamente passa la chiamata
dapprima allo stub e poi di nuovo al client.
Un punto chiave è che sia lo stub che l’oggetto distribuito implementano
la stessa interfaccia (Remote Object), questo significa che lo stub clona la
segnalazione dei metodi, e il client che invoca i metodi sullo stub crede in
realtà di eseguire i metodi dell’oggetto distribuito.
Generalmente gli oggetti distribuiti sono complessi da costruire interamente, per tale motivo ci sarà bisogno dei servizi Middleware, come per
esempio quelli per la gestione delle transazioni e della sicurezza. Ci sono due
diversi tipi di Middleware: Esplicito e Implicito.
La differenza principale tra l’una e l’altra tipologia risiede nella quantità
di codice da scrivere; infatti, nel primo caso bisogna invocare direttamente i
middleware API necessari al raggiungimento dell’obiettivo, nel secondo caso,
invece, la richiesta dei servizi viene delegata ad un Request Interceptor che
provvederà a chiamare i servizi middleware richiesti. Ciò avviene attraverso
la creazione di uno speciale file descrittore.
5.3
Scheletro di un Enterprise Bean
Un Enterprise Bean non è un file monolitico, ma è costituito da un’insieme
di file che lavorano per creare il bean.
5.3.1
Enterprise Bean Class
La prima parte del bean è costituita dall’implementazione vera e propria, il
vero oggetto distribuito, cioè quel file che contiene i metodi della logica di
business, chiamato Enterprise Bean Class. Questa è una classe Java conforme
ad una interfaccia well-defined e obbedisce a regole certe che permettono al
bean di poter essere eseguito in ogni Container.
5.3 Scheletro di un Enterprise Bean
120
public interface javax.ejb.EnterpriseBean extends Serializable
{
}
Listato 5.1: L’interfaccia javax.ejb.EnterpriseBean
Le specifiche dell’EJB definiscono poche interfacce standard che il bean
può implementare, le quali forzano il bean a fornire dei metodi, come definito
dal modello dei componenti EJB. L’EJB Container chiama questi metodi per
allertare o informare il bean di alcuni eventi significativi.
L’interfaccia base che tutti i bean devono implementare è la javax.ejb.EnterpriseBean, mostrata nel Listato 5.1
Questa interfaccia contrassegna il bean come un vero enterprise bean
class. L’aspetto interessante di questa interfaccia è che estende java.io.Serializable, ciò significa che tutti gli enterprise bean possono essere convertiti in
un flusso di byte e condividere tutte le proprietà degli oggetti serializabili.
Session Beans, Entity Beans e Message-driven Beans hanno tutti un’interfaccia che estende la javax.ejb.EnterpriseBean. Tutti i Session Bean devono implementare la javax.ejb.SessionBean; tutti gli Entity Bean devono
implementare la javax.ejb.EntityBean; tutti i Message-driven Bean devono
implementare la javax.ejb.MessageDriverBean. Quindi il bean non deve implementare direttamente la javax.ejb.EnterpriseBean, ma piuttosto implementare
l’interfaccia corrispondente al proprio tipo.
5.3.2
L’EJB Object
Quando un client vuole usare un’istanza dell’enterprise class, non può invocare mai direttamente l’istanza. Piuttosto l’invocazione viene intercettata
dall’EJB Container e in seguito delegata all’istanza del bean. Questo è il concetto del request interceptor accennato in precedenza. Grazie ad esso, l’EJB
Container può eseguire automaticamente il middleware implicito. Tale modo
di operare facilita lo sviluppo perchè non si deve scrivere, correggere o mantenere il codice che chiama i middleware API, quindi, il container agisce come
un livello intermedio tra il cliente e il bean. Questo livello viene chiamato
EJBObject. L’EJBObject viene generato automaticamente dal container in
quanto cambia per ogni bean
Remote Interface
Il client invoca i metodi sull’EJBObject, piuttosto che sul bean, per tale
motivo l’EJBObject deve clonare tutti i metodi che il bean espone. Questo
5.3 Scheletro di un Enterprise Bean
121
public interface javax.ejb.EJBObject extends java.rmi.remote {
public javax.ejb.EJBHome getEJBHome()
throws java.rmi.RemoteException;
public java.lang.Object getPrimaryKey()
throws java.rmi.RemoteException;
public void remove()
throws java.rmi.RemoteException,
javax.ejb.RemoveException;
public javax.ejb.Handle getHandle()
throws java.rmi.RemoteException;
public boolean isIdentical(javax.ejb.EJBObject)
throws java.rmi.RemoteException;
}
Listato 5.2: L’interfaccia javax.ejb.EJBObjeect
avviene attraverso un’interfaccia speciale: la Remote Interface.
Questa interfaccia deve essere conforme alle regole definite dalle specifiche EJB; per esempio: tutte le remote interface devono derivare da una
interfaccia comune, cioè dalla javax.ejb.EJBObject descritta nel Listato 5.2
Questi metodi verranno implementati dal container e non dal client; la
remote interface estende anche java.rmi.Remote, ciò fornisce la possibilità di
chiamare tale oggetto da un’altra Java Virtual Machine. La remote interface
deve essere conforme alla convenzione RMI-IIOP dei parametri passati.
5.3.3
L’Home Object
Si è visto che il client invoca direttamente l’EJBObject invece del bean, in
realtà ciò può avvenire solo se il client ha modo di avere il riferimento a
questo oggetto. In particolare, il client ottiene il riferimento attraverso una
fabbrica di EJBObject: l’Home Object. Quest’oggetto è responsabile della
creazione, ricerca e rimozione di un EJBObject. Come l’EJBObject, l’home
object è specifico per ogni bean. La differenza è che per ogni bean si ha
un’unica istanza dell’Home Object, invece gli EJB Object sono tanti quanti
sono i client che richiedono l’uso di quel bean.
5.3 Scheletro di un Enterprise Bean
122
public interface javax.ejb.EJBHome extends java.rmi.remote {
public EJBMetaData getEJBMetaData()
throws java.rmi.RemoteException;
public void remove(javax.ejb.Handle)
throws java.rmi.RemoteException,
javax.ejb.RemoveException;
public javax.ejb.HomeHandle getHomeHandle()
throws java.rmi.RemoteException;
public void remove(Object)
throws java.rmi.RemoteException,
javax.ejb.RemoveException;
}
Listato 5.3: L’interfaccia javax.ejb.EJBHome
Home Interface
L’Home Interface permette al client di inizializzare correttamente un EJBObject. Per esempio se un EJBObject ha due costruttori, uno che prende
in ingresso un intero e l’altro che prende in ingresso una stringa, tramite
l’home interface il client sceglie quale costruttore usare, e come ritorno si ha
il puntatore all’EJBObject mascherato dalla remote interface.
Come per la remote interface, l’home interface deve estendere l’interfaccia
javax.ejb.EJBHome illustrata nel Listato 5.3
L’home object deriva dalla java.rmi.Remote, il che significa che può
essere invocato attraverso la rete. Inoltre, i tipi di parametri passati nei
metodi della home interface devono essere tipi validi per la Java RMI-IIOP.
5.3.4
Le interfacce locali
Un problema rilevante che si riscontra con l’utilizzo delle home interface e
delle remote interface è nella lentezza della loro creazione. Di seguito sono
riportati tutti i passi che devono essere eseguiti al fine di creare una home
interface:
1. Il client chiama lo stub locale.
2. Lo stub converte i parametri nel formato adatto alla rete.
5.3 Scheletro di un Enterprise Bean
123
3. Lo stub si connette attraverso la rete allo skeleton.
4. Lo skeleton riconverte i parametri nel formato adatto a Java.
5. Lo skeleton chiama l’EJB object
6. L’EJB object esegue i middleware necessari
7. Una volta che l’EJB object chiama l’istanza dell’enterprise bean e il
bean esegue il suo lavoro, ogni passo precedente deve essere ripetuto
per ritornare al client.
Come si può notare è un percorso piuttosto laborioso; per tale motivo un
modo più efficiente per invocare un EJB è riferirsi alle interfacce locali: local
object invece che EJB object e local interface al posto della remote interface.
In tal caso il processo di chiamata attraverso le interfacce locali sarà:
1. Il client chiama un local object.
2. Il local object esegue le operazioni necessarie.
3. Una volta che l’enterprise bean ha finito il suo lavoro restituisce il
controllo al local object che lo restituisce al client.
E’ evidente che con tale approccio si salta tutta la parte relativa alla rete,
dalle chiamate allo stub e allo skeleton fino al marshalling/unmarshalling dei
parametri.
Le local interface estendono la javax.ejb.EJBLocalObject mentre le home
interface estendono la javax.ejb.EJBLocalHome. Il codice di queste classi è
mostrato nel Listato 5.4
Lo svantaggio nell’utilizzo di tali interfacce locali sta nel fatto che esse
non estendono le classi RMI e quindi, per tale motivo, non possono essere
chiamate da client remoti ma solo da bean presenti nello stesso processo,
inoltre, il passaggio dei parametri non avviene per valore ma per riferimento
e ciò comporta un cambio nella semantica dell’applicazione client.
5.3.5
Deployment Descriptor
Per comunicare al container quali middleware utilizzare si devono dichiarare
i servizi che il componente richiede in un deployment descriptor file.
5.3 Scheletro di un Enterprise Bean
public interface javax.ejb.EJBLocalObject {
public javax.ejb.EJBLocalHome getEJBLocalHome()
throws javax.ejb.EJBException;
public java.lang.Object getPrimaryKey()
throws javax.ejb.EJBException;
public void remove()
throws javax.ejb.EJBException;
javax.ejb.RemoveException;
public boolean isIdentical(javax.ejb.EJBLocalObject)
throws javax.ejb.EJBException;
}
public interface java.ejb.EJBLocalHome {
public void remove/java.lang.Object)
throws javax.ejb.EJBException;
javax.ejb.RemoveException;
}
Listato 5.4: Interfacce locali
124
5.4 Session Bean
5.4
125
Session Bean
Un Session Bean rappresenta una sessione di interazione con i client. Esso
implementa la logica di business, le regole di business e gli algoritmi. E’
un componente riusabile che contiene i processi della logica di business. Un
Session Bean rappresenta un singolo client nell’Application Server. Il client
accede al Business Layer di una applicazione invocando i metodi dei Session
Beans. I Session Beans nascondono la complessità della logica di business
dietro a chiamate semplici.
Un’importante differenza tra Entity Bean e Session Bean sta nel fatto
che i session bean sono componenti a vita breve, mentre gli entity bean
possono vivere per anni perche’ sono oggetti persistenti e fanno parte di
sistemi di salvataggio permanente (come ad esempio i database). La vita di
un session bean dura il tempo dell’esecuzione dei metodi richiesti dal client.
Generalmente la lunghezza della sessione del client determina il tempo in
cui il session bean sara’ in uso. L’EJBContainer ha l’autorità di distruggere
i session bean se il client va in time out. Tipicamente i session bean non
sopravvivono ai crash dell’applicazione e sicuramente non sopravvivono ai
crash della macchina.
I session bean sono non persistenti, cio’ significa che non vengono salvati
su un archivio fisico. Essi possono eseguire azioni su database ma non possono
essere salvati su di esso.
Tutti gli enterprise bean ottengono una conversazione con i client. Una
conversazione è un’interazione tra client e bean, ed è composta da una serie
di metodi chiamati tra il client e il bean. Esistono due tipi di session bean:
stateful session bean e stateless session bean, ognuno dei quali modella un
tipo diverso di conversazione.
Stateless Session Bean
Le conversazioni di alcuni processi si limitano semplicemente ad una sola
richiesta. Un singolo processo non ha bisogno di mantenere uno stato tra
le diverse invocazioni dei metodi. Dopo ogni invocazione, il container può
decidere di distruggere il bean, ricrearlo o pulirlo di tutte le informazioni
contenute su di esso nelle passate invocazioni.
Un tipico esempio di stateless session bean è un motore ad alte prestazioni
che risolve complesse operazioni matematiche, come la compressione o la decompressione di dati audio o video. Dal momento che i session bean non
hanno stato, tutte le istanze dello stesso bean sono equivalenti e indistinguibili dal client. Non importa quale istanza è stata invocata da un client
specifico, ogni stateless session bean può servire ogni richiesta di un client.
5.4 Session Bean
126
Stateful Session Beans
In alcuni processi è necessario ricordare lo stato della conversazione, cioè
lo stato del sistema. Per esempio potrebbe essere necessario ricordare cio’
che abbiamo immesso nel carrello della spesa di un sito web. I bean di tipo
stateful contengono variabili che hanno il compito di mantenere lo stato della
sessione del client.
Quando un client invoca un metodo sul bean, il client fa partire una
conversazione con il bean. Questa conversazione viene salvata dal bean e
deve essere accessibile ad ogni chiamata dello stesso client.
Per limitare il numero di bean presenti in memoria (per non sovraccaricare la memoria fisica del computer), l’EJB container esegue l’operazione di
pooling. Questa operazione consiste nell’ esecuzione di uno swap out dello stato della conversazione dalla memoria fisica nell’hard disk quando sono
presenti troppi bean in memoria. Successivamente, quando il client richiede
l’uso del bean, il container esegue uno swap in della conversazione nel bean.
L’operazione di salvataggio del bean su un archivio permanente viene chiamata passivazione. L’operazione inversa viene chiamata attivazione. E’ da
notare che, quando la conversazione viene riattivata, il bean potrebbe non
essere quello originario. Questo non costituisce un problema in quanto cio’
che interessa non è il bean stesso ma lo stato della conversazione tra il bean
e il client.
Come si può immaginare, lo stato della conversazione deve seguire le regole della Java object serialization. Quando un bean viene messo in modalità
passiva, il container usa il protocollo serialization per convertire lo stato della
conversazione in un flusso di dati, formato che consente la scrittura su disco.
L’istanza del bean continua ad esistere, e può essere assegnata ad un’altra
conversazione con un nuovo client.
Il processo di attivazione è l’inverso: un dato che è stato salvato su
hard disk viene riletto, riportato in memoria e riconvertito nella modalità
appropriata. L’interfaccia Serializable, che l’enterprise bean indirettamente
implementa, permette tali processi.
Quando l’EJB container passa una conversazione in modalità passiva,
informa il bean chiamando uno dei metodi che devono essere implementati
obbligatoriamente: ejbPassivate(). Questo metodo notifica al bean che la
conversazione sta per essere trasferita su disco rigido. In questo modo il
bean può rilasciare le risorse che in altri momenti aveva occupato, come una
connessione ad un database, l’apertura di una socket, un file aperto, o altre
risorse che non possono essere salvate usando la serializzazione.
Quello che accade durante l’attivazione è l’esatto opposto. Lo stato della
conversazione viene riportato in memoria, successivamente il container chia-
5.4 Session Bean
127
ma il metodo richiesto ejbActivate(), che dà l’opportunità di riaprire le risorse
che erano state chiuse nella fase di passività.
Nel caso in cui non ci sono risorse da rilasciare come le socket o le
connessioni a database, non c’è bisogno di implementare questi metodi.
5.4.1
Esempi
Nelle sezioni che seguono vedremo in pratica come viene creato un EJB attraverso alcuni esempi. Il primo esempio sarà un Session Bean Stateless: il
bean avrà due metodi per la cifratura e la decifratura di una stringa secondo
una chiave numerica inserita. Il secondo esempio sarà un semplice Session
Bean Stateful: questo bean conterrà semplicemente il numero di chiamate
da parte del client (un numero passato dal client oppure l’intero 1 in caso di
nessuna comunicazione).
Quando si crea un componente EJB, i tipici passi da eseguire sono i
seguenti:
1. Scrivere le classi .java che compongono il bean: le interfacce remote, le
home interface, la enterprise bean class e tutte le altre classi necessarie
all’applicazione.
2. Scrivere il deployment descriptor.
3. Compilare le classi creando i file .class
4. Creare un file EJB-jar che contiene le classi java e il deployment descriptor, secondo uno schema che vedremo in seguito.
5. Installare il bean all’interno del Container (per esempio JBoss).
6. Configurare il container per le esigenze del bean (per esempio le connessioni al database o la dimensione del pooling).
7. Far partire il Container e verificare se il bean è stato installato correttamente.
8. Opzionalmente creare una applicazione stand-alone per testare la funzionalità effettiva del bean creato.
5.4.2
Ciclo vita dei Session Bean
Session Bean Stateless
Quando è stato realizzato il bean e installato all’interno del container, è
quest’ultimo che si prende l’onere di gestire la sua vita, dalla creazione alla
5.4 Session Bean
128
Figura 5.4: Schema del ciclo vita di un stateless session bean
cancellazione. In figura è mostrato lo schema del ciclo vita del Session Bean
Stateless.
Quando un client richiede la creazione di un Session Bean Stateless, il
Container può agire in due modi differenti:
- Creare un nuovo EJB e quindi aggiungerlo al pool dei Session Stateless.
Questo comporta l’invocazione dei metodi 1 e 2 mostrati in Figura
5.4. Dopo l’invocazione del metodo ejbCreate, ovvero dopo che il client
chiama il metodo create, il bean si trova nello stato Ready e quindi il
client può invocare i business methods.
- Restituire un bean già creato e presente nel pool. Il client non potrà
mai sapere che non è stato creato in quanto i bean stateless sono
indistinguibili, non hanno stato.
Dopo l’utilizzo del bean e alla fine della sessione, il container può eliminare l’EJB dal pool, dopo aver invocato il metodo ejbRemove, oppure non
effettuare nessuna operazione invocando il bean alla richiesta di un nuovo
bean. Come si vede dallo schema, i metodi ejbActivate e ejbPassivate possono
anche venir lasciati vuoti in quanto non verranno mai chiamati.
Session Bean Stateful
In Figura 5.5 è mostrato il ciclo di vita di un stateful session bean. Da
ricordare che i metodi del bean sono chiamati dal container, non dal client.
Questo ciclo di vita è molto simile a quello di un stateless session bean,
ma ci sono alcune grandi differenze:
5.4 Session Bean
129
Figura 5.5: Schema del ciclo vita di un stateful session bean
- Il container non può restituire un altro bean presente nel pool in quanto
il suo stato appartiene ad una conversazione precedente e quindi diversa
da quella scelta.
- Esistono le transazioni tra lo stato attivo e passivo.
Quando il client richiede la creazione di un Session Bean Stateless, l’EJB
container crea un nuovo bean, usato solo da quel client, ed invoca i metodi 2
e 3 del grafico. Ora il bean è pronto a ricevere invocazioni remote per i suoi
metodi di business.
Mentre si trova nello stato attivo, il container può decidere di disattivare il bean, facendo lo swap in memoria secondaria, o eliminarlo. Quando
avviene questa disattivazione, il container chiama il metodo ejbPassivate e
successivamente lo disattiva. Se decide di eliminarlo, chiama prima il metodo ejbRemove e poi elimina il bean ed il suo stato. Quando il client invoca un
metodo sul bean passivo, il container lo attiva, chiama il metodo ejbActivate,
e lo riporta in modalità Ready. Se il bean si trova in modalità passiva può
andare in timeout ed in tal caso il container elimina direttamente il bean.
5.4.3
Creazione di un Session Bean Stateless
L’interfaccia remota
Il primo passo consiste nello scrivere l’interfaccia remota. L’interfaccia remota duplica tutti i metodi implementati dall’enterprise bean class. Il codice è
mostrato nel Listato 5.5 (e fa riferimento ad un’applicazione per la criptazione
di una password).
Ci sono alcune cose da notare:
5.4 Session Bean
130
package crypt;
public interface Crypt extends javax.ejb.EJBObject {
String crypt(String origine, int key)
throws java.rmi.RemoteException;
String decrypt(String criptata, int key)
throws java.rmi.RemoteException;
}
Listato 5.5: L’interfaccia Remota
- La classe estende javax.ejb.EJBObject. Ciò significa che l’oggetto generato dal container conterrà tutti i metodi che l’interfaccia javax.ejb.EJBObject definisce, quindi un metodo per comparare due EJB Object,
un metodo per rimuovere un EJB Object, e cosı̀ via.
- Nell’interfaccia remota appaiono due metodi, crypt(String,int) e decrypt(String,int), che devono essere implementati nella enterprise bean
class. Dal momento che EJBObject estende java.rmi.Remote, anche l’interfaccia remota la estenderà e quindi dovrà lanciare la java.rmi.RemoteException.
Inoltre, anche i parametri passati devono rispettare le regole RMI-IIOP,
per questo i parametri che sono usati nell’esempio sono serializzabili
(String é una classe che estende la serializzazione).
La classe enterprise bean
Il secondo passo è scrivere la classe che fornisce l’implementazione del servizio,
cioè il vero Bean. Il codice è mostrato nel Listato 5.6.
Bisogna notare che:
- La nostra implementazione estende javax.ejb.SessionBean, che fa del
bean un Session Bean. Questa interfaccia definisce alcuni metodi che
la classe deve obbligatoriamente implementare. Il container usa questi
metodi per interagire con il bean, invocandoli in caso di eventi importanti (come l’attivazione o la rimozione del bean).
- Il bean ha un metodo ejbCreate() che il container chiama quando il bean
viene creato. Questo metodo non può essere inserito nell’interfaccia
javax.ejb.SessionBean, in quanto i parametri passati possono cambiare
5.4 Session Bean
131
package crypt;
public class CryptBean implements javax.ejb.SessionBean{
public void ejbCreate()
{}
public void ejbRemove()
{}
public void ejbActivate()
{}
public void ejbPassivate()
{}
public void setSessionContext(javax.ejb.SessionContext ctx)
{}
public String crypt(String origine, int key) {
char dest[] = new char[origine.length()];
for (int i = 0; i < origine.length(); i++)
dest[i] = (char) ( ( (int) origine.charAt(i)) + key);
return (new String(dest));
}
public String decrypt(String criptata, int key) {
char dest[] = new char[criptata.length()];
for (int i = 0; i < criptata.length(); i++)
dest[i] = (char) ( ( (int) criptata.charAt(i)) - key);
return (new String(dest));
}
}
Listato 5.6: La classe enterprise bean
5.4 Session Bean
132
package crypt;
public interface CryptHome extends javax.ejb.EJBHome {
Crypt create() throws java.rmi.RemoteException,
javax.ejb.CreateException;
}
Listato 5.7: La home interface
oppure essere assenti come nell’esempio. I parametri qui passati devono
essere gli stessi presenti nel metodo create() della Home interface.
- I metodi ejbActivate() e ejbPassivate() vengono lasciati vuoti in quanto non ci sono risorse da ottenere o rilasciare nel caso di azioni del
Container.
- Quando il bean viene distrutto, non c’è nulla da rimuovere, quindi il
metodo ejbRemove() può essere lasciato vuoto.
- Se il Container deve comunicare con il bean, utilizza i metodi descritti
in precedenza. Se invece il bean deve comunicare con il Container,
può utilizzare la classe SessionContext. Il container chiama il metodo
setSessionContext in caso di cambiamento di alcuni parametri.
La Home interface
Il passo successivo consiste nella creazione della home interface. La home
interface ha i metodi per creare e distruggere gli EJB Object. Infatti il client
chiamerà il metodo create() per avere la enterprise bean class, mascherata
dall’interfaccia remota.
Il codice per la home interface è mostrato nel Listato 5.7. Bisogna notare
che:
- Il metodo create() lancia una javax.rmi.RemoteException e javax.ejb.CreateException. La RemoteException è necessaria in quanto sono oggetti
remoti che lavorano sulla rete. La CreateException è richiesta per ogni
metodo create()
- La classe Home interface estende javax.ejb.EJBHome, estensione richiesta a tutte le home interface, che definisce il modo per distruggere un
EJB Object.
5.4 Session Bean
133
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD
Enterprise JavaBeans 2.0//EN’
’http://java.sun.com/dtd/ejb-jar_2_0.dtd’>
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>Crypt</ejb-name>
<home>crypt.CryptHome</home>
<remote>crypt.Crypt</remote>
<ejb-class>crypt.CryptBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
</ejb-jar>
Listato 5.8: Il deployment descriptor
Il deployment descriptor
Successivamente c’è bisogno di generare il deployment descriptor.
Il deployment descriptor è una delle chiavi degli EJB in quanto permette di
descrivere gli attributi specifici invece di programmare funzioni interne.
Il deployment descriptor è un documento XML. Il deployment descriptor
per il nostro esempio è mostrato nel Listato 5.8.
<ejb-name> Questo è il nickname per uso interno.
<home> Il nome completo della home interface (completo di package)
<remote> Il nome completo del’interfaccia remota
<ejb-class> Il nome completo della classe che implementa il session bean
<session-type> Il tipo di session bean: Stateless o Stateful
Il file jboss.xml
Questo è un file specifico per ogni Container. Ci sono alcune differenze tra
container, come gli algoritmi sul pooling, algoritmi sul clustering e altro.
5.4 Session Bean
134
<?xml version="1.0" encoding="UTF-8"?>
<jboss>
<enterprise-beans>
<session>
<ejb-name>Crypt</ejb-name>
<jndi-name>crypt</jndi-name>
</session>
</enterprise-beans>
</jboss>
Listato 5.9: jboss.xml
Per JBoss il file che descrive queste caratteristiche è chiamato jboss.xml
ed è mostrato per il nostro esempio nel Listato 5.9.
Anche questo file è in formato XML:
<ejb-name> Questo è il nickname per uso interno.
<jndi-name> Questo è il nome usato per il look-up JNDI
Il file Ejb-jar
Una volta creati questi file bisogna raggrupparli in un Ejb-jar, file che nel
caso di JBoss e’ semplicemente una cartella che viene chiamata nomeEJB.jar
(per esempio crypt.jar)
I file .class devono venir generati inserendo nel classpath il package javax.ejb
presente nella cartella: {dir. inst}\client\jboss-j2ee.jar (per esempio C:\Programmi\jboss-4.0.3SP1\client\jboss-j2ee.jar).
Una volta creati i file .class bisogna inserirli nella cartella, insieme ai
descriptor file, secondo lo schema in Figura 5.6
Successivamente inserire la cartella creata nella directory di installazione:
{dir.inst}\server\default\deploy. Se JBoss è gia in esecuzione dopo
qualche secondo compariranno tre righe come in Figura 5.7
Il client
La scrittura del client è molto semplice, grazie alla trasparenza della programmazione EJB è possibile scrivere il codice senza preoccuparsi di dove
verrà eseguito. Questo avviene grazie al looking up del JNDI. I passi da
eseguire per la scrittura di un client sono semplici:
5.4 Session Bean
135
Figura 5.6: Scema del ejb-jar file
Figura 5.7: Visualizzazione del deploy su JBoss
5.4 Session Bean
136
1. Effettuare il look-up per ottenere il riferimento alla home interface.
2. Usare la home interface per creare il riferimento al EJBObject.
3. Eseguire i metodi del bean per risolvere la propria applicazione di
business.
4. Opzionalmente si può rimuovere il bean. Il container eliminerà automaticamente il bean dopo un certo periodo.
Per poter compilare ed eseguire le classi .java, c’è bisogno dei package contenuti nel file {dir.inst}\client\jbossall-client.jar. Inoltre c’è bisogno della copia delle interfacce home e remote (Crypt.class e CryptHome.class)
Nel caso in cui queste classi siano nella stessa directory del client (se non ci
sono c’è bisogno di specificare la loro posizione nel classpath), il classpath per
compilare i file .java potrebbe essere per esempio: C:\Programmi\jboss-4.0.3SP1\
client\jboss-all-client.jar;.
La classe del client è riportata nel Listato 5.10.
5.4.4
Creazione di un Session Bean Stateful
La remote interface
Per prima cosa si implementa la remote interface illustrata nel Listato 5.11.
Quest’interfaccia definisce un singolo metodo, add(), che implementerà la
enterprise bean class. Questo metodo aggiunge una unità al contatore del
bean e restituisce il valore aggiornato.
La enterprise bean class
Questa classe è responsabile dell’incremento della variabile intera val. In
questo esempio lo stato della conversazione è semplicemente la variabile
val definita all’interno della classe. Il codice della classe è mostrato nel
Listato 5.12 e 5.13.
Ci sono alcune cose da notare:
- Esistono due metodi ejbCreate, uno prende come paramentro un intero, l’altro nessun parametro. Questi due metodi sono responsabili
dell’inizio della conversazione, partendo da uno stato già definito o da
un nuovo stato.
- La variabile val, in quanto primitiva, è serializzabile e quindi è possibile salvare il suo stato. In questo modo lo stato della conversazione sopravvive alle diverse chiamate dei metodi ed è salvato automaticamente
durante l’attivazione o durante la passivazione.
5.4 Session Bean
137
package client;
import javax.naming.*;
import java.util.Properties;
import crypto.*;
public class Index {
public static void main(String args[]) throws Exception {
Properties prop = new Properties();
prop.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
prop.put(Context.URL_PKG_PREFIXES, "org.jnp.interfaces");
prop.put(Context.PROVIDER_URL, "localhost");
Context ctx = new InitialContext(prop);
Object obj = ctx.lookup("crypt");
CryptHome home = (CryptHome)
javax.rmi.PortableRemoteObject.
narrow(obj, CryptHome.class);
Crypto convert = home.create();
String cryptArgs0 = convert.crypt(args[0], 3);
System.out.println("LaÃstringaÃcriptataèÃ:Ã" +
cryptArgs0);
cryptArgs0 = convert.decrypt(cryptArgs0, 3);
System.out.println("LaÃstringaÃdecriptataèÃ:Ã" +
cryptArgs0);
}
}
Listato 5.10: Il client
package statefullBean;
public interface CountRemote extends javax.ejb.EJBObject{
public int add() throws java.rmi.RemoteException ;
}
Listato 5.11: L’interfaccia Remota
5.4 Session Bean
138
package statefulBean;
public class CountBean implements javax.ejb.SessionBean {
private int val;
public int add()
{
val++;
System.out.println("HaiÃchiamatoÃilÃmetodoÃdi
incremento... ora stai a "+val+" chiamate");
return val;
}
public void ejbCreate(int val)
throws javax.ejb.CreateException
{
this.val = val;
System.out.println("CreazioneÃdelÃbean
con valore val="+val);
}
public void ejbCreate()
throws javax.ejb.CreateException
{
this.val = 0;
System.out.println("CreazioneÃdelÃbean
con valore val="+val);
}
public void ejbRemove()
{
System.out.println("IlÃbeanÃàverrÃoraÃrimosso");
}
public void ejbPassivate()
{
System.out.println("IlÃbeanÃàverrÃmessoÃpassivoÃe
il suo stato salvato su hard disk");
}
Listato 5.12: L’Enterprise Bean
5.4 Session Bean
139
public void ejbActivate()
{
System.out.println("IlÃbeanèÃÃstatoÃattivatoÃe
la sua conversazioneè stata ricreata");
}
public void setSessionContext(javax.ejb.SessionContext ctx)
{}
}
Listato 5.13: L’Enterprise Bean
package statefullBean;
public interface CountHome extends javax.ejb.EJBHome {
public CountRemote create(int val)
throws javax.ejb.CreateException, java.rmi.RemoteException;
public CountRemote create()
throws javax.ejb.CreateException, java.rmi.RemoteException;
}
Listato 5.14: La home interface
- Questa classe implementa il metodo setSessionContext(). Nel nostro
esempio non è utilizzato ma, come già spiegato nell’ esempio precedente, esso è il collegamento tra bean e Container. Attraverso questa
classe il bean può comunicare con il Container. In Java è possibile
passare il proprio riferimento usando la parola chiave this, in EJB non
è possibile in quanto il bean viene chiamato indirettamente attraverso
l’EJBObject.
La home interface
Per completare il codice del bean stateful, bisogna definire la home interface. Tale interfaccia specifica come creare e distruggere il bean. Il codice è
mostrato nel Listato 5.14.
Da notare le due ricorrenze del metodo create(),una per ciacun metodo
ejbCreate nell’Enterprise Bean (Listato 5.12).
5.4 Session Bean
140
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD
Enterprise JavaBeans 2.0//EN’
’http://java.sun.com/dtd/ejb-jar_2_0.dtd’>
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>CountBean</ejb-name>
<home>statefullBean.CountHome</home>
<remote>statefullBean.CountRemote</remote>
<ejb-class>statefullBean.CountBean</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
</session>
</enterprise-beans>
</ejb-jar>
Listato 5.15: Il deploy descriptor
Il deployment descriptor
Ora che tutte le classi .java sono state create, c’è bisogno di definire il deployment descriptor per specificare al Container i settaggi del bean. Lo schema
XML è esposto nel Listato 5.15.
Si può notare che nel tag <Session-type> si specifica la natura del bean
(Stateless o Statefull), evitando di introdurre la notazione all’interno del
codice. In questo modo si semplifica lo scambio tra stateless e stateful.
Il file jboss.xml
Il file specifico per JBoss è mostrato nel Listato 5.16.
Per completare l’installazione c’è bisogno della creazione del file ejb-jar.
I passi da seguire sono i medesimi dell’esempio precedente.
Un settaggio che è possibile cambiare nel Container è il numero massimo di
bean attivi, cioè il pooling. E’ possibile modificarlo all’interno del file standardjboss.xml che si trova nella directory {dir.inst}\server\default\conf.
All’interno di questo file bisogna cercare il tipo di bean all’interno del tag
<container-name> (per esempio StandardStatefulSessionBean) e modificare
5.4 Session Bean
141
<?xml version="1.0" encoding="UTF-8"?> <jboss>
<enterprise-beans>
<session>
<ejb-name>CountBean</ejb-name>
<jndi-name>Count</jndi-name>
<max-pool-size>2</max-pool-size>
<min-pool-size>1</min-pool-size>
</session>
</enterprise-beans>
</jboss>
Listato 5.16: jboss.xml
il tag <max-capacity>. Questo tag impone al container il numero massimo
di bean presenti in memoria.
Per fare in modo che la modifica venga accettata bisogna far ripartire
JBOSS.
Il client
Ora che il bean è stato installato, possiamo scrivere il codice Java per testarlo.
I passi da eseguire saranno i seguenti:
1. Acquisire un JNDI initial context.
2. Trovare l’home object attraverso il look-up JNDI
3. Creare tre diversi Count EJB object, quindi creare tre diverse conversazioni e simulare tre diversi client.
4. Limitare la bean pool capacity a due bean, in modo tale che durante la
creazione delle tre conversazioni uno dei tre bean debba essere reso passivo. Per essere certi che questo avvenga,è stato inserito un messaggio
all’interno del metodo ejbPassivate().
5. Chiamare il metodo add() su ogni EJB object. Ciò forza il container
ad attivare i bean messi precedentemente in modalità passiva. Come
per il metodo precedente è stato inserito un messaggio all’interno del
metodo ejbActive
6. Rimuovere i tre bean
il codice è mostrato nei Listati 5.17 e 5.18.
5.4 Session Bean
142
package clientBean;
import javax.naming.*;
import statefullBean.*;
public class Index {
public static void main(String[] args)
{
try
{
java.util.Properties prop = new java.util.Properties();
prop.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
prop.put(Context.URL_PKG_PREFIXES,"org.jnp.interfaces");
prop.put(Context.PROVIDER_URL, "localhost");
Context ctx = new InitialContext(prop);
Object obj = ctx.lookup("Count");
CountHome home = (CountHome) javax.rmi.
PortableRemoteObject.narrow(obj, CountHome.class);
CountRemote count[] = new CountRemote[3];
int countVal;
System.out.println("inizializzazioioneÃdeiÃbean");
count[i] = home.create();
Listato 5.17: Il Client
5.4 Session Bean
143
package clientBean;
System.out.println("BeanÃcreatoÃconÃvaloreÃiniziale:Ã0");
count[i] = home.create(1);
System.out.println("BeanÃcreatoÃconÃvaloreÃiniziale:Ã1");
count[i] = home.create(3);
System.out.println("BeanÃcreatoÃconÃvaloreÃiniziale:Ã3");
System.out.println("chiamoÃilÃmetodoÃcountÃsuiÃbeans");
for (int i=0;i<3 ;i++)
{
countVal = count[i].add();
System.out.println("ContatoreÃa:Ã"+countVal);
Thread.sleep(500);
}
for(int i = 0; i < 3 ; i++)
{
count[i].remove();
}
}
catch(Exception er)
{
er.printStackTrace();
}
}
}
Listato 5.18: Il Client
5.5 Entity Bean
5.5
144
Entity Bean
Gli Entity Bean sono oggetti persistenti che possono essere salvati in un
dispositivo permanente, come un database. Questo significa che è possibile
modellare i dati come un Entity Bean. I dati vengono salvati in un database,
mappando una tupla come un oggetto. Questo avviene salvando ogni parte
costituente il Bean in un campo della tabella. Quando si desidera caricare
il Bean dal database, viene creato un nuovo Bean e riempito con i campi
ottenuti dal database.
Questa operazione di mappatura di un oggetto in un database relazionale
è chiamata object relational mapping (O/R mapping). Si tratta di un’azione
di conversione e riconversione da un oggetto in memoria ad un dato relazionale e viceversa. Si noti che questo meccanismo è molto diverso dalla
serializzazione e quindi dal salvataggio su hard disk: decomponendo un oggetto è infatti possibile eseguire query per una qualsiasi informazione piuttosto
che caricare tutto l’oggetto in memoria.
In una qualunque applicazione object-oriented, anche la più sofisticata, è
sempre possibile effettuare una distinzione tra due tipi di componenti:
Applicazioni logiche. Questo tipo di componenti fornisce, attraverso l’uso dei Session Bean, metodi per l’esecuzione di azioni comuni. Contengono spesso algoritmi per la risoluzione di problemi e nascondono
la complessità dietro semplici chiamate.
Dati persistenti. Si tratta di oggetti che possono rappresentare dati semplici o complessi che devono essere salvati. Possono rappresentare, inoltre, persone, luoghi o cose. Questi componenti usano meccanismi
persistenti, come la serializzazione o l’O/R mapping in un database
relazionale o in un object database.
I dati potrebbero essere sempre usati come righe di un database, ma
è più vantaggioso usarli come oggetti perchè in questo modo risultano più
compatti e sono quindi più facili da gestire. Inoltre, è possibile associare dati
ad azioni, raggruppare i dati in oggetti e ottenere il middleware necessario
per un’applicazione server.
Gli Entity Bean sono componenti che rappresentano gli “oggetti di business” conservati in un meccanismo di memoria persistente (database relazionale). Sono completamente diversi dai Session Bean: questi ultimi
modellano processi e azioni, mentre gli Entity Bean contengono i dati necessari per l’applicazione. Per esempio, un Session Bean potrebbe trasferire una
certa quantità di denaro da un account ad un altro, mentre i dati di questi
account sono rappresentati negli Entity Bean.
5.5 Entity Bean
145
Gli Entity Bean contengono un set di interfacce standard comune a tutti gli EJB, tra cui: interfacce locali e remote, local interface e local home
interface, la classe enterprise bean e il deployment descriptor.
Esistono notevoli differenze tra le interfacce e i file degli Entity Bean e
quelli degli altri tipi di EJB:
Le classi entity bean tracciano una definizione dell’entità in uno schema
del database. Per esempio, un Entity Bean potrebbe essere mappato
in una definizione di tabella relazionale. In questo caso, un’istanza di
un Entity Bean della classe verrà salvata in una riga di una tabella.
Inoltre, il Bean potrebbe fornire alcuni semplici metodi per manipolare
dati o per accedervi, come decrementare il bilancio di un conto bancario.
Come la classe session bean anche questa classe deve fornire alcuni
metodi standard.
La classe primary key fa si che ogni Entity Bean sia diverso. Per esempio,
se abbiamo un milione di account bancari, ogni account ha bisogno
di un ID unico che non può essere mai ripetuto in un altro account.
Questa classe può contenere un numero qualsiasi di attributi. Gli EJB
consentono di definire in maniera flessibile ciò che identifica il bean,
compresa la classe primary key. L’unica regola è che questa classe deve
essere serializzabile e seguire le regole della object serialization di Java.
5.5.1
Caratteristiche
Gli Entity Bean sono resistenti a errori critici, come crash di applicazioni
server o anche crash di macchina. Questo perchè sono rappresentazioni di
dati in un sistema di salvataggio permanente e fault-tolerant. Se si verifica
un crash del sistema o della macchina, l’Entity Bean può essere ricreato in
memoria.
Questa è la differenza fondamentale tra i Session e gli Entity Bean. Gli
Entity Bean hanno un ciclo di vita molto più lungo di una sessione di un
client, può durare anche anni, dipende da quanto tempo i dati sono sul
database. La potenza di questo sistema risiede nel fatto che i dati situati in
memoria e sul database possono essere visti come una cosa sola: cioè se un
dato viene cambiato in memoria viene immediatamente cambiato anche nel
database. Ovviamente, i dati in memoria sono la copia dei dati presenti sul
database, quindi deve esserci un meccanismo di trasferimento di informazioni
tra gli oggetti Java e il database. Questo trasferimento è compiuto con
due metodi speciali che gli Entity Bean devono implementare: ejbLoad() e
ejbStore().
5.5 Entity Bean
146
L’EJBContainer usa questi metodi per manipolare gli EJB: il metodo
ejbLoad() viene invocato quando i dati vengono letti dal sistema di salvataggio
permanente in memoria, mentre il metodo ejbStore() viene invocato quando
i dati devono essere salvati nel database. Il container si preoccupa di chiamare questi metodi: possono essere chiamati in qualunque momento (tranne
che nei metodi di business). Inoltre, il container si preoccupa anche della
sincronizzazione dei dati.
Quando ci sono molti accessi agli stessi dati, il container crea istanze
multiple della stessa classe Entity Bean. Questo permette a molti client di
interagire concorrentemente con istanze separate, ognuna rappresentante i
medesimi dati. Per mantenere la consistenza dei dati, i Bean devono essere sincronizzati costantemente. Il container sincronizza i dati chiamando i
metodi ejbLoad() e ejbStore(). Questi metodi, come i metodi relativi al pooling (ejbActivate() ed ejbPassivate()), sono metodi che il container utilizza per
comunicare al Bean che sta per essere caricato o salvato sul database.
Anche gli Entity Bean sono soggetti al pooling. Invece di creare e distruggere un Bean quando un client si connette o disconnette, i Bean vengono
riutilizzati in quanto la struttura del Bean non cambia e i dati possono essere
ricavati dal database. Come accade per i Session Bean, il container utilizza i
metodi ejbActivate() e ejbPassivate() per comunicare al Bean che verrà messo
in modalità attivo o passivo e quindi che esso dovrà lasciare o recuperare
risorse che non possono essere salvate (come le connessioni ad un database o
ad una socket). Ovviamente quando un EJB viene reso passivo, oltre a rilasciare le risorse, deve anche salvare il proprio stato su database. Quindi verrà
chiamato il metodo ejbStore(). In maniera duale, quando esso verrà attivato,
oltre a riprendere le risorse deve ricreare il suo stato e quindi chiamare il
metodo ejbLoad()
Qualunque sia il tipo di supporto scelto per il salvataggio c’è bisogno di
scrivere il codice di accesso al supporto per salvare o caricare i dati. Esistono due tipi di approcci: bean-managed persistent e container-managed
persistent. Nel primo caso, è lo sviluppatore del Bean a scrivere tutto il
codice necessario per trasferire i campi in memoria nel supporto di salvataggio scelto (un database relazionale o ad oggetti). Nel secondo caso, invece,
è il Container che si fa carico di scrivere il codice necessario alle operazioni
di salvataggio, caricamento e modifica dei dati. Come vedremo negli esempi, questo tipo di approccio riduce drasticamente la quantità di codice da
scrivere.
Siccome l’istanza di un Entity Bean e i dati sul database possono essere
pensati come una cosa sola, l’inizializzazione di un Bean potrebbe richiedere
l’inizializzazione di una tupla sul database. Quindi, quando un Bean viene
creato con il metodo ejbCreate(), viene creata la corrispondente riga sul
5.5 Entity Bean
147
database. Cosı̀, in un approccio bean-managed persistent il metodo ejbCreate() è responsabile della creazione dei dati sul database. Dualmente, il
metodo ejbRemove() è responsabile della rimozioni dei dati dal database.
Nel caso, invece, container-managed persistent è il Container a modificare il
database e i metodi create e remove possono essere lasciati vuoti dal codice
di accesso al supporto di salvataggio (per esempio codice SQL).
C’è da ricordare che il client non invoca direttamente il Bean. L’EJB
object è generato attraverso l’home object. Quindi, ad ogni metodo ejbCreate() deve corrispondere il metodo create() nella home interface. Per esempio,
consideriamo un Entity Bean che contenga un account bancario AccountBean
con un’interfaccia remota Account, home interface AccountHome e primary
key AccountPK. Dato questo metodo ejbCreate() in AccountBean:
public AccountPK ejbCreate(String accountID,String
proprietario)...
il metodo Create() deve essere implementato nella home interface (da
notare la mancanza del prefisso “ejb”):
public Account Create(String accountID,String proprietario)...
Il valore di ritorno dei due metodi è diverso. L’istanza del Bean ritorna la
primary key AccountPK mentre l’home object ritorna un EJB object Account.
Questo avviene in quanto il metodo ejbCreate() viene eseguito dal Container,
il quale ha bisogno della primary key per identificare il bean. Con la primary
key il container può generare l’EJBObject e restituirlo al client.
Per distruggere i dati di un Entity Bean in un database, il client deve
chiamare il metodo remove() sulla remote interface. Bisogna osservare che
questo metodo non rimuove l’oggetto in memoria, ma solo i dati dal database.
Ciò accade perchè gli oggetti in memoria vengono riutilizzati da un altro
Bean. Il metodo ejbRemote() è un metodo presente nell’interfaccia e che
deve quindi essere implementato.
Poichè gli Entity Bean contengono informazioni che altri Bean possono
acquisire, essi devono essere facilmente reperibili. Per individuare uno specifico Entity Bean è possibile eseguire diversi tipi di ricerca definiti come metodi
e detti finder. La home interface contiene questi metodi addizionali, oltre ai
metodi di creazione e di distruzione.
5.5.2
Esempi
Di seguito saranno proposti due esempi che spiegheranno in dettaglio la differenza tra Entity Bean container-managed persistent e bean-managed persistent. Il primo esempio tratta la gestione di Account Bancari ed è stato
5.5 Entity Bean
148
public interface javax.ejb.EntityBean {
public
public
public
public
public
public
public
void
void
void
void
void
void
void
setEntityContext(javax.ejb.EntityContext);
unsetEntityContext();
ejbRemove();
ejbActivate();
ejbPassivate():
ejbLoad();
ejbStore();
}
Listato 5.19: L’interfaccia javax.ejb.EntityBean
realizzato con gli Entity Bean di tipo bean-managed persistent. Il secondo
invece si occupa della Gestione di Prodotti ed è stato realizzato con un EJB
container-managed persistent.
5.5.3
Creazione di un Entity Bean Bean-Managed persistent
Quando si scrive questo tipo di bean, il programmatore deve fornire il codice
necessario per l’accesso ai dati. Per scrivere un Entity Bean, bisogna scrivere un file Java che implementi l’interfaccia javax.ejb.EntityBean. Questa
interfaccia, come le altre derivanti da javax.ejb.EnterpriseBean, definisce alcuni metodi che il Bean deve implementare. Il codice di questa interfaccia è
mostrato nel Listato 5.19.
Questi sono i metodi che il container invoca in caso di un evento. Tale
interfaccia deve essere implementata da entrambi i tipi di Entity Bean. Ci
sono anche altri metodi che si possono definire, come i metodi per creare
nuovi Bean o individuare quelli già esistenti.
I metodi ejbFind()
I metodi ejbFind() non creano nuovi dati dal database, ma caricano dati
dagli Entity Bean. Questi metodi vengono implementati solo in questo tipo
di Bean, in quanto per i container-managed persistent i metodi in questione
vengono automaticamente generati.
Si possono avere diversi tipi di metodi finder. Una regola fondamentale è
che tutti questi tipi di metodi devono iniziare con “ejbFind ”. Per esempio,
ejbFindTuttiProdotti() oppure ejbFindNomeProdotto().
5.5 Entity Bean
149
Un’altra regola consiste nel definire obbligatoriamente un metodo find :
ejbFindByPrimaryKey(). Questo metodo, a differenza degli altri metodi che
restituiscono una Collection, trova un’unica istanza dell’Entity Bean (basata
sulla chiave primaria del Bean), e della tabella del database corrispondente.
Cosı̀ come per il metodo ejbCreate(), il client non invoca direttamente questi
metodi, ma invoca i corrispettivi metodi sulla home interface. Per far capire
la corrispondenza tra questi metodi si prenda ad esempio il metodo public Collection ejbFindBigAccount (int unMinimo) throws FinderException. Il
corrispettivo nella home interface sarà: public Collection findBigAccount(int
unMinimo) throws FinderException (da notare il prefisso ejb).
I metodi sopra descritti seguono tutti i seguenti passi:
- Quando il client invoca il metodi find sulla home interface, l’home object
chiede al Bean di trovare tutte le primary key class che corrispondono
ai parametri richiesti. Il Bean restituisce una Collection di queste chiavi
primarie al container.
- Quando riceve la collezione di chiavi dal Bean, il container crea una
collezione di EJB Object, uno per ogni chiave, e restituisce questa
collezione di oggetti al client. Ogni EJB Object rappresenta l’istanza
relativa ai dati contenuti nel database.
La remote interface: Account.java
La classe Account è la prima classe del nostro Bean, la remote interface. É
mostrata nel Listato 5.20.
Come tutte le remote interface, estende la javax.ejb.EJBObject. Inoltre,
offre una serie di metodi per manipolare l’Entity Bean, come i metodi per
effettuare il prelievo ed il deposito. Da notare l’AccountException, una eccezione creata per gestire gli errori di prelievo o di deposito o altri errori
relativi all’account.
L’home interface: AccountHome.java
Questa classe ha la responsabilità di creare i dati sul database che rappresenta l’account bancario. Il metodo create() restituisce un EJB Object al
client capace di manipolare i dati appena creati. La classe è mostrata nel
Listato 5.21.
Inoltre, sono presenti due metodi finder. findByPrimaryKey cerca all’interno del database se esiste l’account con chiave primaria AccountPK. findName
cerca all’interno del database tutti gli account con nome “nomeProp”.
5.5 Entity Bean
150
package entityBean;
import javax.ejb.*; import java.rmi.RemoteException;
public interface Account extends EJBObject {
public void deposit(double amt)
throws AccountException,RemoteException;
public void prelievo(double amt)
throws AccountException,RemoteException;
public double getBilancio()
throws RemoteException;
public String getNomeProprietario()
throws RemoteException;
public void setNomeProprietario(String unNome)
throws RemoteException;
public String getAccountID()
throws RemoteException;
public void setAccountID(String id)
throws RemoteException;
}
Listato 5.20: La remote interface
5.5 Entity Bean
151
package entityBean;
import javax.ejb.*;
import java.rmi.RemoteException;
import java.util.Collection;
public interface AccountHome extends EJBHome
{
public Account create(String accountID, String nomeProp)
throws RemoteException,CreateException;
public Account findByPrimaryKey(AccountPK key)
throws RemoteException,FinderException;
public Collection findName(String nomeProp)
throws RemoteException,FinderException;
public double getTotalBankValue()
throws AccountException,RemoteException;
}
Listato 5.21: La home interface
5.5 Entity Bean
152
Infine c’è il metodo getTotalBankValue() che restituisce l’ammontare dell’intera banca, ovvero la somma di tutti gli account presenti nel database.
La primary key class: AccountPK.java
Il codice di questa classe è mostrato nel Listato 5.22. Nel nostro caso, la
chiave primaria si riduce ad una semplice stringa: AccountID. Questa stringa
deve essere unica all’interno dell’account bancario. Nei casi più complessi,
dove ci sono più campi che identificano la chiave primaria, questa classe
conterrebbe diverse variabili.
Il metodo toString() è un metodo richiesto. Il container chiama questo metodo per ricevere la descrizione della chiave primaria sotto forma di String. In
questo caso, il metodo ritorna semplicemente il campo. In casi più complessi,
bisogna combinare i vari campi per formare una stringa.
Ci sono, inoltre, altri due metodi richiesti: hashCode() e equals(). Il primo
viene utilizzato dal container in quanto può decidere di salvare la primary key
class in una Hashtable. Questo può avvenire quando all’interno del container
viene creata una lista di tutti gli Entity Bean in memoria.
Il metodo equals() viene usato dal container per determinare, al suo
interno, se due entity bean rappresentano gli stessi dati.
L’enterprise bean class: AccountBean.java
Il passo decisivo è creare l’implementazione dell’entity bean. L’implementazione di questo esempio è abbastanza lunga ed è stata suddivisa in 3 parti
contenenti:
1. i campi che identificano lo stato del bean gestito.
2. i metodi di business che il client usa. Questi metodi sono esposti nella
remote interface.
3. i metodi richiesti dall’interfaccia javax.ejb.EntityBean più i metodi find
e i metodi per creare il bean definiti nella home interface.
Questa classe è molto pesante e lunga in quanto ad essa compete tutta
la gestione dei dati sul database. Il codice è mostrato dal Listato 5.23 al
Listato 5.33.
AccountException.java
Questa classe è mostrata nel Listato 5.34. Questa è una semplice classe che
delega alla classe padre java.lang.Exception gli errori sull’account bancario,
5.5 Entity Bean
153
package entityBean;
public class AccountPK implements java.io.Serializable {
private String accountID;
public AccountPK(String ID) {
accountID = ID;
}
public int hashCode()
{
return accountID.hashCode();
}
public String toString()
{
return accountID;
}
public boolean equals(Object obj)
{
if(obj instanceof AccountPK)
{
if(((AccountPK)obj).accountID.equals(accountID))
return true;
else return false;
}
else return false;
}
}
Listato 5.22: La primary key class
5.5 Entity Bean
154
package entityBean;
import
import
import
import
java.sql.*;
javax.naming.*;
javax.ejb.*;
java.util.*;
public class AccountBean implements EntityBean{
private EntityContext ctx;
// Campi dello stato del bean
private String accountID;
private String nomeProp;
private double bilancio;
public AccountBean() {
System.out.println("UnÃnuovoÃaccountèÃÃstatoÃcreato.");
}
Listato 5.23: AccountBean.java
5.5 Entity Bean
155
// Metodi della business logic
public void deposit(double amt) throws AccountException {
System.out.println("StoÃdepositandoÃl’ammontare
di "Ã+ÃamtÃ+Ã" sul conto "Ã+ÃaccountID);
bilancio += amt;
}
public void prelievo(double amt) throws AccountException {
System.out.println("StoÃprelevandoÃ" + amt + "
dal conto "Ã+ÃaccountID);
if (amt > bilancio)throw new AccountException(
"IlÃbilancionÃnonÃcopreÃilÃprelievo.ÃIlÃBilancioÃdel
conto "Ã+ÃaccountIDÃ+Ã"è di: "Ã+ÃbilancioÃ+Ã");
bilancio -= amt;
}
// Metodi get e set dell’entity bean
public double getBilancio(){
System.out.println("IlÃbilancioÃdelÃconto
"+accountID+"è di "+bilancio+");
return bilancio;
}
public void setNomeProprietario(String unNome){
System.out.println("inseriscoÃilÃnomeÃproprietario");
nomeProp = unNome;
}
public String getNomeProprietario() {
System.out.println("E’ÃstatoÃrichiestoÃilÃnomeÃdel
proprietario del conto "+accountID);
return nomeProp;
}
Listato 5.24: AccountBean.java
5.5 Entity Bean
156
public String getAccountID()
{
System.out.println("MièÃÃstatoÃrichiestoÃl’account
di questo conto: "+accountID);
return accountID;
}
public void setAccountID(String unAccount)
{
System.out.println("HoÃricevutoÃlaÃrichiesta
di cambio dell’account:Ã\n
dal vecchio "+accountID+"
al nuovo "+unAccount);
accountID = unAccount;
}
public double ejbHomeGetTotalBankValue()
throws AccountException
{
PreparedStatement pst = null;
Connection conn = null;
try
{
System.out.println("E’ÃstatoÃchiamatoÃilÃmetodo
per la visione completa
del bilancio della banca");
conn = getConnection();
pst = conn.prepareStatement("select
sum(bilancio) as total
from accounts");
ResultSet rs = pst.executeQuery();
if(rs.next()) return rs.getDouble("total");
}
Listato 5.25: AccountBean.java
5.5 Entity Bean
157
catch(Exception e)
{
e.printStackTrace();
throw new AccountException(e);
}
finally
{
try
{
if(pst!=null) pst.close();
if(conn!=null) conn.close();
}
catch(Exception er){er.printStackTrace();}
}
throw new AccountException("errore!");
}
// connessione al database interno a JBOSS
public Connection getConnection() throws Exception
{
try
{
Context ctx = new InitialContext();
javax.sql.DataSource data = (javax.sql.DataSource)
ctx.lookup("java:comp/env/jdbc/ejbPool");
return data.getConnection();
}
catch(Exception er)
{
System.err.println("erroreÃnelÃcaricareÃilÃdatabase");
er.printStackTrace();
throw er;
}
}
Listato 5.26: AccountBean.java
5.5 Entity Bean
158
// metodi richiesti dall’interfaccia
public void ejbActivate(){
System.out.println("IlÃcontoÃ"+accountID+
"èÃÃstatoÃattivato");
}
public void ejbPassivate(){
System.out.println("ilÃcontoÃ"+accountID+"èÃÃstato
messo in àmodalit passivo");
}
public void ejbRemove(){ throws RemoveException
System.out.println("ilÃcontoÃ"+accountID+
"èÃÃstatoÃrimosso");
AccountPK pk = (AccountPK) ctx.getPrimaryKey();
String id = pk.toString();
System.out.println("laÃkiaveÃottentutaèÃ"+pk);
PreparedStatement st = null;
Connection conn = null;
try {
conn = getConnection();
st = conn.prepareStatement("deleteÃfromÃaccounts
where id = ?");
st.setString(1,id);
if(st.executeUpdate() == 0)
throw new RemoveException("impossibileÃcancellare
dal database l’accountÃ"Ã+Ãpk);
}
catch(Exception e) {
throw new EJBException(e.toString());
}
Listato 5.27: AccountBean.java
5.5 Entity Bean
159
finally
{
try
{
if(st!=null) st.close();
if(conn!=null) conn.close();
}
catch(Exception er){er.printStackTrace();}
}
}
public void ejbLoad()
{
AccountPK pk = (AccountPK) ctx.getPrimaryKey();
String id = pk.toString();
System.out.println("laÃkiaveÃottentutaèÃ" + pk);
PreparedStatement st = null;
Connection conn = null;
try
{
conn = getConnection();
st = conn.prepareStatement("selectÃnomeProp,Ãbilancio
from accounts where id = ?");
st.setString(1,id);
ResultSet rs = st.executeQuery();
rs.next();
nomeProp = rs.getString("nomeProp");
bilancio = rs.getDouble("bilancio");
}
catch (Exception er)
{
throw new EJBException("impossibileÃcaricareÃl’account
"+pk+" dal database",er);
}
Listato 5.28: AccountBean.java
5.5 Entity Bean
160
finally
{
try
{
if(st!=null) st.close();
if(conn!=null) conn.close();
}
catch(Exception er){er.printStackTrace();}
}
}
public void ejbStore()
{
System.out.println("E’ÃstatoÃchiamatoÃilÃmetodoÃper
il salvatagggio su database");
PreparedStatement st = null;
Connection conn = null;
try
{
conn = getConnection();
st = conn.prepareStatement("updateÃaccounts
set nomeProp = ?, bilancio = ?
where id = ?");
st.setString(1, nomeProp);
st.setDouble(2,bilancio);
st.setString(3,accountID);
st.executeUpdate();
}
catch (Exception er)
{
throw new EJBException("impossibileÃsalvareÃl’account
"+accountID+" dal database",er);
}
Listato 5.29: AccountBean.java
5.5 Entity Bean
161
finally
{
try
{
if(st!=null) st.close();
if(conn!=null) conn.close();
}
catch(Exception er){er.printStackTrace();}
}
}
public void setEntityContext(EntityContext ctx)
{
System.out.println("chiamatoÃilÃmetodoÃsetEntityContext");
this.ctx = ctx;
}
public void unsetEntityContext()
{
System.out.println("chiamatoÃl’unsetÃdelÃentitycontext");
ctx = null;
}
public void ejbPostCreate(String accountID,String nomeProp)
{
System.out.println("chiamatoÃilÃmetodoÃpostCreate");
}
public AccountPK ejbCreate(String accountID, String nomeProp)
throws CreateException
{
PreparedStatement st = null;
Connection conn = null;
Listato 5.30: AccountBean.java
5.5 Entity Bean
162
try {
System.out.println("CreazioneÃdelÃconto");
this.accountID = accountID;
this.nomeProp = nomeProp;
bilancio = 0;
conn = getConnection();
st = conn.prepareStatement(
"insertÃintoÃaccountsÃ(id,nomeProp,bilancio)
values(?,?,?)");
st.setString(2, nomeProp);
st.setDouble(3, bilancio);
st.setString(1, accountID);
st.executeUpdate();
return new AccountPK(accountID);
}
catch (Exception er) {
throw new CreateException(er.toString());
}
finally {
try {
if (st != null) st.close();
}
catch (Exception er) {
er.printStackTrace();
}
try {
if (conn != null) conn.close();
}
catch (Exception er) {
er.printStackTrace();
}
}
}
Listato 5.31: AccountBean.java
5.5 Entity Bean
163
//metodi find
public AccountPK ejbFindByPrimaryKey(AccountPK key)
throws FinderException
{
PreparedStatement st = null;
Connection conn = null;
try {
System.out.println("chiamatoÃilÃmetodoÃper
la ricerca della chiave");
conn = getConnection();
st = conn.prepareStatement(
"selectÃidÃfromÃaccountsÃwhereÃidÃ=Ã?");
st.setString(1, key.toString());
ResultSet rs = st.executeQuery();
rs.next();
return key;
}
catch (Exception er) {
throw new FinderException(er.toString());
}
finally {
try {
if (st != null) st.close();
}
catch (Exception er) {
er.printStackTrace();
}
try {
if (conn != null) conn.close();
}
catch (Exception er) {
er.printStackTrace();
}
}
}
Listato 5.32: AccountBean.java
5.5 Entity Bean
164
public Collection ejbFindName(String name)
throws FinderException
{
PreparedStatement st = null;
Connection conn = null;
Vector v = new Vector();
try {
System.out.println("chiamatoÃilÃmetodoÃperÃlaÃricerca
del nome del proprietario del cont");
conn = getConnection();
st = conn.prepareStatement(
"selectÃidÃfromÃaccountsÃwhereÃnomePropÃ=Ã?");
st.setString(1, name);
ResultSet rs = st.executeQuery();
while(rs.next())
{
String id = rs.getString("id");
v.addElement(new AccountPK(id));
}
return v;
}
catch (Exception er) {
throw new FinderException(er.toString());
}
finally {
try {
if (st != null) st.close();
}
catch (Exception er) {
er.printStackTrace();
}
try {
if (conn != null) conn.close();
}
catch (Exception er) {
er.printStackTrace();
}
}
}
Listato 5.33: AccountBean.java
5.5 Entity Bean
165
package entityBean;
public class AccountException extends Exception {
public AccountException()
{
super();
}
public AccountException(Exception e)
{
super(e.toString());
}
public AccountException(String e)
{
super(e);
}
}
Listato 5.34: AccountException.java
come per esempio il prelievo di un quantitativo non presente sul conto. Inoltre distingue tra i problemi relativi all’account e altri problemi relativi al
malfunzionamento del programma.
Il deployment descriptor e jboss.xml
Infine per completare il bean da installare su JBoss c’è bisogno degli ultimi
due file XML rappresentati nei listati 5.35 e 5.36.
Il file JBoss.xml non cambia rispetto agli esempi precedenti. Nel deployment descriptor ci sono alcuni nuovi tag:
<persistence-type> Questo elemento indica il tipo di persistenza: BeanManaged o Container-Managed.
<prim-key-class> Specifica il nome della Primary Key class
<reentrant> Questo tag indica se il bean può chiamare se stesso attraverso
un altro bean. Cioè, dati due bean A e B, sussiste il reentrant se, il
bean A chiama il B, il quale richiama il bean A.
<resource-ref> Questo elemento specifica i driver JDBC.
5.5 Entity Bean
166
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD
Enterprise JavaBeans 2.0//EN’
’http://java.sun.com/dtd/ejb-jar_2_0.dtd’>
<ejb-jar>
<enterprise-beans>
<entity>
<ejb-name>Account</ejb-name>
<home>entityBean.AccountHome</home>
<remote>entityBean.AccountRemote</remote>
<ejb-class>entityBean.AccountBean</ejb-class>
<persistence-type>Bean</persistence-type>
<prim-key-class>entityBean.AccountPK</prim-key-class>
<reentrant>False</reentrant>
<resource-ref>
<res-ref-name>jdbc/ejbPool</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</entity>
</enterprise-beans>
</ejb-jar>
Listato 5.35: Deployment descriptor
<?xml version="1.0" encoding="UTF-8"?> <jboss>
<enterprise-beans>
<session>
<ejb-name>Account</ejb-name>
<jndi-name>Bank</jndi-name>
</session>
</enterprise-beans>
</jboss>
Listato 5.36: JBoss.xml
5.5 Entity Bean
167
Setup del database Hypersonic
Ultimo passo, prima di poter installare il bean e poter usarlo, c’è bisogno
di creare la tabella sul database. Per abilitare il database interno di JBoss
bisogna modificare il file hsqldb-ds.xml che si trova nella cartella {dir.inst}
\server\default\deploy. All’interno di questo file XML, sono presenti tre tag connection-url, due di questi sono commentati. Per abilitare il
database si deve commentare il tag esistente e selezionare il tag <connectionurl>jdbc:hsqldb:hsql://localhost:1701</connection-url>.
Inoltre bisogna eliminare il commento dai seguenti tag:
<mbean code="org.jboss.jdbc.HypersonicDatabase"
name="jboss:service=Hypersonic">
<attribute name="Port">1701</attribute>
<attribute name="Silent">true</attribute>
<attribute name="Database">default</attribute>
<attribute name="Trace">false</attribute>
<attribute name="No_system_exit">true</attribute>
</mbean>
<mbean code="org.jboss.jdbc.HypersonicDatabase"
name="jboss:service=Hypersonic,database=localDB">
<attribute name="Database">localDB</attribute>
<attribute name="InProcessMode">true</attribute>
</mbean>
Una volta salvato questo file, nella pagina iniziale di JBoss raggiungibile
attraverso la pagina http://localhost:8080/, all’interno della JMX Console
cliccare sul servizio: service=Hypersonic nella sezione jboss. All’interno
di questa pagina, il metodo void startDatabaseManager() apre il programma
per la gestione del database Hypersonic. Con questo programma è possibile,
attraverso il linguaggio SQL, creare le tabelle per gli Entity Bean. Questo
passaggio è automatico per i Container-Managed Bean.
Per questo esempio c’è bisogno della tabella accounts creata con il seguente
codice sql:
CREATE TABLE accounts
(
id varchar(64),
nomeProp varchar(64),
bilancio numeric(18)
)
Ora il bean è pronto per essere usato.
5.5 Entity Bean
168
Il Client
Nei Listati 5.37 e 5.38 è proposta una semplice classe Java che effettua alcune operazioni bancarie come un deposito e un prelievo. Per evitare di far
rimanere dati inutili sul database è stato creato un blocco try-catch allo
scopo di eliminare il bean in qualunque caso.
5.5.4
Creazione di un Entity Bean Container-Managed
persistent
Nel caso di un EJB Container-Managed Persistent (CMP), non c’è bisogno
di implementare alcuna linea di codice relativa alla gestione dei dati, è l’EJB
Container che fornisce le operazioni di salvataggio.
Un altro vantaggio di questo modello e che c’è una separazione tra un
entity bean e la sua rappresentazione statica. Questa separazione comporta
una maggiore facilità nel modificare la rappresentazione del bean in quanto
questa descrizione non si trova nel codice.
Per ottenere quest’obiettivo, i sorgenti devono poter esser privi di qualunque
codice JDBC o altro codice per la manipolazione dei dati. Il codice per la
manipolazione dei dati viene generato dal container creando una sotto classe
della classe entity bean. In questo modo non c’è bisogno di scrivere i campi
da salvare in quanto sono generati dal container nella sotto classe. L’assenza
di questi campi comporta l’assenza dei metodi get e set nella classe enterprise bean. Anche questi metodi sono generati dal container, per sapere la
nomenclatura di questi metodi bisogna creare la classe enterprise bean come
una classe astratta, dove i metodi get e set sono solo dichiarati.
Il Container conosce il nome di questi campi dal deployment descriptor. I nomi di questi campi, delimitati dal tag <cmp-field>, devono avere
la stessa nomenclatura dei metodi get e set: per esempio data una variabile prezzoBase il nome dei metodi get e set dovrà essere getPrezzoBase()
e setPrezzoBase().
Il passo successivo per la completa comprensione del modello CMP è
sapere come il Container crea i metodi finder. Per scrivere il codice in
maniera indipendente dal Container specifico in modo da mantenere la proprietà fondamentale degli EJB si utilizza il linguaggio EJB Query Language
(EJB-QL). Con questo linguaggio object-oriented molto simile a SQL, il container è capace di generare il codice necessario per l’accesso e il salvataggio
dei dati.
L’esempio seguente mostra un entity bean Container-Managed Persistent.
5.5 Entity Bean
169
package clientBean;
import
import
import
import
entityBean.*;
javax.naming.*;
javax.rmi.*;
java.util.*;
public class Index {
public static void main(String[] args)
{
AccountRemote account = null;
try
{
Properties prop = new Properties();
prop.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
prop.put(Context.URL_PKG_PREFIXES,"org.jnp.interfaces");
prop.put(Context.PROVIDER_URL, "localhost");
Context ctx = new InitialContext(prop);
Object obj = ctx.lookup("Bank");
AccountHome home = (AccountHome)
javax.rmi.PortableRemoteObject.
narrow(obj, AccountHome.class);
System.out.println("IlÃtotaleÃdegliÃaccountÃinizialeÃin
bancaè : "+home.getTotalBankValue());
home.create("121320-456-7890","JonhÃSmith");
Iterator i = home.findName("JonhÃSmith").iterator();
if(i.hasNext())
{
account = (AccountRemote) javax.rmi.
PortableRemoteObject.narrow(i.next(),
AccountRemote.class);
}
else throw new Exception("AccountÃnonÃtrovato");
System.out.println("BilancioÃinizialeÃ=
"+account.getBilancio());
Listato 5.37: Il client Index.java
5.5 Entity Bean
170
account.deposit(100);
System.out.println("DopoÃaverÃdepositatoÃiÃsoldiÃil
uovo bilancioè : "+account.getBilancio());
AccountPK pk = (AccountPK) account.getPrimaryKey();
account = null;
account = home.findByPrimaryKey(pk);
System.out.println("trovatoÃaccountÃconÃilÃseguente
id: "+pk+". Il suo bilancioè di : "+
account.getBilancio());
System.out.println("OraÃprovoÃaÃprelevareÃ150");
account.prelievo(150);
}
catch(Exception er)
{
System.out.println("l’eccezioneèÃÃstata
lanciata...: "+er.toString());
er.printStackTrace();
}
finally
{
try
{
System.out.println("distruzioneÃdelÃbean");
account.remove();
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
}
Listato 5.38: Il client Index.java
5.5 Entity Bean
171
package entityBean;
import javax.ejb.*; import java.rmi.RemoteException;
public interface Product extends EJBObject {
public String getNome()
throws RemoteException;
public void setNome(String unNome)
throws RemoteException;
public String getDescrizione()
throws RemoteException;
public void setDescrizione(String unaDescrizione)
throws RemoteException;
public double getPrezzoBase()
throws RemoteException;
public void setPrezzoBase(double unPrezzo)
throws RemoteException;
public String getProdotto()
throws RemoteException;
}
Listato 5.39: Product.java
La remote interface: Product.java
E’ l’interfaccia che il client invoca, essa è mostrata nel Listato 5.39.
La home interface: ProductHome.java
Questa classe è mostrata nel Listato 5.43.
Qui sono esposti i metodi per la creazione e la ricerca degli entity bean.
La primary key class: ProductPK.java
Come per il modello BMP, anche il Container-Manager Persistent ha bisogno della classe che identifica la chiave primaria. La classe è mostrata nel
Listato 5.41.
5.5 Entity Bean
172
package entityBean;
import java.rmi.RemoteException; import java.util.Collection;
import javax.ejb.*;
public interface ProductHome extends EJBHome{
public Product create(String prodotto,String name,
String descrizione,double prezzo)
throws CreateException, RemoteException;
public Product findByPrimaryKey(ProductPK key)
throws FinderException, RemoteException;
public Collection findNome(String unNome)
throws FinderException, RemoteException;
public Collection findDescrizione(String unaDescrizione)
throws FinderException, RemoteException;
public Collection findPrezzoBase(double unPrezzo)
throws FinderException, RemoteException;
public Collection findProdottiCostosi(double unPrezzo)
throws FinderException, RemoteException;
public Collection findProdottiEconomici(double unPrezzo)
throws FinderException, RemoteException;
public Collection findAll()
throws FinderException, RemoteException;
}
Listato 5.40: ProductHome.java
5.5 Entity Bean
173
package entityBean;
import java.io.Serializable;
public class ProductPK implements Serializable{
public String prodotto;
public ProductPK(String ID) {
prodotto = ID;
}
public ProductPK()
{
}
public int hashCode()
{
return prodotto.hashCode();
}
public String toString()
{
return prodotto;
}
public boolean equals(Object obj)
{
if(obj instanceof ProductPK)
{
if(((ProductPK)obj).prodotto.equals(prodotto))
return true;
else
return false;
}
else return false;
}
}
Listato 5.41: ProductPK.java
5.5 Entity Bean
174
La enterprise bean class: ProductBean.java
Nel Listato 5.42 è mostrata l’implementazione dell’entity bean containermanaged persistent.
Come si nota la dimensione del codice da scrivere è notevolmente diminuita. Questo diminuisce la possibilità di bug nel codice e aumenta la facilità
di manutenzione. Non sono utilizzati campi quindi il metodo ejbCreate()
utilizza i metodi set per popolare il database.
Il deployment descriptor
Il deployment descriptor in questo modello diventa fondamentale. Viene
usato dal container per conoscere i campi del bean e come realizzare il metodo
finder. Il codice per questo esempio è mostrato nei Listati dal 5.44 al 5.47.
La prima parte è la stessa che caratterizza anche il modello BMP. Successivamente c’è la definizione dei campi del bean, che devono essere gli stessi
dei metodi get/set. Infine le query per i metodi find.
jboss.xml e jbosscmp-jdbc.xml
Questi file servono a JBoss per inizializzare il database e sapere come abbinare ai campi del bean la corrispettiva colonna del database. Nel Listato 5.48 è mostrato il file jBoss.xml mentre nel Listato 5.49 è mostrato il file
jbosscmp-jdbc.xml.
Quest’ultimo file gestisce il database nel tag </defaults>, mentre nel
tag </enterprise-beans> gestisce i campi della tabella.
I tag <datasource> e <datasource-mapping> sono specifichi per JBOSS
e identificano il database Hypersonic. Il tag <create-table> indica se al
momento del deploy del bean il container deve creare la cartella. Il tag
<remove-table> viene usato dal container per conoscere se alla rimozione
del bean deve rimuovere anche la tabella. Il tag <pk-constraint> indica il
bisogno di creare una chiave primaria per questa tabella
Per le colonne del database i tag <field-name> e <column-name> specificano l’associazione tra colonna e campo del bean. field-name è lo stesso campo che si trova nel deployment descriptor, column-name è il campo utilizzato
nelle query per i metodi finder.
Il client: Index.java
Questa è una semplice classe Java che ha lo scopo di testare il bean. Normalmente non è il client ad effettuare queste operazioni ma i session bean che
interagiscono con gli entity bean. Il codice client è mostrato nel Listato 5.49.
5.5 Entity Bean
175
package entityBean;
import javax.ejb.*;
public abstract class ProductBean implements EntityBean{
private EntityContext ctx;
public
public
public
public
public
public
public
public
abstract
abstract
abstract
abstract
abstract
abstract
abstract
abstract
String getNome();
void setNome(String unNome);
String getDescrizione();
void setDescrizione(String unaDescrizione);
double getPrezzoBase();
void setPrezzoBase(double unPrezzo);
String getProdotto();
void setProdotto(String unID);
public void ejbActivate()
{
System.out.println("BeanÃattivato");
}
public void ejbPassivate()
{
System.out.println("BeanÃpassivato");
}
public void ejbRemove()
{
System.out.println("BeanÃrimosso");
}
public void ejbLoad()
{
System.out.println("BeanÃcaricato");
}
Listato 5.42: ProductBean.java
5.5 Entity Bean
176
public void ejbStore()
{
System.out.println("BeanÃsalvato");
}
public void setEntityContext(EntityContext unCtx)
{
System.out.println("SettatoÃentityÃcontext");
ctx = unCtx;
}
public void ejbPostCreate(String prodotto,String name,
String descrizione,double prezzo)
{
System.out.println("ChiamatoÃilÃpostÃcreate");
}
public ProductPK ejbCreate(String prodotto,String name,
String descrizione,double prezzo)
throws CreateException
{
System.out.println("beanÃcreatoÃ");
setProdotto(prodotto);
setNome(name);
setDescrizione(descrizione);
setPrezzoBase(prezzo);
ProductPK pk = new ProductPK(prodotto);
return pk;
}
public void unsetEntityContext(){ctx = null;}
}
Listato 5.43: ProductBean.java
5.5 Entity Bean
177
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD
Enterprise JavaBeans 2.0//EN’
’http://java.sun.com/dtd/ejb-jar_2_0.dtd’>
<ejb-jar>
<enterprise-beans>
<entity>
<ejb-name>ProductsBean</ejb-name>
<home>entityBean.ProductHome</home>
<remote>entityBean.Product</remote>
<ejb-class>entityBean.ProductBean</ejb-class>
<persistence-type>Container</persistence-type>
<prim-key-class>entityBean.ProductPK</prim-key-class>
<reentrant>False</reentrant>
<cmp-version>2.x</cmp-version>
<abstract-schema-name>products</abstract-schema-name>
<cmp-field>
<field-name>prodotto</field-name>
</cmp-field>
<cmp-field>
<field-name>nome</field-name>
</cmp-field>
<cmp-field>
<field-name>descrizione</field-name>
</cmp-field>
<cmp-field>
<field-name>prezzoBase</field-name>
</cmp-field>
Listato 5.44: ejb-jar.XML
5.5 Entity Bean
178
<query>
<query-method>
<method-name>findNome</method-name>
<method-params>
<method-param>java.lang.String</method-param>
</method-params>
</query-method>
<ejb-ql>
<![CDATA[select object(a) from products as a
where a.nome = ?1]]>
</ejb-ql>
</query>
<query>
<query-method>
<method-name>findDescrizione</method-name>
<method-params>
<method-param>java.lang.String</method-param>
</method-params>
</query-method>
<ejb-ql>
<![CDATA[select object(a) from products as a
where a.descrizione = ?1]]>
</ejb-ql>
</query>
<query>
<query-method>
<method-name>findPrezzoBase</method-name>
<method-params>
<method-param>double</method-param>
</method-params>
</query-method>
Listato 5.45: Product.java
5.5 Entity Bean
179
<ejb-ql>
<![CDATA[select object(a) from products as a
where a.prezzoBase = ?1 ]]>
</ejb-ql>
</query>
<query>
<query-method>
<method-name>findProdottiCostosi</method-name>
<method-params>
<method-param>double</method-param>
</method-params>
</query-method>
<ejb-ql>
<![CDATA[select object(a) from products as a
where a.prezzoBase > ?1]]>
</ejb-ql>
</query>
<query>
<query-method>
<method-name>findProdottiEconomici</method-name>
<method-params>
<method-param>double</method-param>
</method-params>
</query-method>
<ejb-ql>
<![CDATA[select object(a) from products as
a where a.prezzoBase < ?1]]>
</ejb-ql>
</query>
Listato 5.46: Product.java
5.5 Entity Bean
180
<query>
<query-method>
<method-name>findAll</method-name>
<method-params>
</method-params>
</query-method>
<ejb-ql>
<![CDATA[select object(a) from products as a
where a.prodotto is not null]]>
</ejb-ql>
</query>
</entity>
</enterprise-beans>
</ejb-jar>
Listato 5.47: Product.java
<?xml version="1.0" encoding="UTF-8"?>
<jboss>
<enterprise-beans>
<entity>
<ejb-name>ProductsBean</ejb-name>
<jndi-name>prodotti</jndi-name>
</entity>
</enterprise-beans>
</jboss>
Listato 5.48: jboss.xml
5.5 Entity Bean
181
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE jbosscmp-jdbc
PUBLIC
"-//JBoss//DTDÃJBOSSCMP-JDBCÃ3.0//EN"
"http://www.jboss.org/j2ee/dtd/jbosscmp-jdbc_3_0.dtd">
<jbosscmp-jdbc>
<defaults>
<datasource>java:/DefaultDS</datasource>
<datasource-mapping>Hypersonic SQL</datasource-mapping>
<create-table>true</create-table>
<remove-table>true</remove-table>
<pk-constraint>false</pk-constraint>
</defaults>
<enterprise-beans>
<entity>
<ejb-name>ProductsBean</ejb-name>
<table-name>products</table-name>
<cmp-field>
<field-name>nome</field-name>
<column-name>nomeProd</column-name>
</cmp-field>
<cmp-field>
<field-name>descrizione</field-name>
<column-name>descrizioneProd</column-name>
</cmp-field>
<cmp-field>
<field-name>prezzoBase</field-name>
<column-name>prezzo</column-name>
</cmp-field>
<cmp-field>
<field-name>prodotto</field-name>
<column-name>prod</column-name>
</cmp-field>
</entity>
</enterprise-beans>
</jbosscmp-jdbc>
Listato 5.49: jbosscmp-jdbc.xml
5.5 Entity Bean
182
package clientBean;
import entityBean.*; import javax.naming.*; import
javax.rmi.PortableRemoteObject; import java.util.*;
public class Index {
public static void main(String[] args){
ProductHome home = null;
try {
Properties prop = new Properties();
prop.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
prop.put(Context.URL_PKG_PREFIXES,"org.jnp.interfaces");
Context ctx = new InitialContext(prop);
Object obj = ctx.lookup("prodotti");
home = (ProductHome) javax.rmi.PortableRemoteObject.
narrow(obj, ProductHome.class);
Product c = home.create("123","P5-200",
"PentiumÃ2ÃGHz",100);
home.create("124","P5-300", "PentiumÃ3ÃGHz",600);
home.create("125","P5-350", "PentiumÃ3,5ÃGHz",1000);
home.create("126","AMD-300", "AMDÃ3ÃGHz",1000);
home.create("127","AMD-320", "AMDÃ3,2ÃGHz",1200);
home.create("128","AMD-35", "AMDÃ3.5ÃGHz",1500);
Iterator i = home.findNome("P5-300").iterator();
System.out.println("QuestiÃsonoÃiÃprodottoÃtrovati");
while(i.hasNext())
{
Product prod = (Product) PortableRemoteObject.
narrow(i.next(),Product.class);
System.out.println(prod.getDescrizione());
}
Listato 5.50: Il client
5.5 Entity Bean
183
System.out.println("ricercaÃtuttiÃiÃprodotti
che costano 1000");
i = home.findPrezzoBase(1000).iterator();
while(i.hasNext())
{
Product prod = (Product) PortableRemoteObject.
narrow(i.next(),Product.class);
System.out.println(prod.getDescrizione());
}
}
catch(Exception er)
{
er.printStackTrace();
}
finally
{
try
{
if (home != null) {
Iterator i = home.findAll().iterator();
while (i.hasNext()) {
Product prod = (Product) PortableRemoteObject.
narrow(i.next(), Product.class);
System.out.println("rimozioneÃdelÃprodottoÃ"+
prod.getNome());
prod.remove();
}
}
}
catch(Exception er)
{
er.printStackTrace();
}
}
}
}
Listato 5.51: Il client
5.6 Message-driven Bean
5.6
184
Message-driven Bean
Un Message-Driven Bean (MDB) è un Bean in cui il concetto di interfaccia
remota è del tutto assente, ed il solo meccanismo di interazione con il mondo esterno è quello basato sulla ricezione di messaggi JMS. Si tratta quindi
di componenti stateless transaction aware, il cui unico compito è quello di
processare messaggi JMS provenienti da topic o code. Il valore aggiunto che
si ha nell’usare un MDB al posto di una normale applicazione client JMS
è che in questo caso l’ascoltatore, essendo un Enterprise Bean, vive completamente all’interno di un contesto sicuro, transazionale e fault-tolerant.
Un altro grosso vantaggio è che, rispetto ad un semplice client, il MDB è
in grado di processare i messaggi in modo concorrente: lo sviluppatore si
deve concentrare sullo sviluppo di un solo componente, ovvero sulla logica di
processamento di un messaggio, mentre al resto pensa il Container che potrà
eseguire i vari Bean in modo concorrente. Un MDB può ricevere centinaia di
messaggi e processarli anche tutti nello stesso istante dato che il Container
eseguirà più istanze dello stesso MDB gestite in modalità di pool. In modo
del tutto analogo a quanto avviene per i Session o gli Entity, anche gli MDB
sono gestiti in tutto il loro ciclo di vita e durante l’esecuzione dei metodi di
business dal Container. Per questo, sebbene essi siano in grado di processare messaggi JMS, l’effettiva ricezione viene gestita dal container, che poi
in modalità di callback inoltra la chiamata al Bean.
5.6.1
Utilizzo dei MDB
Per chi si avvicina per la prima volta agli MDB uno dei dubbi più ricorrenti
è su come e quando un MDB debba essere utilizzato. La modalità corretta di
utilizzo di un MDB è anche la più semplice che si possa immaginare. Se in un
contesto EJB gli Entity rappresentano entità astratte e i Session degli oggetti
di servizio in cui eseguire metodi di business logic, gli MDB sono oggetti il
cui solo scopo è quello di operare come intermediari per l’esecuzione della
business logic in concomitanza di determinati eventi (ovvero all’arrivo di un
messaggio). Per questo motivo un MDB dovrebbe implementare solamente
la logica di gestione del messaggio e dell’analisi delle sue caratteristiche e
dar vita ad un flusso di operazioni invocando la business logic contenuta nei
metodi remoti di un session bean. Ad esempio un MDB potrebbe estrapolare
i campi dall’header del messaggio e successivamente stabilire quale metodo
di un Session invocare passandogli gli opportuni parametri. I MDB possono
avere campi di istanza come i Session e sono mantenuti durante il loro ciclo
di vita, ma essendo stateless e non potendo essere associati ad un client in
5.6 Message-driven Bean
185
Figura 5.8: Scema del ciclo vita di un message-driven bean
particolare, devono essere gestiti dal container in modo autonomo secondo la
logica del pool.
Questi Bean sono i più semplici da implementare in quanto richiedono
solo la enterprise bean class. L’unico modo di interazione con i client è
attraverso i messaggi JMS, quindi non c’è bisogno delle interfacce home e
remote con il client. Questo Bean deve implementare le interfacce sia con
JMS (MessageListener) che con il container MessageDrivenBean).
Quando viene installato un MDB sul container, su di esso vengono chiamati i metodi setMessageDrivenContext() e ejbCreate(). A questo punto, il
MDB è pronto a ricevere messaggi. Dopo l’installazione per ogni messaggio
ricevuto dal MDB, il metodo onMessage(Message) viene invocato. Il Container è responsabile di serializzare i messaggi: ogni MDB può processare al
più un solo messaggio alla volta ma il container può avere copie dello stesso
Bean permettendo cosı̀ l’accesso concorrenziale. Quando viene disinstallato
il MDB viene invocato il metodo ejbRemove() . La Figura 5.8 mostra il ciclo
vita di un MDB, molto simile ad un session bean stateless.
Capitolo 6
Applicazione completa sugli
EJB
In tale capitolo verrà mostrata un’applicazione riassuntiva contenente Session
Bean e Message Driven-Bean.
Si riporterà ora in dettaglio il funzionamento di tale applicazione per
meglio comprendere il contesto in cui si opera e le funzionalità che l’applicazione implementa. Successivamente si descriverà nei particolari la tecnologia utilizzata per la sua realizzazione. Per la creazione dell’intero applicativo
ci si può riferire alla Figura 6.1 che mostra le classi dell’applicazione.
Tutti i bean sono contenuti nella stessa applicazione: ciò significa che
il deployment descriptor sarà uno solo e conterrà le informazioni di tutti i
beans.
Per maggiore chiarezza, ogni bean ha un proprio package, in questo modo
è più semplice capire quali siano le classi relative ad ogni bean. Iniziamo nel
descrivere l’applicazione.
6.1
Vendita di libri on-line
Tale esempio riguarda una vendita di libri on-line. In particolare, la pagina
iniziale che si presenta all’utente nel momento in cui si collega all’applicazione attraverso un browser riporta semplicemente gli strumenti necessari
per effettuare il login, come si evince dalla Figura 6.2.
Se i dati sono stati inseriti correttamente, si accede ad una seconda pagina in cui si notifica che il login è avvenuto con successo, nel caso in cui il
nome utente e la password inseriti siano corretti; in caso contrario, la pagina
conterrà un messaggio che avvisa del fallimento del login (di seguito è ripor-
186
6.1 Vendita di libri on-line
Figura 6.1: Schema dell’applicazione
Figura 6.2: Pagina iniziale
187
6.1 Vendita di libri on-line
188
Figura 6.3: Caso A: inserimento dei dati; Caso B: login avvenuto con
successo; Caso C: login errato.
tata una sequenza di immagini in cui si visualizzano i vari casi possibili del
login) Figura 6.3.
Una volta inserito il nome utente e la password in modo corretto, si giunge
alla pagina principale dell’applicazione Figura 6.4 in cui è presente una lista
dei libri disponibili dalla quale l’utente può scegliere quelli di interesse ed
aggiungerli nel proprio carrello. La scelta avviene selezionando il libro scelto
e cliccando sul bottone Add to. A questo punto, verrà visualizzata una
nuova pagina attraverso la quale viene fornito il feedback all’utente: il libro
è stato aggiunto correttamente nel carrello (Figura 6.5). A questo punto,
cliccando su Continua si torna di nuovo nella pagina principale che riporta,
in alto a sinistra, il carrello contenente tutti i libri che il cliente ha deciso di
acquistare con il relativo conto. Il cliente può rimuovere dei libri dal proprio
carrello selezionandoli e cliccando sul tasto Delete (anche in tal caso verrà
visualizzata un’ulteriore pagina che dà conferma all’utente che il libro in
questione è stato eliminato con successo dal carrello). Una volta che l’utente
ha finito i propri acquisti può procedere al pagamento attraverso il tasto
Procedi al pagamento. Si giunge cosı̀ ad un’altra pagina dell’applicazione
nella quale vengono richiesti all’utente alcune informazioni per effettuare il
pagamento (Figura 6.6) quali: nome, numero di carta di credito e tipo di
6.1 Vendita di libri on-line
189
Figura 6.4: Pagina principale
Figura 6.5: Conferma del corretto inserimento del nuovo ordine nel carrello
6.1 Vendita di libri on-line
190
Figura 6.6: Richiesta dati per il pagamento
Figura 6.7: Schermata riepilogativa dei dati per il pagamento
carta di credito. Una volta inseriti tutti i dati, attraverso il bottone Send
To viene caricata una pagina riepilogativa dei dati inseriti per il pagamento
(Figura 6.7) in questa, una volta cliccato su Continua, si torna all’home page
dove altri utenti potranno effettuare nuovi acquisti.
L’applicazione consente al venditore di avere una gestione degli ordini
automatizzata mediante un’interfaccia che gli elenca gli ordini dei clienti
(Figura 6.8). Ciò avviene cliccando sul tasto start. Una volta che l’ordine
è stato evaso, esso può essere rimosso selezionandolo e cliccando sul tasto
Processato. Il tasto Chiudi consente invece di chiudere l’intera applicazione.
6.2 Alcuni Diagrammi UML dell’Applicazione
191
Figura 6.8: Ordini dei clienti
6.2
Alcuni Diagrammi UML dell’Applicazione
Di seguito, mostriamo alcuni diagrammi UML dell’Applicazione.
Use Case Consideriamo come scenario principale quello in cui tutto va a
buon fine:
Acquista un libro
1. Il cliente effettua il login
2. Il cliente scorre la lista e seleziona il libro da acquistare
3. Il cliente aggiunge il libro al carrello
4. Il cliente va alla cassa
5. Il cliente riempie i campi relativi alla carta di credito
6. Il sistema autorizza l’acquisto
Una possibile variante allo scenario principale può essere la seguente:
Autorizzazione Fallita
al passo 6, il sistema boccia l’autorizzazione a rilevare il credito. Consente al cliente di reinserire le informazioni relative alla carta di credito
e riprovare.
In Figura 6.9 è mostrato il Diagramma dei Casi d’Uso della nostra
Applicazione.
Class Diagram In Figura 6.10 è rappresentato il diagramma delle classi
relativo al componente Client Swing della nostra applicazione. Il dia-
6.2 Alcuni Diagrammi UML dell’Applicazione
192
0..*
1
Acquistare Libri
BookOrder Costumer
Figura 6.9: Diagramma Casi d’Uso
PrincipaleListener
Application
main() : void
OrderListener
1
START : String
CHIUDI : String
ELIMINA : String
start() : void
chiudi() : void
elimina() : void
1
listModel : DefaultListModel
connection : DefaultListModel
onMessage(Message : void) : void
stop() : void
1
Order
1
Principale
lista : JList
model : DefaultListModel
startBtn : JButton
chiudiBtn : JButton
eliminaBtn : JButton
sudPnl : JPanel
listScrollPane : int
user : String
books : String
getUser() : void
getBooks() : void
Figura 6.10: Diagramma delle Classi relativo a Client Swing
6.3 Package BookOrder
193
<<Form>>
DeleteBook
1
1
<<submit>>
<<Servlet>>
DeleteBook
index.html
1
1
<<Form>>
Pagamento
1
<<Jsp>>
Ordini
1
1
1
<<Form>>
Login
1
1
<<redirect>>
1
1
<<submit>>
<<Jsp>>
Pagamento
1
1
1
<<Form>>
Controller
1
1
<<submit>>
<<submit>>
1
1
<<Form>>
PagamentoServlet
<<Servlet>>
PagamentoServlet
1
<<submit>>
1
1
<<Servlet>>
ConfirmServlet
<<Servlet>>
Controller
<<submit>>
1
1
Figura 6.11: Diagramma delle Classi relativo ad Accesso Web
gramma delle classi in Figura 6.11 è relativo alla parte web (componente
Accesso web) ed è stato modellato secondo la metodologia descritta da
Jim Conallen nel suo articolo [12]
6.3
Package BookOrder
Il package BookOrder contiene al suo interno le seguenti classi:
BookOrder: interfaccia remota che descrive i metodi implementati dal Bean
per fornire il servizio di acquisto on line (quando un client vuole usare
un’istanza dell’enterprise class, non invocherà mai direttamente l’istanza, piuttosto l’invocazione viene intercettata dal ContainerEJB che
agisce come livello intermedio tra client e bean).
BookOrderBean: contiene la logica del sistema (è un esempio tipico di
sessionbean stateful). Tale bean tiene memoria dello stato di ogni
client (carrello della spesa e costi) e gestisce la comunicazione JMS
degli eventi associati all’acquisto di un libro. All’interno del metodo setSessionContext si realizza la comunicazione tra il Bean ed il
Container. All’interno di tale classe sono contenuti i metodi di gestione dell’ejb, compresi quelli standard per la gestione degli ejb stateful
(ejbActivate utilizzato per richiamare l’EJB dalla memoria di massa
6.4 Package BookOrderMDB
194
quando la conversazione con il client torna attiva, ejbPassivate che
consente di memorizzare l’EJB su un archivio permamente per ridurre il
consumo di memoria) e i metodi che implementano la logica di business
vera e propria.
BookOrderHome: classe che contiene il client che invoca l’EJBObject e
non il bean. Questo avviene solo se il client ha il riferimento a questo
oggetto. Il client ottiene il riferimento attraverso un produttore di
EJBObject: l’HomeObject responsabile della creazione, della ricerca e
della rimozione di un EJBObject. BookOrderHome è l’interfaccia che
consente di inizializzare correttamente l’EJBObject.
Il Listato 6.2 mostra la classe BookOrderHome.Java.
Il vero e proprio bean viene mostrato nei Listati da 6.3 a 6.8. dove sono
implementati tutti i metodi che sono disponibili nella remote interface.
6.4
Package BookOrderMDB
Nel package BookOrderMDB c’è solo la classe BookMDBean contenente i metodi
di gestione del bean tra cui il metodo onMessage().
Il bean riceve il messaggio con l’ordine che è stato eseguito dal client e lo
scrive in maniera opportuna su un file di log C:\orders.log
6.5
Package Archivio
La cartella RMIBookStore contiene il package Archivio. Al suo interno ci
sono le seguenti classi:
Book: contiene i metodi getTitolo, getListaAutori e getCosto.
DataLayer: l’interfaccia remota, contenente i metodi di business dell’rmi
server invocabili remotamente (è l’oggetto che consente la comunicazione remota tra il nostro client realizzato con l’EJB ed il server).
DataLayerImpl: è l’implementazione dell’Interfaccia remota in una classe
concreta che estende UnicastRemoteObject (facilita la creazione di oggetti remoti). Dentro tale classe viene definito univocamente l’elenco degli
utenti che possono accedere ai nostri servizi con le rispettive password,
quindi viene effettuato il controllo dell’identità degli utenti che tentano
di accedere al sistema per comprare i libri e viene verificato che il codice
della carta sia di 16 caratteri e che i caratteri siano numeri, dopodichè
6.5 Package Archivio
package BookOrder;
import javax.ejb.EJBObject;
import java.util.*;
import Archivio.*;
public interface BookOrder extends EJBObject {
boolean addBook(String name)
throws java.rmi.RemoteException;
boolean delBook(String name)
throws java.rmi.RemoteException;
Book[] getCarrello()
throws java.rmi.RemoteException;
float getConto()
throws java.rmi.RemoteException;
Book[] getAvailableBooks()
throws java.rmi.RemoteException;
boolean closeOrder(String numCarta,int tipo)
throws java.rmi.RemoteException;
int VISA = 0;
int MASTERCARD = 1;
int AMERICANEXPR = 2;
//definizione del tipo di carta
}
}
Listato 6.1: BookOrder.java
package BookOrder;
import javax.ejb.*;
public interface BookOrderHome extends EJBHome
{
BookOrder create(String user,String pass)
throws java.rmi.RemoteException, CreateException;
}
Listato 6.2: BookOrderHome.java
195
6.5 Package Archivio
package BookOrder;
import
import
import
import
import
import
javax.ejb.*;
java.rmi.*;
Archivio.*;
java.util.*;
javax.jms.*;
javax.naming.*;
public class BookOrderBean implements SessionBean
{
private SessionContext ctx;
private DataLayer istanza;
private String user;
private Vector carrello;
private float conto;
private javax.jms.Topic destination;
private TopicConnection connection;
//comunicazione jms degli eventi associati all’acquisto
//di un libro
public BookOrderBean()
{
carrello=new Vector();
conto=0;
destination = null;
connection = null;
}
public void setSessionContext(SessionContext parm1)
throws EJBException, RemoteException
//realizza la comunicazione tra il Bean ed il Container
//il parametro viene utilizzato dal MDB
{
ctx=parm1;
}
Listato 6.3: BookOrderBean.java
196
6.5 Package Archivio
//*********
//metodi di gestione degli EJB
//*********
public void ejbCreate(String utente,String passwd)
throws RemoteException, CreateException
{
System.out.println("BookOrderBean:Create()");
try
{
istanza=(DataLayer)Naming.lookup
("rmi://localhost:2000/Archivio");
if (istanza.checkUser(utente,passwd))
{
user=utente;
createJMSConnection();
}
else
throw(new CreateException());
}
catch(JMSException jme)
{
jme.printStackTrace();
throw(new CreateException());
}
catch(NamingException nme)
{
nme.printStackTrace();
throw(new CreateException());
}
catch(java.net.MalformedURLException mue)
{
mue.printStackTrace();
throw(new CreateException());
}
catch(NotBoundException nbe)
{
nbe.printStackTrace();
throw(new CreateException());
}
}
Listato 6.4: BookOrderBean.java
197
6.5 Package Archivio
198
public void ejbRemove()
throws EJBException, RemoteException
{
try
{
System.out.println("BookOrderBean:Remove()");
connection.close();
}
catch(JMSException err)
{
err.printStackTrace();
}
}
//metodi standard per la gestione dell’EJB stateful
public void ejbActivate()
throws EJBException, RemoteException
{ }
public void ejbPassivate()
throws EJBException, RemoteException
{ }
//**************
//metodi che implementano la logica di business vera e propria
//**************
public boolean addBook(String name)
throws RemoteException
{
Book newBook=istanza.getBookByName(name);
if (carrello.indexOf(newBook)==-1)
//se il libro nonè nel carrello
{
carrello.add(newBook);
conto+=newBook.getCosto();
return(true);
}
else
return(false);
}
Listato 6.5: BookOrderBean.java
6.5 Package Archivio
public boolean delBook(String name)
throws RemoteException
{
Iterator iter=carrello.iterator();
while(iter.hasNext())
{
Book aBook=(Book)iter.next();
if (aBook.getTitolo().equals(name))
{
iter.remove();
conto-=aBook.getCosto();
return(true);
}
}
return(false);
}
public Book[] getCarrello()
throws RemoteException
//restituisce i libri che si trovano nel carrello
{
Book[] retValue=new Book[carrello.size()];
Iterator iter=carrello.iterator();
int i=0;
while(iter.hasNext())
{
retValue[i++]=(Book)iter.next();
}
return(retValue);
}
Listato 6.6: BookOrderBean.java
199
6.5 Package Archivio
public float getConto()
throws RemoteException
{
return(conto);
}
public Book[] getAvailableBooks()
throws RemoteException
{
Book list[]=istanza.getList();
int size=list.length-carrello.size();
//numero di libri rimasti ancora non acquistati
Book retValue[]=new Book[size];
int j=0;
for(int i=0;i<list.length;i++)
{
if (carrello.indexOf(list[i])<0)
//verifica sei i libri sono nel carrello o meno
retValue[j++]=list[i];
//se non ci sono aggiorna l’array dei libri
//ancora disponibili
}
return(retValue);
}
Listato 6.7: BookOrderBean.java
200
6.5 Package Archivio
201
public boolean closeOrder(String numCarta, int tipo)
throws RemoteException
{
try
{
if(!istanza.checkCreditCard(user,numCarta,tipo))
//se la cartaè sbagliata non chiude l’ordine
return(false);
TopicSession session=connection.createTopicSession
(false,Session.AUTO_ACKNOWLEDGE);
TopicPublisher publisher =
session.createPublisher(destination);
ObjectMessage message=session.createObjectMessage();
message.setObject(new Order(user,carrello));
publisher.send(message);
return(true);
}
catch(JMSException err)
{
err.printStackTrace();
return(false);
}
}
private void createJMSConnection()
throws NamingException,JMSException
{
Context ctx = new InitialContext();
TopicConnectionFactory cf =
(TopicConnectionFactory)ctx.
lookup("ConnectionFactory");
connection = cf.createTopicConnection();
destination =
(javax.jms.Topic)ctx.lookup("topic/ordiniTopic");
}
Listato 6.8: BookOrderBean.java
6.5 Package Archivio
package BookOrderMDB;
import
import
import
import
javax.jms.*;
javax.ejb.*;
java.io.*;
Archivio.*;
public class BookMDBean
implements MessageListener, MessageDrivenBean
{
private MessageDrivenContext ctx;
private static final String nomeFileLog="/orders.log";
public void setMessageDrivenContext
(MessageDrivenContext parm1) throws EJBException
{
ctx=parm1;
}
public void ejbCreate()
{
}
public void ejbRemove()
throws EJBException
{
System.out.println("BookMDBean:remove()");
}
Listato 6.9: BookMDBean.java
202
6.5 Package Archivio
203
public void onMessage(Message m)
{
if (m instanceof ObjectMessage)
{
try
{
ObjectMessage om=(ObjectMessage)m;
Order unOrdine=(Order)om.getObject();
FileWriter fw=new FileWriter(nomeFileLog,true);
PrintWriter pw=new PrintWriter(fw,true);
pw.print(unOrdine.getUser());
pw.print(":");
String[] books=unOrdine.getBooks();
if (books.length==0)
return;
pw.print("Ã"+books[0]);
for(int i=1;i<books.length;i++)
pw.print(",Ã"+books[i]);
pw.println();
fw.close();
}
catch(JMSException e)
{
e.printStackTrace();
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
}
Listato 6.10: BookMDBean.java
6.6 Package BookStoreSwing
204
definisce il “magazzino”, cioè i libri che abbiamo a disposizione per gli
utenti. Tale classe restituisce le caratteristiche di un libro il cui nome
è dato da bookName.
DataLayerReg: in tale classe si abilita il collegamento tra server e client
RMI, l’applicazione instanzia un oggetto remoto e lo registra tramite
un bind all’interno dell’rmi registry.
6.6
Package BookStoreSwing
Il package BookStoreSwing contiene al suo interno le seguenti classi
Application: è la classe contenente il metodo main, al cui interno si inizializza una nuova istanza della classe Principale. E’ da tale classe che
parte l’esecuzione di tutta l’applicazione.
Principale: classe che estende JFrame, cioè eredita tutte le proprietà di
JFrame. Non contiene metodi, ma all’interno del suo costruttore ha
sia la creazione di nuove istanze dei diversi componenti che andranno
a comporre l’interfaccia principale che visualizzerà l’utente nell’utilizzo dell’applicazione, sia l’implemantazione della gestione degli eventi,
richiamando l’ascoltatore definito nella classe PrincipaleListener.
PrincipaleListener: tale classe implementa gli ActionListener, cioè definisce
le funzioni dichiarate nell’interfaccia ActionListener.
Nel metodo actionPerformed di tale classe vengono definite le azioni
che saranno eseguite dal sistema a seconda dell’evento catturato. Tali
azioni sono definite richiamando i metodi start(), chiudi() ed
elimina() definiti sempre all’interno di tale classe.
OrderListener: in questa classe si definisce una comunicazione JMS attraverso un topic (cioè attraverso un canale virtuale), tramite il metodo
onMessage si definisce il formato dei messaggi da ricevere.
6.7
Modulo Accesso.war
Nel progetto è inoltre presente il modulo Accesso.war. Tale modulo implementa la view, cioè sono state aggiunte delle servlet e delle JSP per permettere agli utenti di visionare ciò che si ottiene dal “dialogo” con i Beans. Le
classi appartenenti a questo modulo sono:
6.7 Modulo Accesso.war
package Archivio;
public class Book implements java.io.Serializable
{
private String titolo;
private String[] listaAutori;
private float costo;
public Book(String unTitolo, String[] autori,
float unCosto)
{
titolo=unTitolo;
listaAutori=autori;
costo=unCosto;
}
public String getTitolo()
{
return(titolo);
}
public String[] getListaAutori()
{
return(listaAutori);
}
public float getCosto()
{
return(costo);
}
public boolean equals(Object unAltro)
{
if (!(unAltro instanceof Book))
return(false);
return(((Book)unAltro).titolo.equals(titolo));
}
}
Listato 6.11: Book.java
205
6.7 Modulo Accesso.war
206
package Archivio;
import java.rmi.*;
public interface DataLayer extends Remote
{
boolean checkUser(String nome, String passwd)
throws RemoteException;
Book[] getList()
throws RemoteException;
boolean checkCreditCard
(String nome, String numCarta, int tipo)
throws RemoteException;
Book getBookByName(String bookName)
throws RemoteException;
}
Listato 6.12: DataLayer.java
Order.jsp In tale JSP viene realizzata la gestione delle ordinazioni. Tale
gestione comprende la possibilità di ottenere diversi servizi come la lista
dei libri ordinati ( contenuta in carrello), la possibilità di eliminare degli
ordini, etc. Come noto, una JSP è una pagina web contente sia codice
HTML che codice Java, il quale viene compilato solo al momento della
prima invocazione della pagina JSP generando una Servlet. In questo
file abbiamo principalmente codice HTML, mentre le richieste Java e
le chiamate ai Java Beans sono contenute in appositi tag.
Pagamento.jsp In questa pagina avviene la gestione dei pagamenti. All’utente si presenta una form contenente la richiesta di diverse informazioni relative alla sua identità ed alla carta di credito utilizzata per
i pagamenti. Nell’esempio considerato l’identità è “cablata” nel codice
( sono abilitati solo alcuni utenti).
Controller In tale classe viene implementata una Servlet. Controller estende HttpServlet, cioè eredita le proprietà dell’interfaccia HttpServlet.
In questa classe si ha solo l’implementazione del metodo service()
il quale realizza la comunicazione bidirezionale con i client tramite i
suoi due parametri: HttpServletRequest che contiene i dati inviati dal
client, e HttpServletResponse che rappresenta la risposta della Servlet
al client. All’interno di service viene dapprima richiamata la sessione,
6.7 Modulo Accesso.war
package Archivio;
import java.rmi.server.*;
import java.rmi.*;
public class DataLayerImpl extends
UnicastRemoteObject implements DataLayer
{
private static String[][] userList=
{
{ "deÃLeoni", "Max" },
{ "Russo", "Ruggero" },
{ "Mecella", "One" },
{ "Topolino", "313" },
{ "Paperino", "Qui" }
};
private static Book[] bookList=new Book[5];
public DataLayerImpl()
throws RemoteException
{
super();
String[] autori1={"deÃLeoni","Russo","Mecella"};
bookList[0]=new Book("J2EE",autori1,100);
String[] autori2={"e","CayÃHorstmann"};
bookList[1]=new Book("ConcettiÃdiÃinformaticaÃe
Fondamenti di Java 2",autori2,40);
String[] autori3={"P.ÃYao","D.ÃDurant"};
bookList[2]=new Book(".NETÃCompactÃProgramming",
autori3,50);
String[] autori4={"AA.VV."};
bookList[3]=new Book("XYZ",autori4,2);
String[] autori5={"S.ÃAmbler","T.ÃJewell"};
bookList[4]=new Book("MasteringÃEJB",autori5,77);
}
Listato 6.13: DataLayerImpl.java
207
6.7 Modulo Accesso.war
public boolean checkUser(String nome, String passwd)
throws RemoteException
{
for(int i=0;i<userList.length;i++)
if(userList[i][0].equals(nome))
return(userList[i][1].equals(passwd));
return(false);
}
public Book[] getList()
throws RemoteException
{
return(bookList);
}
public boolean checkCreditCard
(String nome, String numCarta, int tipo)
throws RemoteException
{
if (numCarta.length()!=16)
return(false);
char array[]=numCarta.toCharArray();
for (int i=0;i<array.length;i++)
if (array[i]<’0’ || array[i]>’9’)
return(false);
return(true);
}
public Book getBookByName(String bookName)
throws RemoteException
{
for(int i=0;i<userList.length;i++)
if(bookList[i].getTitolo().equals(bookName))
return(bookList[i]);
return(null);
}
}
Listato 6.14: DataLayerImpl.java
208
6.7 Modulo Accesso.war
package Archivio;
import java.rmi.*;
public class DataLayerReg
public static void main(String[] args)
throws Exception
{
DataLayerImpl obj=new DataLayerImpl();
Naming.rebind("rmi://localhost:2000/Archivio",obj);
// NUMERO DI PORTA!
}
Listato 6.15: DataLayerReg.java
public class Application {
public static void main(String[] args)
{
new Principale();
}
}
Listato 6.16: Application.java
209
6.7 Modulo Accesso.war
import javax.swing.*;
import java.awt.*;
import Archivio.*;
public class Principale extends JFrame {
public JList lista;
public DefaultListModel model;
private JPanel sudPnl=new JPanel();
private JScrollPane listScrollPane;
public JButton startBtn=new JButton("Start");
public JButton chiudiBtn=new JButton("Chiudi");
public JButton eliminaBtn=new JButton("Processato");
private PrincipaleListener ascoltatore;
public Principale()
{
super("ClientÃordini");
model=new DefaultListModel();
lista = new JList(model);
listScrollPane = new JScrollPane(lista);
sudPnl.add(startBtn);
ascoltatore=new PrincipaleListener(this);
sudPnl.add(eliminaBtn);
sudPnl.add(chiudiBtn);
startBtn.addActionListener(ascoltatore);
chiudiBtn.addActionListener(ascoltatore);
eliminaBtn.addActionListener(ascoltatore);
startBtn.setActionCommand(ascoltatore.START);
chiudiBtn.setActionCommand(ascoltatore.CHIUDI);
eliminaBtn.setActionCommand(ascoltatore.ELIMINA);
getContentPane().add
(listScrollPane,BorderLayout.CENTER);
getContentPane().add(sudPnl,BorderLayout.SOUTH);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
chiudiBtn.setEnabled(false);
eliminaBtn.setEnabled(false);
setSize(300,300);
setVisible(true);
}
}
Listato 6.17: Principale.java
210
6.7 Modulo Accesso.war
import java.awt.event.*;
import javax.swing.*;
import Archivio.*;
import javax.jms.JMSException;
importjavax.naming.NamingException;
public class PrincipaleListener
implements ActionListener {
private Principale frame;
public static final String START="S";
public static final String CHIUDI="C";
public static final String ELIMINA="E";
private OrderListener jmsListener=null;
public PrincipaleListener(Principale unFrame) {
frame=unFrame;
}
public void actionPerformed(ActionEvent e)
{
try
{
String com=e.getActionCommand();
if (com == START)
start();
else if (com == CHIUDI)
chiudi();
else if (com == ELIMINA)
elimina();
}
catch(Exception err)
{
JOptionPane.showMessageDialog
(frame,err.getMessage(),"LanciataÃeccez.",
JOptionPane.ERROR_MESSAGE);
err.printStackTrace();
}
}
Listato 6.18: PrincipaleListener.java
211
6.7 Modulo Accesso.war
212
private void start()
throws NamingException,JMSException
{
jmsListener=new OrderListener
(frame.model,"topic/ordiniTopic");
frame.startBtn.setEnabled(false);
frame.chiudiBtn.setEnabled(true);
frame.eliminaBtn.setEnabled(true);
}
private void chiudi() throws JMSException
{
jmsListener.stop();
frame.dispose();
}
private void elimina()
{
/*Restituisce l’indice dell’elemento selezionato
(-1 se nessun indiceè stato selezionato)*/
int index=frame.lista.getSelectedIndex();
if (index==-1)
/*Se nonè stato selezionato alcun indice, allora
errore, altrimenti lo visualizza*/
{
JOptionPane.showMessageDialog
(frame,"NessunÃindiceÃselezionato","Errore",
JOptionPane.ERROR_MESSAGE);
}
else
frame.model.remove(index);
}
}
Listato 6.19: PrincipaleListener.java
6.7 Modulo Accesso.war
import
import
import
import
import
javax.jms.*;
javax.naming.*;
javax.swing.*;
java.util.Properties;
Archivio.*;
public class OrderListener implements MessageListener {
private DefaultListModel listModel;
private TopicConnection connection = null;
public OrderListener
(DefaultListModel model, String topicName)
throws NamingException, JMSException
{
listModel = model;
InitialContext ctx = null;
TopicConnectionFactory cf = null;
TopicSession session = null;
Topic destination = null;
TopicSubscriber subscriber = null;
Properties properties = new Properties();
properties.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
properties.put
(Context.URL_PKG_PREFIXES, "org.jnp.interfaces");
properties.put(Context.PROVIDER_URL, "localhost");
ctx = new InitialContext(properties);
cf = (TopicConnectionFactory)ctx.
lookup("ConnectionFactory");
destination = (Topic)ctx.lookup(topicName);
connection = cf.createTopicConnection();
session = connection.createTopicSession(false,
Session.AUTO_ACKNOWLEDGE);
subscriber = session.createSubscriber(destination);
subscriber.setMessageListener (this);
connection.start();
}
Listato 6.20: OrderListener.java
213
6.7 Modulo Accesso.war
public void onMessage(Message mex)
{
try
{
if (mex instanceof ObjectMessage)
{
ObjectMessage objMex=(ObjectMessage)mex;
Object obj=objMex.getObject();
if (obj instanceof Order)
{
Order ordine=(Order)obj;
String text=ordine.getUser()+":";
String lista[]=ordine.getBooks();
for(int i=0;i<lista.length;i++)
{
text+="Ã"+lista[i];
}
listModel.addElement(text);
}
}
}
catch(JMSException jmsExc)
{
listModel.addElement
("Errore:Ã"+jmsExc.getMessage());
}
}
public void stop() throws JMSException
{
connection.close();
}
}
Listato 6.21: OrderListener.java
214
6.8 Il deployment descriptor
215
dopodichè viene aggiunto un nuovo elemento al carrello virtuale di spesa
dell’utente.
PagamentoServlet Anche in tale classe si ha l’implementazione di una
Servlet e quindi l’implementazione del metodo service(). In questa
Servlet prima di tutto viene richiamata la sessione, dopodichè si verifica
la tipologia di pagamento e se è avvenuto con successo.
ConfirmServlet In tale classe viene implementata la Servlet necessaria alla
verifica dei dati inseriti dall’utente per fare il login. Se il login ha avuto
esito positivo allora viene inizializzato il contesto JNDI.
DeleteBook L’implementazione di tale Servlet serve per eliminare degli elementi dal carrello di spesa dell’utente. Anche in tal caso abbiamo
l’implementazione del metodo service(), nel quale come prima cosa
appare la chiamata alla sessione.
6.8
Il deployment descriptor
Essendo un singolo EJB composto da diversi bean, i deployment descriptor
sono uniti in un singolo file. L’ejb-jar file di questa applicazione è mostrato
nel Listato 6.34. Il file specifico per JBoss è jboss.xml (Listato 6.33).
Di seguito vengono riportati i file jboss.xml e ejb-jar.xml.
6.9
Provare l’applicazione
Per provare l’applicazione, innanzitutto bisogna compilare e avviare RMI.
I passi da seguire per la compilazione sono i seguenti:
Per compilare RMI:
1. Compilare i file ponendosi nella directory ...\RMIBookStore con l’istruzione
javac Archivio\*.java
2. Compilare l’implementazione dell’interfaccia DataLayerImpl(in questo
modo viene prodotto DataLayerImpl stub.class) attraverso l’istruzione
rmic Archivio.DataLayerImpl per la creazione dello stub
3. Copiare lo stub nella directory del client
6.9 Provare l’applicazione
<html>
<%@ page contentType="text/html;Ãcharset=iso-8859-1"
language="java" import="java.util.*,Ãjavax.naming.*,
java.io.*, BookOrder.*, Archivio.*"ÃerrorPage=""Ã%>
<head> <title>Gestione delle Ordinazioni</title> </head>
<body bgcolor="#FFFFCC" text="#000000">
</style> </head>
<%
BookOrder carrello =
(BookOrder)session.getAttribute("carrello");
if (carrello==null)
{
out.println("EffettuareÃilÃ<aÃhref=\"index.html\">
login</a> prima di continuare!");
return;
}
Book[] bookList = (Book[])carrello.getCarrello();
%>
<b>Libri nel Carrello:</b>
<table width="41%" border="0">
<tr>
<td>
<form method="get" action="DeleteBook">
<select size="3" name="DelLibro">
<%
for(int i=0;i<bookList.length;i++)
{
out.print("<option>");
out.print(bookList[i].getTitolo());
out.println("</option>");
}
%>
Listato 6.22: Order.jsp
216
6.9 Provare l’applicazione
</select></td>
<td align="center" valign="middle"> </td>
</tr>
<tr>
<td width="45%" align="center" valign="middle"></td>
<td width="55%" align="center" valign="middle"></td>
</tr>
<tr>
<td align="center" valign="middle">
<input type="submit" value="Delete..."></td>
</form>
<form method="get" action="Pagamento.jsp">
<td align="center" valign="middle">
<input type="submit" value="ProcediÃalÃpagamento"></td>
</form>
Conto: <%= carrello.getConto() %>
<td align="center" valign="middle"></td>
</tr>
</table>
<p> </p> <p><b>Libri Disponibili:</b> </p>
<table width="41%" border="0">
<tr>
<td align="center">
<form method="get" action="Controller">
<select size="3" name="Biblioteca">
<%
Book[] avail=carrello.getAvailableBooks();
for(int i=0;i<avail.length;i++)
{
out.print("<option>");
out.print(avail[i].getTitolo());
out.println("</option>");
}
%>
Listato 6.23: Order.jsp
217
6.9 Provare l’applicazione
218
</select></td>
<td align="center" valign="middle"> </td>
</tr>
<tr>
<td width="45%" align="center" valign="middle"></td>
<td width="55%" align="center" valign="middle"></td>
</tr>
<tr>
<td align="center" valign="middle">
<input type="submit" value="AddÃto..."></td>
</form>
</tr>
<tr>
<td align="center" valign="middle"></td>
<td align="center" valign="middle"></td>
</tr>
</table> </body> </html>
Listato 6.24: Order.jsp
4. Mandare in esecuzione il registry con l’istruzione start rmiregistry 2000
(2000 è il numero di porta utilizzato nell’esempio considerato, è necessario scegliere il numero di porta perchè JBoss occupa quella utilizzata
di default da RMI)
5. Registrare il Remote Server Object attraverso l’istruzione java Archivio.DataLayerReg
Per la compilazione della servlet:
BookStore.jar
1. BookStore.jar: compilo i file in Archivio, da BookStore.jar compilo i file
in BookOrder: javac -classpath C:\<jboss home>\client\jboss-j2ee.jar;.
BookOrder\*.java javac -classpath.;C:\<jboss home>\client\jboss-j2ee.jar
BookOrderMDB\*.java
2. Accesso.war: compilo le classi java javac-classpath .; C:\<jboss home>\client\jbossj2ee.jar; C:\<jboss home>\client\javax.servlet.jar *.java
Infine per il deploy:
1. Deploy delle servlet: Accesso.war in <jboss home> \. . . \ deploy
6.9 Provare l’applicazione
<html>
<%@ page contentType="text/html;Ãcharset=iso-8859-1"
language="java", import="java.util.*,
javax.naming.*, java.io.*,
BookOrder.*, Archivio.*"ÃerrorPage=""Ã%>
<head> <title>Pagamento</title> </head> <body>
<%
BookOrder b =
(BookOrder)session.getAttribute("carrello");
%>
<form method=GET action="PagamentoServlet">
Nome:<input TYPE=text SIZE=30
NAME="Utente"><br><p>
Credit Card (16 cifre):
<input TYPE=text SIZE=30 NAME="CC"><p>
<tr>
<td>Tipo:</td><p>
<td>
<input type="radio" name="tipo"
value="0" checked>VISA<br>
<input type="radio" name="tipo"
value="1"checked>MASTERCARD<br>
<input type="radio" name="tipo"
value="2"checked>AMERICANEXPR<br><p>
</td>
</tr> <tr>
<td colspan=2>
<input type="submit" value="SendÃData">
</td>
</tr> </form> </body> </html>
Listato 6.25: Pagamento.jsp
219
6.9 Provare l’applicazione
import javax.servlet.*; import javax.servlet.http.*; import
java.io.*; import javax.naming.*; import BookOrder.*;
importjava.util.*; import javax.ejb.*; import java.net.*;
public class Controller extends HttpServlet {
public void service(HttpServletRequest req,
HttpServletResponse res)
throws IOException,ServletException
{
HttpSession s=req.getSession();
BookOrder b=(BookOrder)s.getAttribute("carrello");
String titolo=(String)req.getParameter("Biblioteca");
b.addBook(titolo);
String messaggio="LibroÃaggiuntoÃalÃcarrello";
String link="Order.jsp";
OutputStream os=res.getOutputStream();
PrintWriter out=new PrintWriter(os);
out.println("<HTML><HEAD><TITLE>OK</TITLE></HEAD>");
out.println("<BODY>");
out.println(messaggio);
out.println("<aÃhref=\"" +link+ "\">Continua</a>");
out.println("</BODY></HTML>");
out.close();
}
}
Listato 6.26: Controller.java
220
6.9 Provare l’applicazione
import javax.servlet.*; import javax.servlet.http.*; import
java.io.*; import javax.naming.*; import BookOrder.*; import
java.util.*; import javax.ejb.*; import java.lang.*;
public class PagamentoServlet extends HttpServlet {
public void service
(HttpServletRequest req, HttpServletResponse res)
throws IOException,ServletException
{
//richiamo la sessione
HttpSession s=req.getSession();
BookOrder b=(BookOrder)s.getAttribute("carrello");
String Uid=req.getParameter("Utente");
String CCString=req.getParameter("CC");
String tipoStringa=req.getParameter("tipo");
int tipoInt= Integer.parseInt(tipoStringa);
String Carta=null;
String messaggio=null;
String messaggio2=null;
String link=null;
if (tipoInt==0) Carta="VISA";
if (tipoInt==1) Carta="MASTERCARD";
if (tipoInt==2) Carta="AMERICANEXPR";
Listato 6.27: PagamentoServlet.java
221
6.9 Provare l’applicazione
if(b.closeOrder(CCString,tipoInt))
{
messaggio="haiÃinseritoÃiÃseguentiÃdati:Ã";
messaggio2="pagamentoÃavvenutoÃconÃsuccesso!!!";
link="index.html";
//Annullo la sessione Servlet/JSP
s.invalidate();
/*Notifico al Container che il Session Bean
nonè necessario oltre */
try
{
b.remove();
} catch(RemoveException e) {e.printStackTrace();}
}
else
{
messaggio="iÃseguentiÃdatiÃsonoÃerrati:Ã";
messaggio2="verificaÃl’inserimento";
link="Pagamento.jsp";
}
OutputStream os=res.getOutputStream();
PrintWriter out=new PrintWriter(os);
out.println("<HTML><HEAD><TITLE>
Pagina di conferma</TITLE></HEAD>");
out.println("<BODY>");
out.println("<b>"+messaggio+ "</b><br>");
out.println("<p>");
out.println("<b>Nome:Ã" +Uid+"</b><br>");
out.println("<p>");
out.println("<b>Carta:Ã"+Carta+ "</b><br><p>");
out.println("<b>"+messaggio2+"</b>");
out.println("<aÃhref=\"" +link+ "\">Continua</a>");
out.println("</BODY></HTML>");
out.close();
}
}
Listato 6.28: PagamentoServlet.java
222
6.9 Provare l’applicazione
223
import javax.servlet.*; import javax.servlet.http.*; import
java.io.*; import javax.naming.*; import BookOrder.*; import
java.util.*; import javax.ejb.*;
public class ConfirmServlet extends HttpServlet
//fa da client del BookOrderBean
{
public void service
(HttpServletRequest req, HttpServletResponse res)
throws IOException,ServletException
{
String Uid=req.getParameter("Uid");
String password=req.getParameter("Password");
String messaggio = "<b>loginÃavvenuto
con successo!</b>";
String link="Order.jsp";
OutputStream os=res.getOutputStream();
PrintWriter out=new PrintWriter(os);
HttpSession sess=req.getSession();
//recupero l’oggetto session
//porzione di codice per l’inizializzazione del contesto JNDI
Properties prop = new Properties();
prop.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
prop.put(javax.naming.Context.URL_PKG_PREFIXES,
"org.jnp.interfaces");
prop.put(javax.naming.Context.PROVIDER_URL,
"localhost");
Listato 6.29: ConfirmServlet.java
6.9 Provare l’applicazione
224
try{
//inizializzazione del contesto
javax.naming.Context ctx = new InitialContext(prop);
//acquisizione dell’interfaccia HOME
Object object = ctx.lookup("BookOrder");
BookOrderHome home =
(BookOrderHome)javax.rmi.PortableRemoteObject
.narrow(object, BookOrderHome.class);
//utilizzo l’interfaccia home per creare l’EJB
BookOrder bookServer = home.create(Uid,password);
sess.setAttribute("carrello",bookServer);
}
catch (CreateException e){
//inviata da BookOrderBean
messaggio="loginÃerrato!!!";
link="Index.html";
}
catch (NamingException e) {
messaggio="ImpossibileÃcreareÃcontesto:" +
e.toString();
}
out.println("<HTML><HEAD><TITLE>
Login Page</TITLE></HEAD>");
out.println("<BODY>");
out.println(messaggio);
out.println("<aÃhref=\"" +link+ "\">Continua</a>");
out.println("</BODY></HTML>");
out.close();
}
}
Listato 6.30: ConfirmServlet.java
6.9 Provare l’applicazione
import javax.servlet.*; import javax.servlet.http.*; import
java.io.*; import javax.naming.*; import BookOrder.*; import
java.util.*; import javax.ejb.*; import java.net.*;
//import Archivio.*;
public class DeleteBook extends HttpServlet {
public void service
(HttpServletRequest req, HttpServletResponse res)
throws IOException,ServletException
{
HttpSession s=req.getSession();
BookOrder b=(BookOrder)s.getAttribute("carrello");
String titolo=(String)req.getParameter("DelLibro");
b.delBook(titolo);
String messaggio="LibroÃeliminatoÃdalÃcarrello";
String link="Order.jsp";
/* ServletContext app = getServletContext();
RequestDispatcher disp;
disp = app.getRequestDispatcher("/Order.jsp");
OutputStream os=res.getOutputStream();
PrintWriter out=new PrintWriter(os);
out.println("<HTML><HEAD><TITLE>
Libro eliminato</TITLE></HEAD>");
out.println("<BODY>");
out.println(messaggio);
out.println("<a href=\"" +link+ "\">Continua</a>");
out.println("</BODY></HTML>");
out.close();
}
}
Listato 6.31: DeleteBook.java
225
6.9 Provare l’applicazione
<?xml version="1.0" encoding="UTF-8"?>
<web-appxmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<servlet>
<servlet-name>ConfirmServlet</servlet-name>
<servlet-class>ConfirmServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ConfirmServlet</servlet-name>
<url-pattern>/ConfirmServlet</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>Controller</servlet-name>
<servlet-class>Controller</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Controller</servlet-name>
<url-pattern>/Controller</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>DeleteBook</servlet-name>
<servlet-class>DeleteBook</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DeleteBook</servlet-name>
<url-pattern>/DeleteBook</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>PagamentoServlet</servlet-name>
<servlet-class>PagamentoServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PagamentoServlet</servlet-name>
<url-pattern>/PagamentoServlet</url-pattern>
</servlet-mapping>
</web-app>
Listato 6.32: web.xml
226
6.9 Provare l’applicazione
227
<?xml version="1.0" encoding="UTF-8" ?>
<jboss>
<enterprise-beans>
<session>
<ejb-name>BookOrderEJB</ejb-name>
<jndi-name>BookOrder</jndi-name>
</session>
<message-driven>
<ejb-name>BookMDBean</ejb-name>
<destination-jndi-name>
topic/ordiniTopic
</destination-jndi-name>
</message-driven>
</enterprise-beans>
</jboss>
Listato 6.33: jboss.xml
2. Deploy del topic ordiniTopic-service.xml in <jboss home>\. . . \deploy
3. Deploy dell’EJB: BookOrder.jar in <jboss home> \. . . \deploy
Per la compilazione dell’interfaccia per il venditore: BookStoreSwing
1. Compilo le classi java javac -classpath C:\<jboss home>\client\jbossallclient.jar;. *.java
2. Per visualizzare l’interfaccia, mando in esecuzione la classe Application
contenente il main java -classpath C:\<jboss home>\client\jbossall-client.jar;.
Application
6.9 Provare l’applicazione
<!DOCTYPE ejb-jar PUBLIC ’-//SunÃMicrosystems,ÃInc.//DTD
Enterprise JavaBeans 2.0//EN’ ’http
://java.sun.com/dtd/ejb-jar_2_0.dtd’>
<ejb-jar>
<enterprise-beans>
<session>
<ejb-name>BookOrderEJB</ejb-name>
<home>BookOrder.BookOrderHome</home>
<remote>BookOrder.BookOrder</remote>
<ejb-class>BookOrder.BookOrderBean</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
</session>
<message-driven>
<ejb-name>BookMDBean</ejb-name>
<ejb-class>BookOrderMDB.BookMDBean</ejb-class>
<message-driven-destination>
<destination-type>
javax.jms.Topic
</destination-type>
</message-driven-destination>
<transaction-type>Container</transaction-type>
</message-driven>
</enterprise-beans>
</ejb-jar>
Listato 6.34: ejb-jar.xml
228
Capitolo 7
Web Service
Secondo la definizione data dal W3C 1 un Web Service (servizio web) è un
sistema software progettato per supportare l’interoperabilità tra diversi elaboratori su di una medesima rete. Il concetto di Web Service è molto simile
a quello di oggetto, nel significato con cui lo si usa nella programmazione
ad oggetti: un oggetto è un modulo software che offre una serie di funzioni
utilizzabili dall’esterno da parte di un altro software tramite un interfaccia
di comunicazione dichiarata dall’oggetto stesso. Allo stesso modo anche un
Web Service offre una funzionalità, un servizio, ad altri client sulla rete attraverso un’interfaccia ben definita tramite il protocollo HTTP. Caratteristica
fondamentale di un Web Service è quella di offrire un’interfaccia software
basata su XML (descritta in un formato automaticamente elaborabile quale,
ad esempio, il WSDL) tramite la quale comunica le funzioni che mette a
disposizione e le relative modalità di utilizzo. L’XML può essere utilizzato
correttamente tra piattaforme differenti (Linux, Windows, Mac) e differenti
linguaggi di programmazione. XML è inoltre in grado di esprimere messaggi e funzioni anche molto complesse e garantisce che tutti i dati scambiati
possano essere utilizzati ad entrambi i capi della connessione, essendo uno
standard facilmente interpretabile grazie a software specifici; si può quindi
dire che i Web Service sono basati su XML ed HTTP e che possono essere
utilizzati su ogni piattaforma e con ogni tipo di software. Utilizzando tale
interfaccia, altri sistemi possono interagire con il Web Service invocando le
operazioni descritte nell’interfaccia tramite appositi messaggi inclusi in una
busta SOAP: tali messaggi sono solitamente trasportati tramite il protocollo
HTTP e formattati secondo lo standard XML. Questi messaggi sono utilizzati come un contenitore (envelope), dove le applicazioni inseriscono ogni
informazione che deve essere spedita. Ogni envelope è formato da due par1
The World Wide Web Consortium
229
7.1 Le tecnologie alla base dei Web Services
230
Figura 7.1: Rappresentazione schematica di un messaggio SOAP
ti: un’intestazione (header ) ed un corpo del messaggio (body) (Figura 7.1)
L’header è opzionale, il body invece è obbligatorio, tutti i messaggi SOAP
devono avere un corpo del messaggio. Sia l’header che il body possono avere
sottostrati multipli nella forma di header blocks o body blocks. Il protocollo
SOAP assume che ogni messaggio abbia un mittente ed un destinatario finale,
e che un numero arbitrario di intermediari (detti nodi), processano il messaggio e lo instradano al destinatario. L’informazione che il mittente vuole
trasmettere al destinatario sarà incapsulata nel body del messaggio, invece,
ogni informazione aggiuntiva necessaria per il processamento intermedio è
posta nell’header. L’idea base è quella di qualsiasi protocollo standard di
comunicazione. La differenza sostanziale tra un servizio web offerto in Internet e un Web Service sta nel tipo di fornitore del servizio. Per i servizi
web (come per esempio per gli accessi a banche o ad e-mail) è la persona fisica che accede direttamente attraverso una form html, mentre i Web Service
sono a disposizione di programmi che, tramite il protocollo SOAP, si scambiano informazioni e quindi l’integrazione con il servizio avviene attraverso
lo scambio di messaggi sulla rete.
7.1
Le tecnologie alla base dei Web Services
Il protocollo di base per i Web Service è HTTP. Questo protocollo si occupa
di mettere in comunicazione il Web Service con l’applicazione che intende
usufruire delle sue funzioni. I Web Service si basano su tre tecnologie di
7.1 Le tecnologie alla base dei Web Services
231
fondamentali:
SOAP (Simple Object Access Protocol) per la comunicazione. Le interazioni con i Web Service sono basate sul Simple Object Access Protocol
(SOAP) per la definizione dei messaggi. Mediante l’uso di tale protocollo, è possibile lo scambio di messaggi usando uno standard che
permetta la conversione del servizio in messaggi XML. SOAP sta al
di sopra del livello applicativo/trasporto e un suo punto di forza è che
questo protocollo può essere usato insieme a una varietà di protocolli
(http, smtp, ftp, jabber e cosı̀ via). Il più usato è solitamente HTTP
grazie alla sua vasta diffusione è adatto allo sviluppo di Web Service.
Questo è dovuto alla sua semplicità, alla robustezza e alla possibilità
di viaggiare sulla rete evitando problematiche legate ai firewall, come
invece può accadere usando protocolli come CORBA o RMI.
WSDL (Web Services Description Language) per la descrizione dell’interfaccia del servizio. Il Web Service Definition Language (WSDL) è il
modo standardizzato in cui viene descritto il servizio e in cui vengono
mostrate le sue interfacce. Questo protocollo è un XML-based IDL
(Interface Definition Language) che introduce delle estensioni rese necessarie dalla mancanza di un middleware centralizzato. Attraverso il
WSDL è possibile conoscere i metodi supportati dal Web Service e quali
sono i loro input ed output. Grazie ad esso, è possibile generare lo stub
e gli strati intermedi che rendono la chiamata al Web Service trasparente. In generale WSDL cerca di separare gli aspetti “astratti” della
descrizione di un servizio da quelli “concreti” che lo legano a particolari
protocolli di rete o di RPC.
UDDI (Universal Description, Discovery and Integration) Per poter utilizzare i Web Service in modo dominante e su scala globale è necessario standardizzare i registri dei Web Service. Questo standard
prende il nome di Universal Description Discovery and Indegration
(UDDI), ovvero un linguaggio che rappresenta il repository che contiene
il servizio. Tramite esso, gli utenti possono ricercare servizi adatti alle
proprie necessità. Un UDDI registry è utilizzato con il significato di
scoperta dei Web Service descritti usando WSDL. L’idea è che l’UDDI
registry può essere cercato in vari modi per ottenere il contatto dell’informazione e la disponibilità dei Web Service per varie organizzazioni.
L’UDDI potrebbe essere un modo di tenere aggiornato il Web Service
che l’organizzazione ha appena utilizzato.
Questi tre protocolli aperti sono alla base della interoperabilità tra i diversi
sistemi software e tra le diverse piattaforme hardware. Grazie a questi proto-
7.1 Le tecnologie alla base dei Web Services
232
colli indipendenti dal linguaggio di programmazione o dal sistema operativo,
è possibile far coesistere per esempio un client scritto in C con un server
scritto in Java. Questo avviene grazie all’XML su cui, come già detto, sono
basati i protocolli utilizzati.
Nei prossimi paragrafi entreremo più nel dettaglio dei tre elementi caratterizzanti il Web Service e descritti sommariamente nel presente paragrafo.
7.1.1
SOAP: Simple Object Access Protocol
Questo protocollo fornisce una via di comunicazione tra applicazioni eseguite
su sistemi operativi diversi, con diverse tecnologie e linguaggi di programmazione, tramite HTTP ed XML.
Acronimo di Simple Object Access Protocol, è uno standard nato in casa
Microsoft e supportato in una seconda fase da IBM. Ora è uno standard
W3C.
Un messaggio SOAP è un documento XML che contiene i seguenti elementi:
1. Envelope, identifica il documento come un messaggio SOAP
2. Un elemento Header opzionale contenente informazioni specifiche per
l’applicazione che permette di definire alcuni messaggi anche con diversi
destinatari nel caso il messaggio dovesse attraversare più punti di arrivo
3. Body, elemento indispensabile che contiene le informazioni scambiate
dalle richieste/risposte
4. Fault, elemento opzionale che fornisce informazioni riguardo ad eventuali errori manifestati durante la lettura del messaggio
Le regole principali per realizzare un messaggio SOAP sono le seguenti:
1. Deve essere codificato con XML;
2. Non deve contenere il collegamento ad un DTD2 e non deve contenere
istruzioni per processare XML
Nelle specifiche SOAP convivono quattro diversi aspetti:
1. Il modello di scambio dei messaggi definisce un modo standard per
definire come strutturare messaggi XML che saranno scambiati tra i
diversi nodi, come gestire gli errori e come chiamare i tag XML che
contengono informazioni.
2
Document Type Definition, definisce le componenti ammesse nella costruzione di
un documento XML
7.1 Le tecnologie alla base dei Web Services
233
<?xml version="1.0"?>
<SOAP-ENV:Envelope
smlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/>
<SOAP-ENV:Body>
<ns1:getRate xmlns:ns1="urn:xmethods-CurrencyExcange">
<contry1>Inghilterra</contry1>
<contry2>Giappone</contry2>
</ns1:getRate>
<SOAP-ENV:Body>
<SOAP-ENV:Envelope>
}
Listato 7.1: Una richiesta SOAP
2. L’encoding SOAP fornisce una specifica (opzionale) per definire come
strutture dati vengono rappresentate in XML. Essa è basata su una
prima bozza delle specifiche XML Schema.
3. Il collegamento tra SOAP e HTTP. Il collegamento HTTP definisce il
modo in cui i messaggi SOAP debbano essere veicolati e come devono
essere utilizzate le intestazioni HTTP e i codici di ritorno per l’invio di
messaggi SOAP.
4. L’utilizzo di SOAP come meccanismo di RPC. Il modello RPC definisce
il modo in cui SOAP può essere utilizzato per rappresentare chiamate a
procedure remote. Le specifiche SOAP consentono infatti di realizzare
applicazioni distribuite di semplice messaggistica o più evolute.
Anatomia del messaggio
La comunicazione tramite SOAP avviene con lo scambio di messaggi strutturati in modo specifico comprendente elementi obbligatori ed elementi opzionali. Il tag principale che racchiude tutti gli altri è Envelope. Dentro questo
sono presenti Header e Body. Il primo è opzionale, mentre nel corpo sono
presenti tutte le informazioni applicative che il nodo vuole inviare.
Nel listato 7.1 è presente un semplice messaggio SOAP. I tag Envelope e
Body appartengono al namespace http://schemas.xmlsoap.org/soap/envelope.
7.1 Le tecnologie alla base dei Web Services
234
<?xml version="1.0"?>
<SOAP-ENV:Envelope
smlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/>
<SOAP-ENV:Body>
<ns1:getRateResponse xmlns:ns1=
"urn:xmethods-CurrencyExcange">
<return>154.9423</return>
</ns1:getRate>
<SOAP-ENV:Body>
<SOAP-ENV:Envelope>
}
Listato 7.2: La risposta
L’utilizzo di SOAP-ENV è semplicemente una convenzione: al suo posto si
sarebbe potuto utilizzare qualsiasi nome simbolico, come ad esempio ns1,
utilizzato nel corpo del messaggio.
Come si può notare, all’interno del tag Body è definito il messaggio applicativo: nel tag getRate sono presenti i tag country1 e country2. Questo
blocco SOAP potrebbe essere un messaggio utilizzato in una applicazione
che si occupa di fornire il tasso di conversione tra diverse valute.
La risposta SOAP (Listato 7.2) è strutturata in modo da simulare una
risposta applicativa.
Per convenzione, il nome del tag utilizzato nella risposta è lo stesso della
richiesta, a cui è aggiunta la parola Response.
Dopo aver visto come è strutturato un messaggio SOAP, cerchiamo ora di
capire dove dev’essere inviato il messaggio SOAP di richiesta per contattare
il Web service e come si collega questo documento SOAP ad HTTP per
raggiungere il Web service.
Per rispondere bene a queste domande bisogna ricordare il tag <service>
e gli attributi soapAction impostati all’interno del binding.
Partiamo dal tag <service>. Al suo interno si sceglie il nome del nostro
Web Service e si crea una porta tramite l’elemento <port> collegandola al
binding SOAP. All’interno di quella porta si definisce un indirizzo scrivendo
questa riga: <soap:address location=’’http://www.html.it/guida ai Web
7.1 Le tecnologie alla base dei Web Services
235
services/esempio.php/’’>
L’attributo location indica ad un applicazione, che intende sfruttare il
Web Service dove deve inviare le sue richieste HTTP.
A questo punto SOAP viene incapsulato nel protocollo HTTP ed inviato
sulla rete per raggiungere il Web Service. All’URL fornito naturalmente il
Web Service è in attesa di ricevere delle richieste ed è pronto a dare le risposte
più appropriate.
Per far comprendere al Web Service quale azione deve intraprendere,
l’applicazione aggiunge all’header HTTP una stringa in grado di indicare
l’azione SOAP da intraprendere (cosı̀ come viene inserita nel binding per
ogni operazione definita).
7.1.2
WSDL: Web Service Definition Language
Un altro elemento fondamentale nell’insieme di tecnologie che supportano i
Web Service è lo standard WSDL (Web Service Definition Language). Esso
consente di descrivere un servizio in tutti i suoi aspetti. In un documento
WSDL si trovano tutte le chiamate che si possono fare ad un Web Service,
le specifiche delle strutture dati di input e output e le URL per accedere
ai servizi. WSDL, a differenza di SOAP, non è uno standard legato a un
livello di trasporto particolare ma è aperto all’utilizzo con protocolli differenti, specificando di volta in volta i bindings (collegamenti) verso il protocollo
adottato. Concretamente, la descrizione di un Web Service è composta da:
1. I binding che legano ciascuna porta astratta a un protocollo e a un
formato dati per i messaggi scambiati;
2. Dalle porte che legano le porte astratte istanziate dai binding ad indirizzi di rete reali;
3. I servizi composti da insiemi di porte correlate (che spesso offrono
modalità differenti di accesso alla stessa tipologia di servizio).
La definizione astratta dei Web Service di WSDL è composta da cinque
diversi elementi:
- I tipi (types) cioè strutture dati utilizzate come elementi di base per la
costruzione dei messaggi di input e output;
- I messaggi (messages) sono gli elementi che costituiscono gli input e gli
output dei servizi;
- Le operazioni (portType) sono le funzionalità offerte dal servizio;
7.1 Le tecnologie alla base dei Web Services
236
- I collegamenti (bindings) utilizzati per la mappatura del servizio astratto;
- La definizione del servizio (service) necessaria per raccogliere tutte le
operazioni sotto un unico nome.
Lo standard WSDL privilegia il protocollo SOAP, indicando in una sezione
delle sue specifiche le modalità di collegamento a questo standard. Avendo
a disposizione un documento WSDL, un sistema software evoluto può dinamicamente invocare un Web Service ed elaborarne il risultato. Questo
tipo di informazioni sono quelle che dovranno essere presenti all’interno del
registry dei Web Service e costituiscono le specifiche tecniche per l’accesso a
un servizio.
L’utilizzo di WSDL ha diversi vantaggi tra cui la possibilità di avere a
disposizione strumenti di sviluppo che sono in grado di prendere in input
file WSDL e produrne codice che implementa l’infrastruttura per la realizzazione del servizio, inoltre è un modo per disaccoppiare il Web Service dal
protocollo di trasporto e dai percorsi fisici. Come accennato, nel file WSDL
è presente l’indicazione del protocollo da utilizzare (per esempio un sistema
di runtime che supporti diversi protocolli di comunicazione XML potrebbe
capire dal WSDL come ricondurre a messaggi SOAP reali le rappresentazioni
astratte contenute nel documento WSDL). Un servizio potrebbe infatti partire utilizzando SOAP come protocollo per poi passare ad un nuovo tipo di
protocollo con l’intento di ottimizzarne le prestazioni, ad esempio per via
della sua potenziale minore occupazione di banda.
All’interno di WSDL
Un documento WSDL è un file XML contenente un insieme di definizioni.
L’aspetto principale del WSDL è costituito dall’insieme di regole che consentono di definire in modo astratto un Web Service. Non meno importanti
sono le specifiche per collegare il servizio a SOAP e HTTP.
Tipi
Per ogni messaggio scambiato nell’ambito delle operazioni WSDL deve essere
definito il tipo. I tipi definiti da WSDL sono l’equivalente delle strutture
(struct) del linguaggio C, cioè sono strutture dati anche complesse utilizzate
come elementi di base per costruire i messaggi di input e output definiti
nella sezione messaggi. Per definire i tipi usati all’interno del documento,
WSDL usa l’elemento types. All’interno di types può essere inserito un intero
7.1 Le tecnologie alla base dei Web Services
237
<types>
<schema targetNamespace="http://examples.com/stockquote.xsd"
xmlns="http://www.w3c.org/2000/10/XMLSchema">
<element name="TradePrinceRequest">
<complexType>
<all>
<element name="ticjedSymbol" type="string"/>
</all>
</complexType>
</element>
<element name="TradePrince">
<complexType>
<all>
<element name="price" type="float"/>
</all>
</complexType>
</element>
</schema>
</types>
Listato 7.3: il primo elemento dei WSDL: i tipi
schema XML o qualsiasi altra definizione di tipo valida e definibile tramite
una notazione XML riconosciuta.
A titolo di esempio, nel Listato 7.3 sono definiti due elementi TradePriceRequest, composto da una stringa, e TradePrice, composto da un valore
in virgola mobile.
In questo caso, dove la struttura dati contiene un solo campo, la definizione
di un tipo specifico non è indispensabile: la sua utilità è per lo più relativa alla capacità di mantenere coerenza all’interno del documento WSDL. In
questo modo infatti viene creato un alias e nel resto del documento WSDL si
utilizzeranno i tipi definiti al posto dei tipi di dati primitivi, disaccoppiando
il resto delle definizioni dai tipi di dati reali.
Messaggi
I messaggi sono la base della costruzione di un Web Service con WSDL.
Sono gli elementi che costituiscono gli input e gli output dei servizi, e sono
definiti in uno o più elementi message che seguono la sezione types. Ciascun
messaggio ha un nome univoco ed è costituito da una o più parti, ciascuna
7.1 Le tecnologie alla base dei Web Services
238
<message name="getRateRequest">
<part name="contry1" type="xsd:string"/>
<part name="contry2" type="xsd:string"/>
</message>
<message name="getRateResponse">
<part name="Result" type="xsd:float"/>
</message>
Listato 7.4: Un messaggio di esempio
con un nome e un tipo distinti. I singoli messaggi possono contenere i tipi
di dati complessi definiti nella sezione Type oppure semplici dati primitivi.
La definizione dei messaggi segue immediatamente quella dei tipi, ciascun
elemento message contiene una o più part, ognuna delle quali specifica una
parte del messaggio con un determinato nome e tipo.
Nel Listato 7.4 è mostrato un file WSDL del servizio mostrato nell’esempio precedente. Come si può notare osservando il listato, vengono definiti
due diversi messaggi, uno relativo alla richiesta (getRateRequest) e uno relativo alla risposta (getRateResponse). Il primo definisce due elementi di
tipo Stringa, chiamati Country1 e Country2, il secondo definisce un unico
elemento di risposta Result di tipo float.
Operazioni
In tale sezione definiamo le operazioni fornite dal Web Service. Le operazioni
equivalgono alle funzionalità che saranno esposte dall’interfaccia del servizio,
sono definite in WSDL all’interno degli elementi operation; a loro volta gli
elementi operation contengono elementi input, output e fault specificanti i
messaggi scambiati durante l’operazione. Se si ponessero in relazione i Web
Service con la programmazione distribuita (ad esempio RMI) le operazioni
WSDL equivarrebbero ai singoli metodi di input e di output. Un’operazione
di riferimento è presente nel Listato 7.5.
L’elemento <operation>, come già detto, definisce una singola operazione e può contenere opzionalmente almeno uno dei due sottoelementi
<output> ed <input>. La presenza e l’ordine di questi elementi determina la tipologia del servizio, che può rientrare all’interno di quattro diverse
categorie:
One-way. Anche detto fire&forget, è una configurazione dove l’endpoint
si limita a ricevere il messaggio inviato dal client. In tal caso è presente
7.1 Le tecnologie alla base dei Web Services
239
<portType name="CurrencyExchangePortType">
<operation name="getRate">
<input message = "tns:getRateRequest" />
<output message = "tns:getRateResponse" />
</operation>
</portType>
Listato 7.5: Un’operazione di esempio
un solo elemento di input, quindi il client spedisce un messaggio al
servizio.
Request-response. In tal caso l’endpoint riceve un messaggio di richiesta, esegue l’elaborazione necessaria e restituisce al client un messaggio
di risposta correlato alla richiesta ricevuta. Sono presenti gli elementi
input e output, quindi il client spedisce un messaggio al servizio e riceve
una risposta.
Solicit-response. E’ l’opposto del caso precedente. In questo caso infatti
è l’endpoint che inizia la comunicazione inviando un messaggio al client
che a sua volta dovrà rispondere, quindi il servizio invia un messaggio a
un client e questo risponde. Nel file WSDL è presente prima l’elemento
output e poi quello input.
Notification. E’ l’opposto della tipologia one-way, l’endpoint invia un
messaggio al client senza che questo debba inviare una risposta. E’
presente solo l’elemento output.
Il modello di interazione da utilizzare dipende dalla natura del servizio.
L’interazione request-response costituisce il tipico modello di comunicazione
RPC quindi potrebbe essere quella usata per la maggior parte dei servizi,
mentre le altre modalità potrebbero risultare più indicate in altri ambiti. Ad
esempio una interazione one-way può essere utile in sistemi dove il client non
è interessato al risultato dell’elaborazione dei dati inviati al servizio.
La definizione di una operazione corrisponde alla dichiarazione di un metodo Java appartenente all’interfaccia di servizio. Il messaggio di input della
operazione consta di argomenti e tipi corrispondenti, mentre il messaggio di
output contiene solo il tipo. Qualora il messaggio di output fosse costituito
da più parti, l’operazione definita non potrebbe avere un suo corrispondente
Java, in quanto non potrebbe più essere interpretata come una RPC.
7.1 Le tecnologie alla base dei Web Services
240
<wsdl:binding name="nmtoken" type="qname">*
<wsdl:operation name="nmtoken">*
<wsdl:input>?
</wsdl:input>
<wsdl:output>?
</wsdl:output>
<wsdl:fault name="nmtoken"> *
</wsdl:fault>
</wsdl:operation>
</wsdl:binding>
Listato 7.6: Sezione WSDL relativa ai collegamenti
Collegamenti
In questa sezione avviene la mappatura del servizio astratto, definito nelle
sezioni precedenti, con il protocollo concreto di comunicazione (ad esempio
SOAP). Il contenuto di questa sezione è fortemente dipendente dal protocollo
utilizzato e quindi dalle estensioni WSDL relative. Un binding è l’istanziazione di una porta astratta, ogni elemento bindings contiene un attributo
type che si riferisce al particolare portType istanziato. La definizione del portType viene ripetuta aggiungendo nuovi elementi che specificano come codificare le operazioni e i messaggi. WSDL definisce gli elementi per esprimere
vari tipi di binding: SOAP, HTTP, MIME.
In WSDL, la struttura di un collegamento (semplificata) è mostrata nel
Listato 7.6
Come si può notare, un collegamento può contenere diverse operazioni,
ciascuna delle quali può avere un elemento di input, di output e una serie di
elementi di errore (fault). Nel listato 7.7, in particolare, è presente un esempio
di collegamento con SOAP. La struttura del collegamento è la seguente: sono
definiti lo stile di codifica e il tipo di trasporto. Lo stile di codifica viene
specificato dall’attributo style di soap:bindings e/o soap/operation, il
quale può essere RPC oppure document. Se è di tipo RPC, allora il messaggio
WSDL viene codificato come una RPC. All’interno del body del messaggio
viene inserito un elemento con lo stesso nome dell’operazione e tanti figli
quante sono le part del messaggio associato; se invece è di tipo document, il
7.1 Le tecnologie alla base dei Web Services
241
messaggio WSDL viene codificato usandone il contenuto letteralmente. In tal
caso, ogni part del messaggio associato a un’operazione figurerà come figlio
distinto dell’elemento Body nel messaggio SOAP.
<soap:binding style="rpc"
transport="http://schemas.xmlsoap.org/soap/http"/>
In questo caso la comunicazione è di tipo RPC e il trasporto avviene sul
protocollo HTTP. In secondo luogo, è necessario definire come trattare i messaggi di input ed output per ciascuna operazione. Il contenuto degli elementi
input e/o output che compongono ciascuna operation non deve essere ripetuto all’interno del binding; gli input e gli output conterranno invece dei nuovi
elementi che specificano più in dettaglio come verrà costruito il corrispondente messaggio SOAP, in particolare quali informazioni dovranno essere
mappate nell’Header e quali nel Body del messaggio. Per definire il contenuto
del corpo del messaggio SOAP si usa l’elemento soap:body specificando:
1. Le parti del messaggio che saranno incluse nel corpo (attributo parts);
2. La codifica dei tipi interessati (attributi use e encodingStyle): (Use=
encoded richiede la codifica di ogni parte in base al suo tipo secondo le
regole di encodingStyle, Use=literal indica che le parti vanno copiate
letteralmente nel messaggio).
3. Il namespace di provenienza degli elementi usati nel messaggio (attributo namespace)
Il messaggio di input è cosı̀ definito:
<soap:body use ="encoded"
namespace="urn:xmethods-CurrencyExchange"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
In questo elemento viene indicato il namespace del messaggio (urn:xmethods
- CurrencyExchange) e il tipo di encoding. L’attributo use assume valore
encoding, quindi l’attributo encodingStyle dovrà indicare il tipo di encoding
utilizzato. In questo caso viene utilizzato l’encoding SOAP. Gli elementi del
namespace SOAP contenuti nell’elemento binding servono a descrivere come
costruire un messaggio SOAP a partire dal messaggio astratto definito in
WSDL. In questo caso viene utilizzato l’encoding SOAP.
è mostrato un esempio di collegamento, relativo sempre al cambio valuta.
7.1 Le tecnologie alla base dei Web Services
<binding name="CurrencyExchangeBinding"
type="tns:CurrencyExchangePortType">
<soap:binding style="rpc"transport=
"http://schemas.xmlsoap.org/soap/http"/>
<operation name="getRate">
<soap:operation soapAction=""/>
<input>
<soap:body use ="encoded" namespace=
"urn:xmethods-CurrencyExchange"
encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/"/>
</input>
<output>
<soap:body use ="encoded" namespace=
"urn:xmethods-CurrencyExchange"
encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/"/>
</output>
</operation>
</binding>
Listato 7.7: Esempio di un collegamento
242
7.1 Le tecnologie alla base dei Web Services
243
<service name="CurrencyExchangeService">
<documentation>
Ritorna il tasso di scambio tra due valute
</documentation>
<port name="CurrencyExchangePort"
binding="tns:currencyExchangeBinding">
<soap:address location=
"http://service.xmethods.net:80/soap"/>
</port>
</service>
Listato 7.8: Definizione del servizio
Definizione del servizio
L’ultimo elemento di un file WSDL è la definizione del servizio: questa sezione
consente di raccogliere tutte le operazioni sotto un unico nome. Il servizio
è identificato da un nome (l’attributo name dell’elemento service) e può
avere una descrizione contenuta nel sottoelemento opzionale documentation.
L’elemento service è l’elemento di livello più alto in WSDL. Esso dichiara un
servizio web con un particolare nome come raccolta di porte. Al suo interno
vengono elencate tutte le operazioni esposte dal servizio sotto forma di elementi port. Per ciascuno di questi viene indicato il collegamento utilizzato.
Il contenuto dell’elemento port cambia in funzione del tipo di collegamento
utilizzato.
Nel Listato 7.8 è presente una definizione di servizio, in particolare del
servizio di cambio valute, dove il collegamento utilizzato è quello con SOAP.
In questo caso nell’elemento port è presente un elemento address appartenente al namespace soap (utilizzato in tutto il WSDL per riferirsi agli elementi strettamente relativi al collegamento di WSDL con SOAP). Questo
elemento indica l’URL fisico dell’endpoint del servizio. L’utilizzo di WSDL
può diventare anche molto complesso nel momento in cui ci si addentra in
collegamenti e protocolli diversi da SOAP e HTTP. Per ciascun protocollo e
livello di trasporto è infatti necessario definire una estensione di WSDL specifica e costruire i documenti WSDL secondo questa grammatica. Il vantaggio
di grande estendibilità di WSDL si paga quindi in termini di complessità.
7.1 Le tecnologie alla base dei Web Services
7.1.3
244
UDDI: Universal Description Discovery and Integration
L’UDDI (acronimo di Universal Description Discovery and Integration) è un
registry (ovvero una base dati ordinata ed indicizzata) basato su XML ed
indipendente dalla piattaforma hardware, che permette alle aziende la pubblicazione dei propri dati e dei servizi offerti su internet. UDDI, un’iniziativa
open sviluppata tra il 1999 ed il 2000 e sponsorizzata dall’Organization for the
Advancement of Structured Information Standards (OASIS: consorzio internazionale per lo sviluppo e l’adozione di standard nel campo dell’e-business
e dei Web Services), permette quindi la scoperta e l’interrogazione dei servizi
offerti sul web, delle aziende che li offrono e della maniera per usufruirne.
Il registro UDDI è supportato da una rete mondiale di nodi collegati tra
di loro in una sorta di federazione in modo similare alla tecnologia DNS.
Quando un client sottopone una informazione al registro, questo la propaga
agli altri nodi. In questo modo si attua la ridondanza dei dati fornendo una
certa affidabilità. Il ruolo del singolo nodo rimane comunque fondamentale
poichè nel momento in cui un client sottopone dei dati, questo ne diviene il
proprietario, e sarà in futuro solo questo nodo a poter operare importanti
operazioni sui dati quali la loro eliminazione.
Le chiamate a UDDI sono implementate tramite comunicazioni SOAP
di tipo request-response e permettono sostanzialmente due operazioni: quella di pubblicazione delle informazioni e quelle di ricerca delle stesse. Le
informazioni gestite dal registro UDDI sono di tre tipi:
- Pagine bianche. Contengono informazioni anagrafiche delle aziende,
come l’indirizzo e i numeri di telefono. Costituiscono un primo approccio alle aziende, di tipo umano prima che tecnico.
- Pagine gialle. Definiscono la categorizzazione (tassonomia) dei servizi e
delle aziende. Un’azienda potrebbe essere infatti catalogata in base alla
sua posizione geografica o al proprio settore industriale. Le tassonomie
dovrebbero rispettare standard internazionali, quali quelli dettati dall’ISO.
- Pagine Verdi. Costituiscono le informazioni tecniche dei servizi, quelle
utili a livello informatico. Queste riguardano gli URL dei servizi o gli
eventuali documenti WSDL.
Nei registri UDDI, la descrizione dei Web Service contiene quattro tipi di
informazioni: business information, service information, binding information,
e informazioni sulle specifiche del servizio, descritte da diverse entità.
7.1 Le tecnologie alla base dei Web Services
245
businessEntity fornisce una descrizione delle organizzazioni che forniscono
il Web Service. È una lista di nomi, indirizzi, telefoni e altri tipi di
informazioni relative alla società fisica.
businessService descrive un gruppo di Web Service offerti dalla società
descritta nel businessEntity. Questo gruppo comprende servizi dello stesso tipo ma forniti in diversi indirizzi, diverse versioni o diverse
tecnologie. Ad un businessEntity possono corrispondere diversi businessService, ma ad un businessService è associato uno ed un solo businessEntity.
businessTemplate fornisce una descrizione tecnica necessaria per l’uso di
un particolare Web Service. Essenzialmente esso definisce l’indirizzo
dove reperire il servizio ed una serie di dettagli tecnici che descrivono
l’interfaccia del Web Service o altre proprietà del servizio. Ad un businessService possono corrispondere diversi businessTemplate (uno per
ogni interfaccia o indirizzo), ma ad un businessTemplate è associato
uno ed un solo businessService.
tModel acronimo di “technical model”, esso è il contenitore generico per
ogni tipo di specifiche. Può rappresentare l’interfaccia di un servizio
WSDL, una classificazione, un protocollo di interazione o può descrivere
la semantica di un’operazione. Diversamente dalle entità precedenti, il
tModel non è soggetto a nessuna relazione fissa.
Riassumendo, se vogliamo pubblicare uno o più servizi in un registro
UDDI, prima dobbiamo definire un’insieme di tModel che descrivono le diverse caratteristiche del servizio (come l’interfaccia WSDL o le operazioni
svolte dal servizio). Successivamente bisogna pubblicare le informazioni sulla compagnia (businessEntity), le informazioni generali sul servizio proposto
(businessService) e infine un set di informazioni tecniche per ogni diversa
implementazione e punto di accesso (businessTemplate).
JAXR
L’accesso ai registri di servizi Web dalla piattaforma Java avviene tramite
le Java API for XML Registries (JAXR). In generale si parla di registri
perché JAXR ha un’architettura multiregistro, capace di accedere anche ad
altre tipologie di registry, come ebXML R. Inoltre JAXR può propagare una
singola ricerca di servizi a più registry contemporaneamente. L’accesso ai
registri avviene tramite una connessione ottenuta dalla classe ConnectionFactory che viene opportunamente configurata tramite una serie di proprietà che
7.1 Le tecnologie alla base dei Web Services
246
indicano, tra le altre cose, l’URL del registro al quale si vuole accedere. Una
volta in possesso della connessione, è possibile eseguire ricerche e realizzare
la registrazione di aziende e servizi. Quest’ultima operazione è però sottoposta ad autenticazione e per poterla portare a termine è necessario essere
registrati presso il particolare registro a cui si vuole accedere.
Un aspetto interessante di JAXR è il suo uso del pattern futures (anche
detto lazy-loading): una ricerca su un registro mondiale può potenzialmente
restituire una grande mole di informazioni che non è garantito venga poi analizzata tutta. Per ovviare a questo inconveniente, ad ogni ricerca gli oggetti
creati da JAXR non contengono tutte le informazioni ma solo quelle indispensabili. Nel momento in cui si renda necessario ottenere quelle mancanti,
il runtime di JAXR esegue l’accesso ai dati necessari. Questa caratteristica
ha due aspetti: per prima cosa, vengono recuperati solo i dati effettivamente
necessari, con un guadagno in termini di performance; in secondo luogo, i
client JAXR devono per forza essere sempre connessi alla rete in modo che i
successivi caricamenti dati possano essere eseguiti correttamente.
Le JAXR consentono la gestione delle informazioni presenti nel registro
come la creazione, l’aggiornamento e l’eliminazione di elementi. Inoltre é
supportata l’interrogazione del registro per eseguire le ricerche di servizi ed
aziende.
Ciascun elemento del registro é individuato univocamente da una UUID
conforme al DCE 128 bit. Questa chiave é solitamente creata in modo
trasparente dal registro, anche se per alcuni registri (come ebXML) é possibile fornire dall’esterno la chiave da utilizzare. Il ciclo di vita degli oggetti del
registro inizia con l’operazione di creazione (l’interfaccia LifeCycleManager
fornisce una serie di metodi di utilità per creare i differenti oggetti definiti
nel modello informativo).
Un oggetto creato non é ancora presente all’interno del registro e non
può dunque essere oggetto di aggiornamenti o cancellazioni. Per inserire
l’oggetto nel registro é necessario che questo passi attraverso un’operazione di
salvataggio (save). Una caratteristica interessante di JAXR é la possibilità di
deprecare gli oggetti, con un concetto simile al tag @deprecated di Javadoc.
Gli oggetti deprecati non consentono nuove referenze (come associazioni o
classificazioni), ma continuano a funzionare normalmente. Se da una parte
l’interfaccia LifeCycleManager consente di gestire tutti gli elementi ad alto
e basso livello del registro, dall’altra può essere necessario concentrarsi più
su elementi di business. L’interfaccia BusinessLifeCycleManager contiene
alcune importanti chiamate ad alto livello definendo una API simile alle
Publisher API di UDDI.
Con questa interfaccia non vengono introdotte nuove funzionalità ma vengono aiutati gli sviluppatori UDDI, che dovrebbero trovarsi ad operare con
7.1 Le tecnologie alla base dei Web Services
247
API più familiari. Un limite di JAXR in questa versione é che le operazioni
sul ciclo di vita degli oggetti non sono applicabili a connessioni federate. Non
é ad esempio possibile creare la stessa entità direttamente in due registri
differenti.
A differenza delle operazioni di disposizione (come la creazione o la modifica di informazioni), le operazioni di ricerca avvengono in modo non privilegiato. Non é necessaria dunque l’autenticazione dell’utente. L’interrogazione
può avvenire in modo puntuale o tramite query. Nel primo caso viene utilizzata l’interfaccia BusinessQueryManager che, in modo similare a BusinessLifeCycleManager,
ha una impostazione più ad alto livello e permette l’interrogazione delle interfacce più funzionali del modello informativo. Molti metodi dell’interfaccia
richiedono argomenti simili, alcuni tra i più importanti sono:
- findQualifiers. Definiscono le regole di confronto di stringhe, ordinamento e operatori logici sui parametri di ricerca.
- namePatterns. E’ una Collection di stringhe che contengono nomi anche parziali con eventuali wildcard come specificato nelle SQL-92 per
la parola chiave LIKE.
- classifications. E’ una Collection di Classification che identifica in quali
classificazioni eseguire la ricerca.
Nel secondo caso é possibile utilizzare una query dichiarativa, tramite l’interfaccia DeclarativeQueryManager. Ad oggi l’unico standard supportato é
una derivazione di SQL-92, in particolare dell’istruzione SELECT, estesa con
le specifiche relative alle stored procedures. Le query dichiarative sono supportate anche su connessioni federate ma sono una caratteristica opzionale
dei provider JAXR 1.0.
Capitolo 8
Applicazione completa sui Web
Service
Questa semplice applicazione invia e riceve messaggi.
8.1
Il Web Service
Per la realizzazione di un Web Service con JBoss, inizialmente si creano le
classi Java: un’interfaccia che espone i metodi del web service (Listato 8.1)
e la classe che implementa il Web Service (Listati 8.2 e 8.3).
Successivamente, bisogna creare una serie di file XML che servono al
Container per gestire il Web Service, per creare l’interfaccia WSDL e il file
di mapping JAXR. Il file config.xml (Listato 8.4) definisce l’interfaccia del
Web Service e serve per generare automaticamente il file WSDL e il file
mapping.xml.
package Archivio;
import java.rmi.*;
public interface Messaggio extends Remote {
void sendMex(String mex) throws RemoteException;
String receiveMex() throws RemoteException;
}
Listato 8.1: Messaggio.java
248
8.1 Il Web Service
249
package Archivio;
import
import
import
import
java.rmi.*;
java.util.*;
java.rmi.server.*;
java.io.*;
public class MessaggioImpl extends UnicastRemoteObject {
public MessaggioImpl() throws RemoteException
{
System.out.println("MessaggioImpl()");
}
public void sendMex(String messaggio) throws
RemoteException
{
Vector coda;
System.out.println("send()");
try
{
coda = (Vector)new ObjectInputStream
(new FileInputStream ("appoggio")).readObject();
}
catch(IOException err)
{
coda=new Vector();
}
catch(ClassNotFoundException clfe)
{
return;
}
coda.add(messaggio);
try
{
new ObjectOutputStream
(new FileOutputStream ("appoggio")).
writeObject(coda);
}
Listato 8.2: MessaggioImpl.java
8.1 Il Web Service
250
catch(IOException err)
{
err.printStackTrace();
}
}
public String receiveMex() throws RemoteException
{
Vector coda;
System.out.println("send()");
try
{
coda = (Vector) new ObjectInputStream
(new FileInputStream ("appoggio")).
readObject();
}
catch(IOException err)
{
return(null);
}
catch(ClassNotFoundException clfe)
{
return(null);
}
try
{
String messaggio = (String) coda.get(0);
coda.remove(0);
new ObjectOutputStream
(new FileOutputStream ("appoggio")).
writeObject(coda);
return(messaggio);
}
catch(NoSuchElementException nsee)
{
return(null);
}
catch(IOException err)
{
err.printStackTrace();
return(null);
}
}
}
Listato 8.3: MessaggioImpl.java
8.1 Il Web Service
251
<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns=
"http://java.sun.com/xml/ns/jax-rpc/ri/config"> <service
name="ArchivioService"
targetNamespace="http://archivio.ws.jboss.org/"
typeNamespace="http://archivio.ws.jboss.org/types"
packageName="Archivio">
<interface name ="Archivio.Messaggio"/>
</service> </configuration>
Listato 8.4: config.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app>
<servlet>
<servlet-name>ArchivioWS</servlet-name>
<servlet-class>Archivio.ArchivioImpl</servlet-class>
</servlet>
<servlet-mapping> <servlet-name>ArchivioWS</servlet-name>
<url-pattern>/ArchivioUrl</url-pattern> </servlet-mapping>
</web-app>
Listato 8.5: web.xml
La generazione di questi file avviene attraverso il tool wscompile che fa
parte del Java Web Service Developer Pack (WSDP).
Di seguito è mostrata la stringa di compilazione per questo Web Service:
wscompile -classpath <classpath> -gen:server -f:rpcliteral
-mapping config.xml
Il classpath si riferisce alla directory dove trovare le classi java.
A questo punto bisogna creare un package (nel nostro caso Archivio),
all’interno del quale si pone una sottocartella con lo stesso nome del package
in cui sono presenti i file .java.
Il passo successivo consiste nella creazione del file web.xml (Listato 8.5)
che non ha al suo interno elementi di configurazione ma serve unicamente
per comunicare al Container come installare il Web Service.
L’ultimo file xml da creare è webservices.xml (Listato 8.6). Quest’ultimo
deployment descriptor è necessario a JBoss per comunicare che stiamo realizzando un Web Service e non una normale Servlet.
Questo file comunica al Container dove trovare il file wsdl tramite l’elemento <wsdl-file>, mentre l’elemento <jaxrpc-mapping-file> indica
8.1 Il Web Service
252
<webservices> <webservice-description>
<webservice-description-name> ArchivioService
</webservice-description-name>
<wsdl-file>WEB-INF/wsdl/ArchivioService.wsdl
</wsdl-file>
<jaxrpc-mapping-file> WEB-INF/mapping.xml
</jaxrpc-mapping-file>
<port-component>
<port-component-name>Messaggio</port-component-name>
<wsdl-port>MessaggioPort</wsdl-port>
<service-endpoint-interface>
Archivio.Messaggio </service-endpoint-interface>
<service-impl-bean>
<servlet-link>ArchivioWS</servlet-link>
</service-impl-bean> </port-component>
</webservice-description>
</webservices>
Listato 8.6: webservices.xml
Figura 8.1: Schema WebService
dove trovare il file mapping generato dal tool wscompile.
Una volta creato anche quest’ultimo file, il Web Service è pronto per
essere installato all’interno del container. Lo schema di come preparare i file
è mostrato in Figura 8.1
Il deploy va eseguito come negli EJB, ponendo la cartella esempioWS.war
in deploy all’interno di JBoss. Dopo pochi secondi nella finestra di avvio di
8.1 Il Web Service
JBoss si può verificare l’avvenuta installazione.
253
8.2 Il Client
254
import Archivio.*; import javax.xml.rpc.Service; import
javax.xml.rpc.ServiceFactory; import javax.xml.namespace.QName;
import java.net.URL;
public class send {
public static void main(String[] args) throws Exception
{
String urlstr = "http://localhost:8080/esempioWS/
ArchivioUrl?wsdl";
System.out.println
("ContactingÃwebserviceÃatÃ" + urlstr);
URL url = new URL(urlstr);
QName qname = new QName
("http://archivio.ws.jboss.org/","ArchivioService");
ServiceFactory factory = ServiceFactory.newInstance();
Service service = factory.createService(url, qname);
Messaggio arch =
(Messaggio) service.getPort(Messaggio.class);
arch.sendMex(args[0]);
}
}
Listato 8.7: send.java
8.2
Il Client
Di seguito viene illustrata la struttura del client, che compie due operazioni:
invia e riceve messaggi. Sarà perciò composto da due classi di cui illustriamo
il codice (Listati 8.7 e 8.8) e che servono a inviare un messaggio passato da
linea di comando al Web service e a verificare l’avvenuta ricezione.
Quando viene eseguito, il client come prima cosa cerca di connettersi al
WSDL del Web Service all’indirizzo http://localhost:8080/esempioWS/
ArchivioUrl?wsdl. L’indirizzo di questo esempio si riferisce ad un Web Service presente nello stesso computer del client (localhost), nel caso sia diverso
è necessario cambiare localhost nel nome o nell’indirizzo IP del server su cui
il Web Service è dispiegato. La seconda parte dell’indirizzo esempioWS è il
nome della cartella nella quale il Web Service è stato installato all’interno di
JBoss. In questo esempio era esempioWS.war. L’ultima parte ArchivioUrl è
il nome del pattern che si trova all’interno del file web.xml all’interno dei tag
<url-pattern>.
8.2 Il Client
255
import Archivio.*; import javax.xml.rpc.Service; import
javax.xml.rpc.ServiceFactory; import javax.xml.namespace.QName;
import java.net.URL;
public class receive {
public static void main(String[] args) throws Exception
{
String urlstr =
"http://localhost:8080/esempioWS/ArchivioUrl?wsdl";
System.out.println
("ContactingÃwebserviceÃatÃ" + urlstr);
URL url = new URL(urlstr);
QName qname = new QName
("http://archivio.ws.jboss.org/","ArchivioService");
ServiceFactory factory = ServiceFactory.newInstance();
Service service = factory.createService(url, qname);
Messaggio arch =
(Messaggio) service.getPort(Messaggio.class);
String mex=arch.receiveMex();
System.out.println(mex);
}
}
Listato 8.8: receive.java
8.2 Il Client
256
Figura 8.2: deploy del web service su JBoss
Quest’indirizzo è visibile nella finestra di JBoss mostrata in Figura 8.2
dove vengono mostrate le quattro righe con cui JBoss comunica l’avvenuta
installazione.
Il passo successivo che il client esegue è la richiesta dell’interfaccia del web
service, la classe Messaggio. Questo avviene creando un oggetto QNAME
con i parametri del targetNamespace inserito nel file config.xml e il nome
del servizio. Una volta ottenuta la classe di interfaccia del web service, si
utilizzano i metodi esposti (sendMex() e receiveMex()) come se fossero in
locale.
Appendice
Compilazione e Deploy1 di componenti J2EE
in JBOSS
.
A.1
Servlet e JSP
Vediamo quali sono i passi preliminari da compiere per poter provare e far
girare una servlet. Per prima cosa, va installato JBoss.
Per maggiori dettagli sull’installazione e il deploy si faccia riferimento [11]
e [9].
Quando si vuol far girare una servlet con JBoss, bisogna predisporre con
un certo criterio le classi java e gli altri file necessari (xml e jsp) che sono
stati creati. Innanzi tutto, si crea una cartella nomecartella.war. All’interno
di questa directory va creata una sottodirectory, che deve essere nominata
WEB-INF. Al suo interno, la directory WEB-INF conterrà una sottodirectory classes con i file .java e .class della servlet; essa conterrà inoltre il file
web.xml. Quando si crea un Progetto con JCreator e si vuole compilare,
bisogna prima aggiungere il package javax.servlet.jar, nel modo seguente: dal
menu Project si sceglie Project Properties e poi Required Libraries
(vedi Figura 8.3), dopo di che si clicca il bottone Add e si seleziona Add
Archive; a questo punto, verrà visualizzata una nuova finestra dalla quale
sarà possibile accedere al package richiesto, dal seguente percorso:
{Dir. installaz. di JBoss}\server\default\lib\javax.servlet.jar
(vedi Figura 8.4). Una volta selezionato e aggiunto il package, gli si dà un
nome simbolico, come mostrato in Figura 8.5. Dopo aver premuto il tasto
1
Con il termine Deploy si intende la messa in esercizio di un componente distribuito
all’interno di un Application Server
257
Appendice
258
Figura 8.3: Finestra Set Library
Figura 8.4: Finestra Apri
OK, dalla finestra Set Library, sarà possibile selezionare il package rappresentato dal nome simbolico scelto (nel nostro caso Servlet), come mostrato in
Figura 8.6. Infine, è possibile compilare la servlet, semplicemente attraverso
il comando del menu Build\Compile Project.
A.2
Uso di JMS con JBOSS
I passi per la compilazione delle classi che fanno uso del package J2EE
javax.jms sono analoghi a quelli per compilare le servlet. In questo caso,
però, occorre importare l’archivio JAR:
Appendice
259
Figura 8.5: Finestra Set Library Name
Figura 8.6: Finestra Project Properties Finale
Appendice
260
{Dir. installaz. di JBoss}\client\jbossall-client.jar
Questa stessa libreria è anche richiesta durante l’esecuzione. Si osservi
che, ovviamente, i produttori e i consumatori sia nel paradigmi Point-toPoint che Publish/Subscribe non devono essere in alcun modo dispiegati
nell’Application Server.
Configurazione di JNDI in JBOSS
La creazione dello InitialContext in JBOSS e il successivo lookup delle Connection Factory e delle Destination richiede l’esistenza di un file il cui nome
è jndi.properties che si deve trovare nel CLASSPATH. Tale file deve
contenere le seguenti tre righe:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.provider.url=jnp://localhost:1099
java.naming.factory.url.pkgs=org.jboss.naming:org.jnp.interfaces
In particolare la seconda riga contiene l’indirizzo IP o il nome simbolico
del calcolatore su cui è installato JBOSS. É importante sottolineare che,
sebbene la riga sia obbligatoria, di fatto può contenere quasi indirizzo IP o
nome simbolico, anche se su computer indirizzato JBOSS non è installato
(addirittura può non esistere il computer indirizzato!). Infatti, al momento
di utilizzo, il client del protocollo JNDI prova ad accedere al computer con
l’indirizzo specificato. Se tale computer non ha installato JBOSS oppure
l’indirizzo non è valido, allora inizia una procedura di discovery per risolvere
il contesto JNDI. Quindi, in pratica, a prescindere dal valore specificato nel
contesto, la procedura di lookup va a buon fine se nella rete locale esiste un
computer con JBOSS e su tale PC è effettivamente presente una Connection
Factory o una destination con tale nome.
Deploy delle destination
L’unico oggetto che è necessario dispiegare all’interno di JBOSS per JMS
sono ovviamente le Destination (Queue e Topic).
Per dispiegare una specifica Destination occorre creare un file XML il cui
nome deve essere del tipo XXX-service.xml che deve essere copiato in
{Dir. installaz. di JBoss}
\server\default\deploy\jbossall-client.jar
Tale file deve contenere un unico elemento XML mbean come da esempio:
<mbean code="org.jboss.mq.server.jmx.Topic"
name="jboss.mq.destination:service=Topic,name=esempio">
Bibliografia
261
<depends optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager</depends>
</depends>
</mbean>
Tale esempio crea un Topic di nome esempio. Per creare una Queue è
sufficiente sostituire nell’esempio tutte le volte in cui compare Topic con
Queue.
Bibliografia
[1] J. Farley, W. Crowford, D. Flanagan, Java Enterprise in a Nutshell: A
Desktop Qick Reference.
[2] G. Alonzo, F. Casati, H. Kuno, V. Machiraju, Web Services: Concepts,
Architectures and Applications, Springer-Verlag
[3] Servlet Tutorial in Servlet API 2.0,
http://javasoft-mirror.java.tin.it/products/servlet/index.html
[4] The Java Tutorial, How to Use Root Panes,
http://java.sun.com/docs/books/tutorial/uiswing/
components/rootpane.html
[5] E. Gamma, R. Helm, R. Johnson, J. Vlissides, Design Patterns, Addison
Wesley.
[6] Java 2 Platform Standard Edition 5.0 API Specification
http://java.sun.com/j2se/1.5.0/docs/api/
[7] J. Hunter, W. Crawford, Java Servlet Programming, O’Reilly.
[8] H. Bergstein, Java Server Pages, O’Reilly.
[9] S. Davis, T. Marrs, JBoss at Work: A Practical Guide, O’Reilly
[10] J. Ball, D. Bode Carson, I. Evans, K. Haase, E. Jendrock, The Java EE
5 Tutorial For Sun Java System Application Server Platform Edition 9,
http://java.sun.com/javaee/reference/
[11] The JBoss 4 Application Server Guide JBoss AS 4.0.3 Release 4, JBoss
Inc.
[12] J. Conallen, Communication Of The ACM October 1999, Vol. 42, No.
10
262
Scarica