Tutorial Mokabyte Java/CORBA - Dipartimento di Ingegneria

Transcript

Tutorial Mokabyte Java/CORBA - Dipartimento di Ingegneria
© 2001 – proprietà di MokaByte® s.r.l.
tutti i diritti riservati
è vietata la riproduzione non autorizzata anche parziale
Java e CORBA
DI
GIANLUCA MORELLO
Introduzione
Nell’era di Internet e delle grandi Intranet aziendali, il modello computazionale dominante è
chiaramente quello distribuito. Un tipico ambiente distribuito vede la presenza di mainframe,
server Unix e macchine Windows e pone quindi seri problemi di interoperabilità tra piattaforme
differenti, sistemi operativi differenti e linguaggi differenti.
Lo scopo dichiarato delle specifiche CORBA è proprio quello di definire un’infrastruttura standard per la comunicazione tra e con oggetti remoti ovunque distribuiti, indipendentemente dal
linguaggio usato per implementarli e dalla piattaforma di esecuzione. È bene quindi notare che, a
differenza di altre tecnologie distribuite quali RMI, Servlet o EJB, non si sta parlando di una tecnologia legata a una specifica piattaforma, ma di uno standard indipendente dal linguaggio adottato che consente a oggetti Java di comunicare con “oggetti” sviluppati in COBOL, C++ o altri
linguaggi ancora.
L’acronimo CORBA sta per Common Object Request Broker Architecture e non rappresenta uno specifico prodotto, bensì un’insieme di specifiche volte a definire un’architettura completa e standardizzata di cui esistono varie implementazioni. Le specifiche sono prodotte da OMG, un consorzio che comprende più di 800 aziende e include i più illustri marchi dell’industria informatica.
L’elemento fondamentale dell’intera architettura è il canale di comunicazione degli oggetti
nell’ambiente distribuito, l’Object Request Broker (ORB).
Le specifiche CORBA 1.1 furono pubblicate da OMG nell’autunno del 1991 e definivano
un’API e un linguaggio descrittivo per la definizione delle interfacce degli oggetti CORBA detto
Interface Definition Language (IDL). Soltanto nel dicembre del 1994, con CORBA 2.0, vennero definiti i dettagli sulla comunicazione tra differenti implementazioni di ORB con l’introduzione dei
protocolli GIOP e IIOP.
Sebbene CORBA possa essere utilizzato con la maggior parte dei linguaggi di programmazione,
Java risulta il linguaggio privilegiato per implementare le sue specifiche in un ambiente eterogeneo in quanto permette agli oggetti CORBA di essere eseguiti indifferentemente su mainframe,
network computer o telefono cellulare.
1
Nello sviluppo di applicazioni distribuite, Java e CORBA si completano a vicenda: CORBA affronta e risolve il problema della trasparenza della rete, Java quello della trasparenza
dell’implementazione rispetto alla piattaforma di esecuzione.
Object Management Group
L’OMG è un consorzio no-profit interamente dedicato alla produzione di specifiche e di standard; vede la partecipazione sia dei grandi colossi dell’informatica, sia di compagnie medie e piccole.
L’attività di OMG cominciò nel 1989 con soli otto membri tra cui Sun Microsystems, Philips,
Hewlett-Packard e 3Com. Le specifiche più conosciute prodotte dal consorzio sono sicuramente
UML e CORBA che comunque nella logica OMG, rappresentano strumenti strettamente cooperanti nella realizzazione di applicazioni Enterprise OO.
Sin da principio il suo scopo è stato quello di produrre e mantenere una suite di specifiche di
supporto per lo sviluppo di software distribuito in ambienti distribuiti, coprendo l’intero ciclo di
vita di un progetto: analisi, design, sviluppo, runtime e manutenzione.
Per ridurre complessità e costi di realizzazione, il consorzio ha introdotto un framework per la
realizzazione di applicazioni distribuite. Questo framework prende il nome di Object Management
Architecture (OMA) ed è il centro di tutte le attività del consorzio; all’interno di OMA convivono
tutte le tecnologie OMG.
Nell’ottica OMG la definizione di un framework prescinde dall’implementazione e si “limita”
alla definizione dettagliata delle interfacce di tutti i componenti individuati in OMA.
I componenti di OMA sono riportati di seguito.
Object Request Broker (ORB)
È l’elemento fondamentale dell’architettura. È il canale che fornisce l’infrastruttura che permette
agli oggetti di comunicare indipendentemente dal linguaggio e dalla piattaforma adottata. La
comunicazione tra tutti i componenti OMA è sempre mediata e gestita dall’ORB.
Object Services
Standardizzano la gestione e la manutenzione del ciclo di vita degli oggetti. Forniscono le interfacce base per la creazione e l’accesso agli oggetti. Sono indipendenti dal singolo dominio applicativo e possono essere usati da più applicazioni distribuite.
Common Facilities
Comunemente conosciute come CORBAFacilities. Forniscono due tipologie di servizi, orizzontali e verticali. Quelli orizzontali sono funzionalità applicative comuni: gestione stampe, gestione
documenti, database e posta elettronica. Quelli verticali sono invece destinati a una precisa tipologia di applicazioni.
Domain Interfaces
Possono combinare common facilities e object services. Forniscono funzionalità altamente specializzate per ristretti domini applicativi.
2
Application Objects
È l’insieme di tutti gli altri oggetti sviluppati per una specifica applicazione. Non è un’area di
standardizzazione OMG.
Di questi componenti OMG fornisce una definizione formale sia delle interfacce (mediante
IDL), sia della semantica. La definizione mediante interfacce lascia ampio spazio al mercato di
componenti software di agire sotto le specifiche con differenti implementazioni e consente la
massima interoperabilità tra componenti diversi di case differenti.
I servizi CORBA
I CORBAServices sono una collezione di servizi system-level descritti dettagliatamente con
un’interfaccia IDL; sono destinati a completare ed estendere le funzionalità fornite dall’ORB.
Forniscono un supporto che va a coprire lo spettro completo delle esigenze di una qualunque
applicazione distribuita.
Alcuni dei servizi standardizzati da OMG (ad esempio il Naming Service) sono diventati fondamentali nella programmazione CORBA e sono presenti in tutte le implementazioni. Altri servizi
appaiono invece meno interessanti nella pratica comune, assumono un significato solo dinanzi a
esigenze particolari e non sono presenti nella maggior parte delle implementazioni sul mercato.
OMG ha pubblicato le specifiche di ben 15 servizi, qui riportati.
Collection Service
Fornisce meccanismi di creazione e utilizzo per le più comuni tipologie di collection.
Concurrency Control Service
Definisce un lock manager che fornisce meccanismi di gestione dei problemi di concorrenza
nell’accesso a oggetti agganciati all’ORB.
Event Service
Fornisce un event channel che consente ai componenti interessati a uno specifico evento di ricevere una notifica, pur non conoscendo nulla del componente generatore.
Externalization Service
Definisce meccanismi di streaming per il trattamento di dati da e verso i componenti.
Licensing Service
Fornisce meccanismi di controllo e verifica di utilizzo di un componente. È pensato per
l’implementazione di politiche “pago per quel che uso”.
Lyfe Cycle Service
Definisce le operazioni necessarie a gestire il ciclo di vita di un componente sull’ORB (creare,
copiare e rimuovere).
3
Naming Service
Consente a un componente di localizzare risorse (componenti o altro) mediante nome. Permette
di interrogare sistemi di directory e naming già esistenti (NIS, NDS, X.500, DCE, LDAP). È il
servizio più utilizzato.
Persistence Service
Fornisce mediante un’unica interfaccia le funzionalità necessarie alla memorizzazione di un componente su più tipologie di server (ODBMS, RDBMS e file system).
Properties Service
Permette la definizione di proprietà legate allo stato di un componente.
Query Service
Fornisce meccanismi di interrogazione basati su un’estensione di SQL chiamata Object Query
Language.
Relationship Service
Permette di definire e verificare in modo dinamico varie tipologie di associazioni e relazioni tra
componenti.
Security Service
Framework per la definizione e la gestione della sicurezza in ambiente distribuito. Copre ogni
possibile aspetto: autenticazione, definizione di credenziali, gestione per delega, definizione di
access control list e non-repudiation.
Time Service
Definisce un’interfaccia di sincronizzazione tra componenti in ambiente distribuito.
Trader Service
Fornisce un meccanismo modello Yellow Pages per i componenti.
Transaction Service
Fornisce un meccanismo di two-phase commit sugli oggetti agganciati all’ORB che supportano
il rollback; definisce transazioni flat o innestate.
Le basi CORBA
CORBA e OMA in genere si fondano su alcuni principi di design:
— Separazione tra interfaccia e implementazione: un client è legato all’interfaccia di un oggetto CORBA, non alla sua implementazione.
— Location transparency e access transparency: l’utilizzo di un qualunque oggetto CORBA
non presuppone alcuna conoscenza sulla sua effettiva localizzazione.
— Typed interfaces: ogni riferimento a un oggetto CORBA ha un tipo definito dalla sua interfaccia.
4
Parlando di CORBA è particolarmente significativo il concetto di trasparenza, inteso sia come
location transparency, sia come trasparenza del linguaggio di programmazione adottato. In pratica è trasparente al client la collocazione dell’implementazione di un oggetto, locale o remota.
La location transparency è garantita dalla mediazione dell’ORB. Un riferimento a un oggetto
remoto va inteso come un identificativo unico di una sua implementazione sulla rete.
Architettura CORBA
L’architettura CORBA ruota intorno al concetto di Objects Request Broker. L’ORB è il servizio
che gestisce la comunicazione in uno scenario distribuito agendo da intermediario tra gli oggetti
remoti: individua l’oggetto sulla rete, comunica la richiesta all’oggetto, attende il risultato e lo
comunica indietro al client.
Figura 1 — Architettura CORBA.
L’ORB opera in modo tale da nascondere al client tutti i dettagli sulla localizzazione degli oggetti
sulla rete e sul loro linguaggio d’implementazione; è quindi l’ORB a individuare l’oggetto sulla
rete e a effettuare le opportune traslazioni nel linguaggio d’implementazione. Queste traslazioni
sono possibili solo per quei linguaggi per i quali è stato definito un mapping con IDL (questa
definizione è stata operata per i linguaggi più comuni).
In fig. 1 si può osservare l’architettura CORBA nel suo complesso:
Object è l’entità composta da identity, interface e implementation (servant nel gergo CORBA).
Servant è l’implementazione dell’oggetto remoto. Implementa i metodi specificati
dall’interfaccia in un linguaggio di programmazione.
Client è l’entità che invoca i metodi (operation nel gergo CORBA) del servant. L’infrastruttura
dell’ORB opera in modo tale da rendergli trasparenti i dettagli della comunicazione remota.
ORB è l’entità logica che fornisce i meccanismi per inviare le richieste da un client all’oggetto
remoto. Grazie al suo operato, che nasconde completamente i dettagli di comunicazione, le
chiamate del client sono assimilabili a semplici invocazioni locali.
5
ORB Interface essendo un’entità logica, l’ORB può essere implementato in molti modi. Le
specifiche CORBA definiscono l’ORB mediante un’interfaccia astratta, nascondendo completamente alle applicazioni i dettagli d’implementazione.
IDL stub e IDL skeleton: lo stub opera da collante tra client e ORB; lo skeleton ha la stessa
funzione per il server. Stub e skeleton sono generati nel linguaggio adottato da un compilatore
apposito che opera partendo da una definizione IDL.
Dynamic Invocation Interface (DII) è l’interfaccia che consente a un client di inviare dinamicamente una request a un oggetto remoto, senza conoscerne la definizione dell’interfaccia e senza avere un legame con lo stub. Consente inoltre a un client di effettuare due tipi di chiamate asincrone: deferred synchronous (separa le operazioni di send e di receive) e oneway (solo send).
Dynamic Skeleton Interface (DSI) è l’analogo lato server del DII. Consente a un ORB di recapitare una request a un oggetto che non ha uno skeleton statico, ossia non è stato definito precisamente il tipo a tempo di compilazione. Il suo utilizzo è totalmente trasparente a un client.
Object Adapter assiste l’ORB nel recapitare le request a un oggetto e nelle operazioni di attivazione/disattivazione degli oggetti. Il suo compito principale è quello di legare l’implementazione
di un oggetto all’ORB.
Invocazione CORBA
Utilizzando l’ORB, un client può inviare una Request in modo trasparente a un oggetto CORBA
che risieda sulla stessa macchina od ovunque sulla rete.
Per raggiungere questo livello di astrazione, ogni oggetto remoto è dotato di uno stub e di uno
skeleton; questi due elementi agiscono rispettivamente da collante tra client e ORB e tra ORB e
oggetto CORBA.
In maniera similare a quanto accade in RMI, lo stub effettua il marshalling dei dati, traslando i
data types dal linguaggio di programmazione client-side a un generico formato CORBA;
quest’ultimo è convogliato via rete dal messaggio di Request.
Il client invoca i metodi non sull’oggetto remoto, bensì sul suo stub locale; l’effettiva invocazione remota viene operata dallo stub. Come si vedrà più dettagliatamente in seguito, il meccanismo dello stub è una precisa implementazione del pattern Proxy.
In maniera speculare a quanto effettuato dallo stub, l’unmarshalling dei dati è eseguito sul server
dallo skeleton; in questo caso il formato della Request viene traslato nel linguaggio di programmazione server-side.
Come si è detto in precedenza, Stub e skeleton sono generati automaticamente da un compilatore a partire dalla definizione IDL dell’oggetto CORBA.
6
Figura 2 — Una richiesta da client a oggetto CORBA.
Interfaccia e funzionalità di un ORB
L’interfaccia di un ORB è definita dalle specifiche CORBA. La maggior parte degli ORB forniscono alcune operazioni addizionali, ma esistono alcuni metodi che dovrebbero essere forniti da
tutte le implementazioni.
L’inizializzazione dell’ORB va effettuata invocando il metodo init
static ORB init()
static ORB init(Applet app, Properties props)
static ORB init(String[] args, Properties props)
Il metodo senza parametri opera da singleton, restituendo un ORB di default con alcune limitazioni. Gli altri due metodi restituiscono un ORB con le proprietà specificate e sono pensati esplicitamente per le Java application e le Applet. L’array di stringhe e l’oggetto Properties consentono di impostare alcune proprietà dell’istanza di ORB restituita dal metodo init; l’array
viene usato per i parametri da linea di comando. Le proprietà standard ORBClass e ORBSingletonClass consentono ad esempio di specificare l’utilizzo di un custom ORB differente da quello di default. Ogni implementazione fornisce anche proprietà aggiuntive; tutte le proprietà non
riconosciute sono semplicemente ignorate.
Altre funzionalità sicuramente offerte da un ORB sono le operazioni relative agli object reference. Ogni riferimento a un oggetto (Interoperable Object Reference, IOR) può essere convertito in
stringa; è garantito anche il processo inverso.
String object_to_string(Object obj)
Object string_to_object(String str)
Prima che un oggetto remoto sia utilizzabile da un client, va attivato sull’ORB. Come si vedrà in
seguito esistono più tipologie di attivazione. Il modo più semplice in assoluto è dato dal metodo
connect.
void connect(Object obj)
void disconnect(Object obj)
Il metodo disconnect disattiva l’oggetto consentendo al garbage collector di rimuoverlo.
7
In un contesto distribuito è necessario avere a disposizione meccanismi che consentano di scoprire quali oggetti CORBA sono disponibili e ottenere un riferimento ad essi. Anche in questo
caso esistono più possibilità, la più semplice è fornita da due metodi dell’ORB
String[] list_initial_services()
Object resolve_initial_references(String object_name)
Il primo metodo elenca i servizi disponibili sull’ORB, mentre il secondo restituisce un generico
riferimento a un oggetto individuato per nome. Va precisato che un servizio è comunque un oggetto remoto e quindi recuperabile via resolve_initial_references.
Interoperabilità tra ORB
Le specifiche CORBA 1.1 si limitavano a dare le basi per la portabilità di oggetti applicativi e
non garantivano affatto l’interoperabilità tra differenti implementazioni di ORB. Le specifiche
2.0 colmarono questa significativa lacuna con la definizione di un protocollo (GIOP) espressamente pensato per interazioni ORB-to-ORB.
Il General Inter-ORB Protocol specifica un insieme di formati di messaggi e di rappresentazioni
dati comuni per la comunicazione tra ORB. I tipi di dato definiti da OMG sono mappati in un
messaggio di rete flat (Common Data Representation, CDR).
GIOP definisce un formato multi-ORB di riferimento a un oggetto remoto, l’Interoperable Object
References (IORs). L’informazione contenuta e specificata dalla struttura dello IOR assume significato indipendentemente dall’implementazione dell’ORB, consentendo a un’invocazione di
transitare da un ORB a un altro. Ogni ORB fornisce un metodo object_to_string che consente di ottenere una rappresentazione stringa dello IOR di un generico oggetto.
Vista la diffusione di TCP/IP, comunemente viene usato l’Internet Inter-ORB Protocol (IIOP)
che specifica come i messaggi GIOP vengono scambiati su TCP/IP. IIOP è considerato il protocollo standard CORBA e quindi ogni ORB deve connettersi con l’universo degli altri ORB
traslando le request sul e dal backbone IIOP.
Tools e implementazioni CORBA
Per realizzare un’applicazione che utilizzi il middleware definito da OMG, occorre in primo luogo disporre di un prodotto che ne fornisca un’implementazione. La garanzia fornita è comunque
quella di scrivere codice utilizzabile con differenti prodotti CORBA.
Lo standard CORBA è dinamico e complesso. Di conseguenza, lo scenario dei prodotti attualmente disponibili è in continuo divenire e il livello di aderenza dei singoli prodotti alle specifiche
non è quasi mai completo. In ogni caso è sempre possibile utilizzare CORBA in maniera tale da
garantire un’elevata portabilità.
È necessario comunque prestare molta attenzione alla scelta dell’ORB da utilizzare in quanto
questi differiscono sia come prestazioni, sia come funzionalità fornite. Per una panoramica completa dei prodotti CORBA disponibili si veda prodotti CORBA in bibliografia.
8
Gli esempi presenti in questo documento fanno esplicito riferimento a due implementazioni:
Sun Java IDL e Inprise VisiBroker. L’utilizzo di altri ORB con questi esempi potrebbe comportare modifiche.
Java IDL attualmente è disponibile in due versioni decisamente differenti. L’implementazione
fornita con il JDK 1.2 è limitata e il compilatore IDL va scaricato a parte da
http://developer.java.sun.com/developer/earlyAccess/jdk12/idltojava.html. Una
migliore implementazione è presente invece nel Java 2 SDK a partire dalla versione 1.3.
Inprise VisiBroker è probabilmente la migliore e più diffusa implementazione CORBA presente
sul mercato; è disponibile in versione trial: si veda VisiBroker in bibliografia.
Interface Definition Language
CORBA fornisce una chiara separazione tra l’interfaccia di un oggetto e la sua implementazione.
In modo simile a quanto accade in RMI, il client non si deve occupare in modo diretto dei dettagli di implementazione, ma solo dell’interfaccia implementata dall’oggetto che intende utilizzare.
In un middleware distribuito tutti gli oggetti, compresi quelli che lo compongono, sono trattati
come interfacce. Questo è sia una valida scelta di design, sia un’esigenza di distribuzione: un
client tipicamente non conosce e non deve conoscere l’implementazione di un oggetto destinato
a essere eseguito su una macchina server.
Questa considerazione ha una valenza ancora maggiore in un contesto tecnologico che consente
ad esempio il dialogo tra oggetti Java e procedure Cobol che per natura probabilmente risiederanno addirittura su macchine ad architetture differenti.
Poiché CORBA è trasparente rispetto al linguaggio, OMG ha definito nelle sue specifiche un
nuovo linguaggio interamente descrittivo (IDL) destinato alla definizione delle interfacce degli
oggetti CORBA. In momenti successivi sono stati definiti i differenti mapping tra i vari linguaggi
di programmazione e IDL. È da notare che in molti dei linguaggi utilizzabili con CORBA non
esiste il concetto di interfaccia (ad esempio COBOL e C).
Un oggetto remoto quindi, indipendentemente dal fatto che sia applicativo o appartenente
all’infrastruttura (l’ORB, i servizi, ecc.), per essere utilizzato in un middleware CORBA deve essere in primo luogo definito mediante IDL. Nel caso di un oggetto applicativo la definizione sarà a carico dello sviluppatore, nel caso di un oggetto di infrastruttura viene fornita da OMG. Ecco ad esempio parte della definizione IDL dell’ORB:
// IDL
module CORBA {
interface ORB {
string object_to_string (in Object obj);
Object string_to_object (in string str);
Object resolve_initial_references (in ObjectId
identifier) raises (InvalidName);
// ecc...
9
};
};
Sintassi e caratteristiche
La sintassi IDL è chiaramente C-like e di conseguenza è piuttosto simile anche alla sintassi Java.
Sebbene sia un linguaggio descrittivo orientato agli oggetti, in modo simile al C++, IDL include
la possibilità, non contemplata da Java, di definire strutture dati che non siano classi.
I blocchi logici IDL sono racchiusi in parentesi graffe; a differenza di Java è necessario terminare sempre il blocco con un “ ; ” e anche il singolo statement è terminato da un “ ; ”. Con “ :: ”
è possibile specificare la gerarchia delle classi (equivale al “ . ” Java, per esempio CORBA::Object).
Nelle specifiche si parla di IDL come di un linguaggio case-insensitive, ma esistono implementazioni che non rispettano questa direttiva. A proposito delle regole di naming, va notato che
CORBA non nasce nel mondo Java e quindi i tool IDL e le interfacce definite da OMG non rispettano le regole di naming abituali in un contesto Java.
In IDL è importante la sequenza delle definizioni dei vari elementi. Non è possibile utilizzare un
elemento, sia esso una exception, una struttura dati o un’interfaccia, se non è già stato definito o
almeno dichiarato; esiste comunque un meccanismo di forward declaration.
IDL non implementa l’override e l’overload, queste limitazioni sono legate al fatto che molti dei
linguaggi supportati non forniscono queste caratteristiche.
A differenza di quanto accade in Java, in un file IDL possono esistere molte interfacce pubbliche.
IDL in pratica
La definizione IDL di un oggetto permette di specificare solo gli aspetti relativi alla sua interfaccia. Si potranno quindi definire le signature dei metodi, le eccezioni che questi rilanciano,
l’appartenenza ai package, costanti e strutture dati manipolate dai metodi.
Data la definizione IDL sarà necessario utilizzare un apposito compilatore fornito a corredo
dell’ORB. Dalla compilazione si otterranno un buon numero di file .java, fra cui stub, skeleton e altri contenenti codice di supporto per l’aggancio all’ORB. A partire dai file generati sarà
possibile realizzare l’opportuna implementazione Java.
Si provi a definire ad esempio una semplice costante in IDL
// IDL
module basic {
const float PI = 3.14159;
};
Si compili il file IDL creato (nell’esempio basic.idl). Per la sintassi e il significato dei flag usati si rimanda alla documentazione dell’ORB.
10
idltojava -fno-cpp basic.idl
(per l’implementazione JDK 1.2)
idlj –fall basic.idl
(per l’implementazione J2SE 1.3)
idl2java –boa basic.idl
(per l’implementazione VisiBroker)
Verrà creata una sottodirectory basic e un file PI.java
// JAVA
package basic;
public interface PI {
public final static float value = (float)3.14159;
}
La generazione del file operata dal compilatore IDL è basata sulle regole di mapping definite da
OMG per il linguaggio Java.
Mapping IDL–Java
La trasposizione da linguaggio IDL a linguaggio Java effettuata dal compilatore si basa
sull’insieme di regole definite da OMG che costituiscono il mapping tra i due linguaggi.
Tipi base
La definizione di regole di mapping tra IDL e un linguaggio di programmazione implica in primo luogo la definizione di corrispondenze tra i differenti tipi di base; a runtime questo può causare errori di conversione durante il marshalling dei dati. La gestione di questi errori a runtime è
a carico del programmatore.
Il problema si pone tipicamente per i tipi più ampi in Java che in IDL; ad esempio per i char che
in Java, a differenza della maggior parte degli altri linguaggi, sono trattati come Unicode (16 bit)
e non ASCII (8 bit). In IDL i tipi che trattano caratteri Unicode sono wchar e wstring.
IDL Type
Java type
Exceptions
boolean
char
wchar
octet
string
boolean
char
char
byte
java.lang.String
wstring
java.lang.String
short
unsigned short
long
unsigned long
long long
unsigned long long
float
double
fixed
short
short
int
int
long
long
float
double
java.math.BigDecimal
CORBA::DATA_CONVERSION
CORBA::DATA_CONVERSION
CORBA::MARSHAL
CORBA::DATA_CONVERSION
CORBA::MARSHAL
CORBA::DATA_CONVERSION
11
CORBA::DATA_CONVERSION
Alcuni dei tipi supportati da IDL non trovano corrispondenza in Java (ad esempio i tipi une FALSE in IDL sono costanti e vengono mappate con i literal Java true e false.
signed). TRUE
Particolare attenzione va prestata all’utilizzo di null: CORBA non ha la nozione di null riferito alle stringhe o agli array. Un parametro stringa dovrà ad esempio essere trattato come una
stringa vuota pena l’eccezione org.omg.CORBA.BadParam.
In IDL ogni tipo, base o complesso, può essere associato a un nome mediante la parola chiave
typedef; poiché in Java il concetto di alias per un tipo non esiste, nel codice generato verranno
usati comunque i tipi primitivi che lo compongono.
Module e interface
Come forse si è notato nell’esempio precedente la parola chiave module viene mappata esattamente sul package Java
// IDL
module basic {...}
// generated Java
package basic;
In IDL la keyword interface permette di specificare la vera e propria interfaccia dell’oggetto remoto definendone dati membro e metodi (nel gergo CORBA attributi e operazioni).
Il mapping di un’interface è ottenuto con la generazione di un’interfaccia e alcune classi Java.
Definendo la semplice interfaccia IDL
// IDL
module basic {
interface HelloWorld {
string hello();
};
};
il compilatore creerà una directory basic e una serie di file Java (usando VisiBroker verranno
generati anche altri file):
_HelloWorldImplBase è lo skeleton, la classe base per la generazione dell’oggetto remoto; fornisce i meccanismi di ricezione di una request dall’ORB e quelli di risposta.
_HelloWorldStub è lo stub, l’implementazione client-side dell’oggetto remoto; fornisce i meccanismi di conversione tra l’invocazione del metodo e l’invocazione via ORB dell’oggetto remoto.
HelloWorldOperations
HelloWorld
è l’interfaccia Java che contiene le signature dei metodi.
è l’interfaccia Java dell’oggetto remoto, specializza HelloWorldOperations.
12
HelloWorldHelper
e HelloWorldHolder saranno spiegati più avanti.
Insieme le interfacce HelloWorldOperations e HelloWorld definiscono l’interfaccia
dell’oggetto CORBA; sono dette rispettivamente operations interface e signature interface. Il
JDK 1.2 utilizza vecchie regole di mapping e non genera l’interfaccia operation.
La signature interface generata sarà
// generated Java
package basic;
public interface HelloWorld extends HelloWorldOperations,
org.omg.CORBA.Object, org.omg.CORBA.portable.IDLEntity
}
mentre l’operations interface sarà
package basic;
public interface HelloWorldOperations {
String hello ();
}
Come si vedrà più avanti, le altre classi serviranno come base per l’implementazione e l’utilizzo
dell’oggetto remoto vero e proprio.
Il linguaggio IDL supporta l’ereditarietà multipla utilizzando la normale derivazione Java tra interfacce.
// IDL
module basic {
interface ClasseBaseA {
void metodoA();
};
interface ClasseBaseB {
void metodoB();
};
interface ClasseDerivataAB: ClasseBaseA, ClasseBaseB {
};
};
ClasseDerivataAB
deriva dalle altre due interfacce e avrà quindi una rappresentazione Java.
// generated Java
package basic;
public interface ClasseDerivataAB extends ClasseDerivataABOperations,
basic.ClasseBaseA, basic.ClasseBaseB {
}
un oggetto di questo tipo dovrà quindi fornire l’implementazione dei due metodi (metodoA e
metodoB).
13
Attributi e metodi
In IDL le signature dei vari metodi sono fornite in maniera simile a Java. Per comodità è
possibile dare una definizione dei metodi accessori di un attributo (i classici get e set Java)
utilizzando la keyword attribute con l’eventuale modificatore readonly.
// IDL
module basic {
interface Motocicletta {
readonly attribute string colore;
void cambiaMarcia(in long marcia);
};
};
Poiché l’attributo colore è readonly, sarà generato solo il corrispondente metodo di lettura.
// generated Java
package basic;
public interface MotociclettaOperations {
String colore();
void cambiaMarcia(int marcia);
}
In IDL il passaggio di parametri a un metodo implica la dichiarazione del tipo di passaggio che
si desidera adottare. Mentre in Java il passaggio per valore (tipi primitivi) o per riferimento (oggetti, array, ecc.) è implicitamente associato al tipo, in IDL è possibile specificarlo utilizzando
nella signature le keyword in, out o inout. Come si può intuire un parametro out può essere modificato dal metodo invocato.
Poiché in Java non tutti i parametri sono trattati per riferimento, esistono delle classi wrapper
apposite dette Holder.
Classi Holder
Le classi Holder sono utilizzate per supportare il passaggio di parametri out e inout. Come si
è visto in precedenza, dalla compilazione di un’interfaccia IDL viene generata una corrispondente classe <NomeInterfaccia>Holder; l’Holder è generato per ogni tipo utente. Nel package
org.omg.CORBA sono forniti gli Holder per tutti i tipi primitivi. Ogni Holder fornisce un costruttore di default che inizializza il contenuto a false, 0, null o null unicode a seconda del tipo.
Ecco per esempio l’Holder del tipo base int:
// JAVA
final public class IntHolder implements org.omg.CORBA.portable.Streamable {
public int value;
public IntHolder() {}
public IntHolder(int initial) {...}
public void _read(org.omg.CORBA.portable.InputStream is) {...}
14
public void _write(org.omg.CORBA.portable.OutputStream os) {...}
public org.omg.CORBA.TypeCode _type() {...}
}
Classi Helper
Per ogni tipo definito dall’utente il processo di compilazione genera una classe Helper con il
nome <TipoUtente>Helper. La classe Helper è astratta e fornisce alcuni metodi statici che
implementano varie funzionalità per manipolare il tipo associato (lettura e scrittura del tipo
da/verso uno stream, lettura del repository id, e così via).
L’unica funzionalità di utilizzo comune è fornita dal metodo narrow implementato dall’Helper
// generated Java
package basic;
public class HelloWorldHelper {
//...
public static basic.HelloWorld narrow(org.omg.CORBA.Object that)
throws org.omg.CORBA.BAD_PARAM {
if (that == null)
return null;
if (that instanceof basic.HelloWorld)
return (basic.HelloWorld) that;
if (!that._is_a(id())) {
throw new org.omg.CORBA.BAD_PARAM();
}
org.omg.CORBA.portable.Delegate dup
= ((org.omg.CORBA.portable.ObjectImpl) that)._get_delegate();
basic.HelloWorld result = new basic._HelloWorldStub(dup);
return result;
}
}
Il metodo narrow effettua un cast “sicuro” dal generico Object Corba al tipo definito. Grazie a
una serie di controlli ciò che verrà ritornato sarà sicuramente un oggetto del tipo atteso oppure
una Exception CORBA BAD_PARAM.
Tipi strutturati
Come detto, mediante IDL è possibile dare la definizione di entità che non siano classi o interfacce, ma semplici strutture dati. Il mapping con Java sarà comunque operato mediante classi e
interfacce.
Esistono tre categorie di tipi strutturati: enum, union e struct. Tutti i tipi strutturati sono
mappati in Java con una final class fornita degli opportuni campi e costruttori, Helper e Holder.
15
L’enum è una lista ordinata di identificatori, la union è un incrocio tra la Union C e
un’istruzione di switch, la struct è una struttura dati che consente di raggruppare al suo interno più campi.
// IDL
module basic {
enum EnumType {first, second, third, fourth, fifth};
union UnionType switch (EnumType) {
case first: long win;
case second: short place;
default: boolean other;
};
struct Struttura {
string campoA;
string campoB;
};
};
Nell’esempio, Struttura sarà mappata con Helper, Holder e la classe
// generated Java
package basic;
public final class Struttura implements org.omg.CORBA.portable.IDLEntity {
// instance variables
public String campoA;
public String campoB;
// constructors
public Struttura() { }
public Struttura(String __campoA, String __campoB) {
campoA = __campoA;
campoB = __campoB;
}
}
Sequence e array
In IDL esistono due collezioni tipizzate di dati: sequence e array. Entrambe sono mappate su
array Java. Le sequence possono avere dimensioni predefinite (bounded) o non predefinite (unbounded).
// IDL
module basic {
typedef sequence<octet> ByteSequence;
typedef string MioArray[20];
struct StrutturaConArray {
ByteSequence campoA;
};
};
La compilazione dell’esempio genererà solo Helper e Holder per ByteSequence e MioArray.
All’interno della struttura il tipo ByteSequence sarà trattato come array di byte.
16
// generated Java
package basic;
public final class StrutturaConArray implements org.omg.CORBA.portable.IDLEntity {
// instance variables
public byte[] campoA;
// constructors
public StrutturaConArray() { }
public StrutturaConArray(byte[] __campoA) {
campoA = __campoA;
}
}
Exception
La definizione di una exception in IDL non è dissimile da quella di una struct. La signature del
metodo che la rilancia utilizza la keyword raises (equivalente del Java throws).
// IDL
module basic {
exception MyCustomException {
string reason;
};
interface HelloWorldWithException {
string hello() raises (MyCustomException);
};
};
Anche il mapping Java assomiglia a quello di una struct, quindi una classe final con i campi definiti nell’exception e i costruttori opportuni più i soliti Helper e Holder
// generated Java
package basic;
public final class MyCustomException extends org.omg.CORBA.UserException
implements org.omg.CORBA.portable.IDLEntity {
// instance variables
public String reason;
// constructors
public MyCustomException() {
super();
}
public MyCustomException(String __reason) {
super();
reason = __reason;
}
}
Le SystemException CORBA derivano da java.lang.RuntimeException, mentre ogni
UserException definita in una IDL specializza java.lang.Exception. Per questa ragione è
obbligatorio l’handle or declare su tutte le eccezioni utente, mentre non lo è per tutte le SystemException CORBA (CORBA::MARSHAL, CORBA::OBJECT_NOT_EXIST, ecc.).
17
Figura 3 — Gerarchia delle eccezioni CORBA.
Un po’ di pratica
In un caso semplice i passi da seguire per creare, esporre e utilizzare un oggetto CORBA sono i
seguenti:
— Descrivere mediante IDL l’interfaccia dell’oggetto che si intende implementare.
— Compilare con il tool apposito il file IDL.
— Identificare tra le classi e le interfacce generate quelle necessarie alla definizione
dell’oggetto e specializzarle opportunamente.
— Scrivere il codice necessario per inizializzare l’ORB e informarlo circa la presenza
dell’oggetto creato.
— Compilare il tutto con un normale compilatore Java.
— Avviare la classe di inizializzazione e l’applicazione distribuita.
Definizione IDL
Si definisca un semplice oggetto Calcolatrice che esponga un metodo in grado di computare
la somma tra due numeri dati in input.
// IDL
module utility {
interface Calcolatrice {
long somma(in long a, in long b);
};
};
Si compili il file Calcolatrice.idl mediante il compilatore fornito dall’ORB. Il processo di
compilazione creerà una directory utility e gli opportuni file Java: _CalcolatriceImplBase,
18
_CalcolatriceStub, CalcolatriceOperations, Calcolatrice, CalcolatriceHelper
CalcolatriceHolder.
e
Implementare l’oggetto remoto
La classe base per l’implementazione è la classe astratta _CalcolatriceImplBase.java, ovvero lo skeleton.
Figura 4 — Gerarchia di derivazione della classe servant.
Si può notare nella fig. 4 come l’implementazione dell’interfaccia “remota” Calcolatrice non
sia a carico dell’oggetto remoto, bensì a carico dello skeleton.
Lo skeleton è una classe astratta e non fornisce alcuna implementazione del metodo somma definito nell’interfaccia IDL. Quindi per definire il servant CalcolatriceImpl sarà necessario
specializzare _CalcolatriceImplBase e fornire l’opportuna implementazione del metodo
somma.
Ecco il codice completo del servant:
// JAVA
package server;
import utility.*;
public class CalcolatriceImpl extends _CalcolatriceImplBase {
public CalcolatriceImpl() {
super();
}
// Implementazione del metodo remoto
public int somma(int a, int b) {
return a + b;
}
}
19
Poiché Java non supporta l’ereditarietà multipla, in alcune situazioni può essere limitante dover
derivare necessariamente il servant da ImplBase. Nel caso in cui il servant debba derivare da
un’altra classe è possibile utilizzare un meccanismo alternativo di delega detto Tie che non implica la specializzazione di ImplBase. In questo capitolo l’approccio Tie non sarà esaminato.
Implementare la classe Server
Si è già avuto modo di notare come nel gergo CORBA il componente remoto che espone i servizi venga definito servant. Il server invece è la classe che inizializza l’environment, istanzia
l’oggetto remoto, lo rende disponibile ai client e si pone in attesa.
La classe server è quindi una classe di servizio che ha come compito fondamentale quello di creare e agganciare all’ORB l’istanza di oggetto remoto che utilizzeranno i client e di fornire a questa un contesto di esecuzione.
L’inizializzazione dell’ORB è effettuata utilizzando il metodo init.
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args, null);
Il parametro args è semplicemente l’array di input. Saranno quindi valorizzabili da linea di
comando alcune proprietà dell’ORB (per un elenco delle proprietà disponibili consultare la documentazione dell’implementazione CORBA utilizzata).
In questo primo esempio l’aggancio è effettuato senza l’ausilio di un Object Adapter. Come già
anticipato, una semplice forma di “registrazione” è fornita dal metodo connect dell’ORB
CalcolatriceImpl calc = new CalcolatriceImpl();
orb.connect(calc);
Utilizzando un meccanismo di questo tipo, il servant va considerato come un oggetto CORBA
di tipo transient. Un riferimento a un oggetto di questo tipo è valido solo nel tempo di vita di
una precisa istanza del servant. Più avanti saranno analizzati gli oggetti di tipo persistent.
Per ogni ORB CORBA 2.0 compliant, l’object reference (IOR) in versione stringa è ottenibile
invocando
orb.object_to_string(calc)
La stringa ottenuta è il riferimento CORBA all’istanza di calcolatrice; come tale è esattamente
tutto ciò di cui necessita un client per accedere ai servizi dell’oggetto. Per fornire al client lo IOR
esistono molte soluzioni, la più semplice consiste nel salvarlo su file.
PrintWriter out
= new PrintWriter(new BufferedWriter(new FileWriter(args[0])));
out.println(orb.object_to_string(calc));
out.flush();
out.close();
A questo punto il server può mettersi in attesa. L’attesa è necessaria in quanto l’istanza di calcolatrice “vive” solo e soltanto nel contesto fornito dall’applicazione server; è all’interno di questa
20
che è stato effettuato il new. Si può implementare un’attesa idle del processo server utilizzando il
metodo wait di un Java Object
java.lang.Object sync = new java.lang.Object();
synchronized (sync) {
sync.wait();
}
Ecco il codice completo della classe CalcolatriceServer.
// JAVA
package server;
import utility.*;
import java.io.*;
public class CalcolatriceServer {
public static void main(String[] args) {
if (args.length!=1) {
System.err.println("Manca argomento: path file ior");
return;
}
try {
// Inizializza l'ORB.
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args, null);
// Crea un oggetto Calcolatrice
CalcolatriceImpl calc = new CalcolatriceImpl();
orb.connect(calc);
// Stampa l'object reference in versione stringa
System.out.println("Creata Calcolatrice:\n"
+ orb.object_to_string(calc));
// Scrive l'object reference nel file
PrintWriter out
= new PrintWriter(new BufferedWriter(new FileWriter(args[0])));
out.println(orb.object_to_string(calc));
out.close();
// Attende l'invocazione di un client
java.lang.Object sync = new java.lang.Object();
synchronized (sync) {
sync.wait();
}
}
catch (Exception e) {
System.err.println("Server error: " + e);
e.printStackTrace(System.out);
}
}
}
21
Implementare il Client
Il client dovrà in primo luogo inizializzare l’ORB con il metodo init(), come effettuato nella
classe server.
Per ottenere il riferimento all’istanza di calcolatrice, il client dovrà leggere lo IOR memorizzato
nel file generato dal server.
BufferedReader in = new BufferedReader(new
String ior = in.readLine();
in.close();
FileReader(args[0]));
È possibile ottenere lo IOR invocando il metodo opposto a quello utilizzato per trasformarlo in
stringa. Il metodo string_to_object fornito dall’ORB restituisce un CORBA Object a partire dalla stringa che rappresenta il suo IOR.
org.omg.CORBA.Object obj = orb.string_to_object(ior);
Il metodo string_to_object restituisce un oggetto di tipo generico e non un riferimento che
consenta di invocare il metodo somma.
Per ottenere tale riferimento, in uno scenario Java, si effettuerebbe un cast (in RMI, ad esempio,
dopo aver effettuato una lookup si opera un cast per ottenere il tipo corretto). In un contesto
CORBA invece, per convertire il generico oggetto in un oggetto di tipo determinato, bisogna
utilizzare il metodo narrow della classe <Tipo>Helper.
Calcolatrice calc = CalcolatriceHelper.narrow(obj);
A questo punto il client è in condizione di invocare il metodo remoto con le medesime modalità
usate per una comune invocazione di metodo
calc.somma(a, b)
dove a e b sono da intendersi come 2 int. È da notare come questo non sia l’unico modello di
invocazione CORBA. Un’invocazione di questo tipo viene detta invocazione statica, più avanti
sarà affrontata l’invocazione dinamica.
Ecco il codice completo del client:
package client;
import utility.*;
import java.io.*;
public class CalcolatriceClient {
public static void main(String args[]) {
if (args.length!=1) {
System.err.println("Manca argomento: path file ior");
return;
}
22
try {
// Crea e inizializza l'ORB
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args, null);
// Legge dal file il reference all'oggetto
// Si assume che il server lo abbia generato
BufferedReader in
= new BufferedReader(new FileReader(args[0]));
String ior = in.readLine();
in.close();
// Ottiene dal reference un oggetto remoto...
org.omg.CORBA.Object obj = orb.string_to_object(ior);
// ...e ne effettua il narrow a tipo Calcolatrice
Calcolatrice calc = CalcolatriceHelper.narrow(obj);
// Ottiene da input tastiera i 2 numeri da sommare
BufferedReader inputUser
= new BufferedReader (new InputStreamReader(System.in));
String first, second;
int a, b;
// Leggo primo addendo
System.out.println ();
System.out.print("A = ");
first = inputUser.readLine();
a = Integer.valueOf(first).intValue ();
// Leggo secondo addendo
System.out.println ();
System.out.print("B = ");
second = inputUser.readLine();
b = Integer.valueOf(second).intValue ();
// Invoca il metodo remoto passandogli i parametri
System.out.println ();
System.out.print("Il risultato è: ");
System.out.print(calc.somma(a, b));
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Eseguire l’esempio
Dopo aver compilato tutte le classi, comprese quelle generate dal precompilatore, è finalmente
possibile eseguire l’esempio.
La prima classe da mandare in esecuzione è la classe CalcolatriceServer
java server.CalcolatriceServer calc.ior
passando come parametro il path del file su cui si intende memorizzare lo IOR. L’output prodotto sarà
23
Creata Calcolatrice:
IOR:000000000000001d49444c3a7574696c6974792f43616c636f6c6174726963653a
312e300000000000000001000000000000002c0001000000000004696e6b0005b70000
00000018afabcafe00000002a1ed120b000000080000000000000000
Si noti che, poiché il processo server si pone in attesa, l’esecuzione effettivamente non termina.
Se si terminasse il processo, il client avrebbe uno IOR inservibile in quanto l’istanza identificata
da questo non sarebbe più “viva”.
A questo punto si può mandare in esecuzione il client fornendogli il path del file generato dal
server
java client.CalcolatriceClient calc.ior
Il programma richiederà in input i due numeri da sommare (si noti che non sono stati gestiti eventuali errori di conversione) e stamperà, a seguito di un’invocazione remota, il risultato.
Client e server stub
È bene effettuare una breve digressione sul concetto di stub (letteralmente “surrogato”). Lo stub
compare in molti scenari di programmazione (ad esempio in DLL e in generale nella programmazione distribuita). Esiste una precisa corrispondenza con un celebre pattern di programmazione: il pattern Proxy.
Figura 5 — Il pattern Proxy e gli stub server e client.
24
Il pattern Proxy viene tipicamente utilizzato per aggiungere un livello di indirezione. Il Proxy è
un surrogato di un altro oggetto ed è destinato a controllare l’accesso a quest’ultimo. In generale
implica la presenza di un oggetto, detto Proxy, che abbia la stessa interfaccia dell’oggetto effettivo, detto Real Subject. Il Proxy riceve le richieste destinate al Real Subject e le comunica a
quest’ultimo effettuando eventualmente delle operazioni prima e/o dopo l’accesso.
Osservando la fig. 5 è possibile vedere come lo stub e lo skeleton implementino la stessa interfaccia, quella dell’oggetto remoto. Quindi un generico client sarà in grado di dialogare con lo
stub invocando i metodi che intende far eseguire all’oggetto remoto. In questo senso lo stub opera da procuratore dell’oggetto presso il client (proxy significa per l’appunto “procuratore”, “delegato”).
Lo stub, nella sua opera di delegato, sarà in grado di rendere invisibili al client tutti i dettagli della
comunicazione remota e della locazione fisica dell’oggetto. Nell’ottica del client il dialogo sarà
operato direttamente con l’oggetto remoto (questo è garantito dal fatto che lo stub implementa
l’interfaccia dell’oggetto remoto). Sostituire un’implementazione non avrà alcun impatto sul
client purché l’interfaccia rimanga inalterata.
Un possibile miglioramento
La soluzione proposta nell’esempio precedente è decisamente primitiva. Di fatto l’accesso a un
oggetto remoto è possibile solo se il client e il server condividono una porzione di file system (in
realtà sono utilizzabili anche altri meccanismi quali mail, floppy, …).
Un primo miglioramento potrebbe essere ottenuto utilizzando un Web Server. Con un Web
Server attivo un client potrebbe leggere il file contenente lo IOR via HTTP. Il codice di lettura
del client potrebbe essere qualcosa del genere:
URL urlIOR = new URL(args[0]);
DataInputStream in = new DataInputStream(urlIOR.openStream());
String ior = in.readLine();
in.close();
Al client andrebbe fornito non più il path, ma l’URL corrispondente al file generato dal server,
ad esempio http://localhost/corba/calc.ior.
Il server dovrà generare il file nella virtual directory corretta del Web Server (corba
nell’esempio). Nel caso non si disponga di un Web Server è possibile scaricare gratuitamente lo
“storico” Apache da www.apache.org.
Anche con questa modifica la soluzione, pur essendo praticabile in remoto e totalmente portabile, è ben lontana dall’ottimale. Tra i difetti che presenta è bene notare come in parte violi la
location transparency promessa da CORBA: non si conosce l’effettiva collocazione
dell’implementazione, ma è necessario conoscere l’URL del file IOR.
Un approccio decisamente migliore prevede l’utilizzo del CORBA Naming Service.
25
CORBA Naming Service
Il Naming Service è sicuramente il principale meccanismo CORBA per la localizzazione di oggetti su un ORB. Fa parte delle specifiche CORBA dal 1993 ed è il servizio più importante tra
quelli standardizzati da OMG.
Fornisce un meccanismo di mapping tra un nome e un object reference, quindi rappresenta anche un metodo per rendere disponibile un servant a un client remoto. Il meccanismo è simile a
quello di registrazione e interrogazione del registry in RMI.
Nella programmazione distribuita l’utilizzo di un nome per reperire una risorsa ha degli evidenti
vantaggi rispetto all’utilizzo di un riferimento. In primo luogo il nome è significativo per lo sviluppatore, in secondo luogo è completamente indipendente dagli eventuali restart dell’oggetto
remoto.
Struttura del Naming Service
L’idea base del Naming Service è quella di incapsulare in modo trasparente i servizi di naming e
directory già esistenti. I nomi quindi sono strutturabili secondo uno schema gerarchico ad albero, uno schema astratto indipendente dalle singole convenzioni delle varie piattaforme di naming
o di directory.
L’operazione che associa un nome a un reference è detta bind (esiste anche l’operazione di unbind). L’operazione che recupera un reference a partire da un nome è detta naming resolution.
Esistono dei naming context all’interno dei quali il nome è univoco. I naming context possono
essere ricondotti ai nodi intermedi dell’albero di naming e al concetto di directory in un file
system. Possono esistere più nomi associati a uno stesso oggetto.
In questo scenario quindi un nome è una sequenza di name components; questi formano il cosiddetto compound name. I nodi intermedi sono utilizzati per individuare un context, mentre i
nodi foglia sono i simple name.
Figura 6 — Struttura ad albero di Naming.
26
Un compound name quindi individua il cammino (path) che, attraverso la risoluzione di tutti i
context, porta al simple name che identifica la risorsa.
Ogni NameComponent è una struttura con due elementi. L’identifier è la stringa nome, mentre il
kind è un attributo associabile al component. Questo attributo non è considerato dal Naming
Service, ma è destinato al software applicativo.
La definizione IDL del NameComponent è la seguente
// IDL
module CosNaming {
struct NameComponent {
Istring id;
Istring kind;
};
typedef sequence <NameComponent> Name;
}
L’interfaccia principale è NamingContext e fornisce tutte le operazioni necessarie alla definizione dell’albero di Naming e alla sua navigazione. Per quel che concerne il bind vengono fornite due funzionalità di bind tra Name – Object, due funzionalità di bind Name – NamingContext
e una funzionalità di unbind.
// IDL
module CosNaming{
// ...
interface NamingContext
{
// ...
void bind(in Name n,
in Object obj) raises(NotFound, CannotProceed,
InvalidName, AlreadyBound);
void rebind(in Name n,
in Object obj) raises(NotFound, CannotProceed,
InvalidName);
void bind_context(in Name n,
in NamingContext nc) raises(NotFound, CannotProceed,
InvalidName, AlreadyBound);
void rebind_context(in Name n,
in NamingContext nc) raises(NotFound, CannotProceed,
InvalidName);
void unbind(in Name n) raises( NotFound, CannotProceed, InvalidName);
};
};
27
I metodi rebind differiscono dai metodi bind semplicemente nel caso in cui il nome sia già presente nel context; rebind sostituisce l’object reference mentre bind rilancia un’eccezione AlreadyBound.
La definizione dell’albero implica anche la possibilità di creare o distruggere i NamingContext.
// IDL
module CosNaming{
// ...
interface NamingContext
{
// ...
NamingContext new_context();
NamingContext bind_new_context(in Name n) raises(NotFound, AlreadyBound,
CannotProceed, InvalidName);
void destroy() raises(NotEmpty);
};
};
La risoluzione di un name è attuata mediante il metodo resolve. La procedura di risoluzione di
un nome gerarchico implicherà la navigazione ricorsiva dell’albero di context.
// IDL
module CosNaming{
// ...
interface NamingContext
{
// ...
Object resolve(in Name n) raises(NotFound, CannotProceed, InvalidName);
};
};
La navigazione del Naming è invece legata all’utilizzo del metodo list che ritorna un insieme
di name sui quali è possibile operare iterativamente. Più precisamente ciò che viene ritornato è
un oggetto di tipo BindingIterator che, tramite i metodi next_one e next_n, permette di
navigare attraverso tutti i bind associati al context.
// IDL
module CosNaming{
//...
interface BindingIterator {
boolean next_one(out Binding b);
boolean next_n(in unsigned long how_many, out BindingList bl);
28
void destroy();
};
interface NamingContext
{
// ...
void list(in unsigned long how_many, out BindingList bl,
out BindingIterator bi);
};
};
Utilizzare il Naming Service
Alla luce di quanto visto finora sul Naming Service, è possibile migliorare l’esempio della calcolatrice visto in precedenza. Lo scenario generale non cambia: esiste un oggetto servant (non necessita di alcuna modifica), un oggetto server di servizio (pubblica il servant) e il client.
Il Naming Service ha impatto solo sulle modalità di registrazione e di accesso all’oggetto, quindi
solo sul client e sul server. Anche l’IDL non necessita di alcuna modifica.
Per pubblicare un oggetto, il server dovrà in primo luogo ottenere un riferimento al Naming
Service. Come detto in precedenza, è possibile ottenere un riferimento a un qualunque servizio
CORBA invocando sull’ORB il metodo resolve_initial_references. Ottenuto il riferimento, come per qualunque oggetto CORBA, andrà utilizzato il narrow fornito dal corrispondente Helper.
org.omg.CORBA.Object objRef
= orb.resolve_initial_references("NameService");
NamingContext ncRef = NamingContextHelper.narrow(objRef);
A questo punto è possibile effettuare il bind tra il NameComponent opportuno e l’istanza di servant (calc è in questo caso l’istanza di CalcolatriceImpl; si veda più avanti il codice completo).
NameComponent nc = new NameComponent("Calc", " ");
NameComponent path[] = {nc};
ncRef.rebind(path, calc);
I due parametri del costruttore NameComponent sono, rispettivamente, il name e il kind.
Ecco il codice completo della classe server
package server;
import
import
import
import
utility.*;
java.io.*;
org.omg.CosNaming.*;
org.omg.CosNaming.NamingContextPackage.*;
public class CalcolatriceServer {
29
public static void main(String[] args) {
try {
// Inizializza l'ORB.
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args,null);
// Crea un oggetto Calcolatrice
CalcolatriceImpl calc = new CalcolatriceImpl();
orb.connect(calc);
// Stampa l'object reference in versione stringa
System.out.println("Creata Calcolatrice:\n"
+ orb.object_to_string(calc));
// Root naming context
org.omg.CORBA.Object objRef
= orb.resolve_initial_references("NameService");
NamingContext ncRef = NamingContextHelper.narrow(objRef);
// Fa il bind dell'oggetto nel Naming
NameComponent nc = new NameComponent("Calc", " ");
NameComponent path[] = {nc};
ncRef.rebind(path, calc);
// Attende l'invocazione di un client
java.lang.Object sync = new java.lang.Object();
synchronized (sync) {
sync.wait();
}
} catch (Exception e) {
System.err.println("Server error: " + e);
e.printStackTrace(System.out);
}
}
}
Il client dovrà effettuare le stesse operazioni per ottenere il riferimento al Naming Service. Poi
dovrà effettuare la resolve per ottenere un riferimento all’oggetto remoto Calcolatrice.
NameComponent nc = new NameComponent("Calc", " ");
NameComponent path[] = {nc};
Calcolatrice calc = CalcolatriceHelper.narrow(ncRef.resolve(path));
Ecco il codice completo della classe client.
package client;
import
import
import
import
utility.*;
java.io.*;
org.omg.CosNaming.*;
org.omg.CORBA.*;
public class CalcolatriceClient {
public static void main(String args[]) {
try {
// Crea e inizializza l'ORB
30
ORB orb = ORB.init(args, null);
// Root naming context
org.omg.CORBA.Object objRef
= orb.resolve_initial_references("NameService");
NamingContext ncRef
= NamingContextHelper.narrow(objRef);
// Utilizzo il Naming per ottenere il riferimento all'oggetto
NameComponent nc = new NameComponent("Calc", "");
NameComponent path[] = {nc};
Calcolatrice calc = CalcolatriceHelper.narrow(ncRef.resolve(path));
// Ottiene da input tastiera i 2 numeri da sommare
BufferedReader inputUser
= new BufferedReader (new InputStreamReader(System.in));
String first, second;
int a, b;
// Leggo primo addendo
System.out.println ();
System.out.print("A = ");
first = inputUser.readLine();
a = Integer.valueOf(first).intValue ();
// Leggo secondo addendo
System.out.println ();
System.out.print("B = ");
second = inputUser.readLine();
b = Integer.valueOf(second).intValue ();
// Invoca il metodo remoto passandogli i parametri
System.out.println ();
System.out.print("Il risultato è: ");
System.out.print(calc.somma(a, b));
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Per eseguire l’esempio sarà necessario effettuare un passo in più rispetto a quanto fatto in precedenza, ovvero attivare il Naming Service prima di mandare in esecuzione il server.
L’attivazione del servizio è legata all’implementazione CORBA, sarà quindi differente da ORB a
ORB (nel caso si utilizzi VisiBroker sarà necessario avviare anche il tool OsAgent trattato in seguito).
tnameserv -ORBInitialPort 1050
(per l’implementazione Sun)
nameserv NameService
(per l’implementazione VisiBroker)
A questo punto sarà possibile avviare il server (per il significato dei flag utilizzati si rimanda alla
documentazione del prodotto).
31
java server.CalcolatriceServer -ORBInitialPort 1050
(per l’implementazione Sun)
java -DSVCnameroot=NameService server.CalcolatriceServer
(per l’implementazione VisiBroker)
E infine avviare il client
java client.CalcolatriceClient -ORBInitialPort 1050
(per l’implementazione Sun)
java -DSVCnameroot=NameService client.CalcolatriceClient
(per l’implementazione VisiBroker)
È possibile notare come in questa modalità l’utilizzo di CORBA abbia parecchie similitudini con
quello di RMI: l’utilizzo di tnameserv (nameserv) rimanda a quello di rmiregistry, così come i metodi del NamingContext bind e rebind rimandano ai metodi usati per RMI Naming.bind e Naming.rebind. Una volta ottenuto il reference, l’utilizzo dell’oggetto remoto è
sostanzialmente identico.
Accesso concorrente a oggetti remoti
Uno dei compiti più complessi nell’ambito della programmazione distribuita è
l’implementazione e la gestione della concorrenza. Un oggetto remoto distribuito sulla rete può
essere utilizzato contemporaneamente da più client. Questo è uno dei fattori che rendono allettante la programmazione distribuita poiché l’utilizzo in concorrenza di una qualunque risorsa
implica un suo migliore sfruttamento.
La gestione della concorrenza in ambiente distribuito è strettamente legata al design
dell’applicazione e tipicamente va a ricadere in uno dei tre approcci qui riportati.
Unico thread: il thread è unico, le richieste sono gestite in modo sequenziale ed eventualmente
accodate.
Un thread per client: viene associato un thread alla connessione con il client; le successive richieste del client sono a carico di questo thread.
Un thread per request: esiste un pool di thread utilizzati in modo concorrente per rispondere
alle richieste dei client.
Esisteranno comunque situazioni in cui i differenti thread dovranno accedere a una risorsa condivisa (p.e.: connessione a DB, file di log, …); in questi casi l’accesso alla risorsa andrà opportunamente sincronizzato.
Come si vedrà più avanti, con CORBA è possibile specificare le politiche di gestione dei thread
mediante l’utilizzo degli object adapter. Nella pratica, questi aspetti vengono spesso gestiti applicativamente con l’adozione di opportuni pattern di programmazione.
32
Questo permette di realizzare soluzioni complesse del tutto indipendenti dall’implementazione e
dalla tecnologia. Tipicamente queste soluzioni sono attuate con l’adozione del pattern Factory.
Il pattern Factory
Quando si istanzia un oggetto è necessario fare un riferimento diretto ed esplicito a una precisa
classe, fornendo gli eventuali parametri richiesti dal costruttore. Ciò vincola implicitamente
l’oggetto utilizzatore all’oggetto utilizzato. Se questo può essere accettabile nella maggior parte
dei casi, talvolta è un limite troppo forte.
Figura 7 — Il pattern Factory.
In questi casi può essere molto utile incapsulare in una classe specializzata tutte le valutazioni
relative alla creazione dell’oggetto che si intende utilizzare. Questa soluzione è il più noto tra i
creational patterns: il pattern Factory (anche detto Factory Method o Virtual Constructor).
Factory letteralmente vuol dire “fabbrica” ed è proprio questo il senso dell’oggetto Factory:
fabbricare sulla base di alcune valutazioni un determinato oggetto. Più formalmente, facendo riferimento anche alla fig. 7, un oggetto Factory fabbrica oggetti ConcreteProduct appartenenti a una determinata famiglia specificata dalla sua interfaccia (o classe astratta) Product.
Un client non crea mai in modo diretto un’istanza del Product (in un contesto remoto probabilmente non ne conoscerà nemmeno la classe), ma ne ottiene un’istanza valida attraverso
l’invocazione del FactoryMethod sull’oggetto Factory.
In questo modo è possibile sostituire in ogni momento l’implementazione ConcreteProductA
con un’implementazione omologa ConcreteProductB senza che un eventuale client se ne accorga.
In realtà i vantaggi elencati in precedenza sono già impliciti in uno scenario CORBA (il disaccoppiamento è garantito dalla funzionalità Proxy dello stub). Nella programmazione distribuita il
pattern Factory ha però altri vantaggi, in particolare consente di implementare meccanismi di
load-balancing e fault-tolerance.
33
Poiché è la Factory a determinare la creazione del Product, essa potrà:
— istanziare un oggetto per ogni client.
— applicare un round-robin tra le differenti istanze già create, ottenendo un semplice loadbalancing.
— restituire differenti tipologie di oggetti appartenenti alla famiglia Product sulla base di valutazioni legate all’identità del client.
— implementare un semplice fault-tolerance, escludendo dal pool di oggetti quelli non più
funzionanti o non più raggiungibili via rete.
Un esempio di Factory
Per sperimentare un tipico caso di applicazione del pattern Factory si realizzi il classico “carrello
della spesa”. Per l’esempio saranno implementate la classe carrello (ShoppingCart) e una sua
Factory (ShoppingCartFactory). La prima fornirà una funzionalità di acquisto e una di restituzione del contenuto, la seconda sarà dotata del solo metodo getShoppingCart.
Si definisce l’IDL
// IDL
module shopping {
struct Book {
string Author;
string Title;
};
typedef sequence <Book> BookList;
interface ShoppingCart {
void addBook(in Book book);
BookList getBookList();
};
interface ShoppingCartFactory {
ShoppingCart getShoppingCart(in string userID);
};
};
Sono definite come interface sia la Factory (con il Factory Method getShoppingCart), sia il
carrello vero e proprio. La Factory può essere considerata una classe di infrastruttura, mentre
tutti i metodi di business sono implementati nello ShoppingCart.
La classe ShoppingCartImpl implementa i due semplici metodi di business e non presenta
nulla di nuovo rispetto a quanto visto in precedenza.
package server;
import shopping.*;
import java.util.Vector;
34
public class ShoppingCartImpl extends _ShoppingCartImplBase {
Vector v = new Vector();
public ShoppingCartImpl() {
super();
}
// Aggiunge un libro al carrello
public void addBook(Book book) {
v.add(book);
}
// Restituisce l'elenco dei libri acquistati
public Book[] getBookList() {
Book[] books = new Book[v.size()];
for (int i=0; i<v.size(); i++)
books[i] = (Book) v.elementAt(i);
return books;
}
}
Più interessante è l’oggetto Factory che ha il compito di generare le istanze di ShoppingCartImpl da assegnare ai client. Nel metodo getShoppingCart viene stabilita la politica di creazione e restituzione delle istanze di carrello, nel caso in esame la decisione è ovvia in quanto il
carrello ha evidentemente un rapporto uno a uno con i client.
Per memorizzare le varie istanze viene utilizzato un oggetto di tipo Dictionary. Alla prima
connessione dell’utente la Factory creerà il carrello e lo “registrerà” sull’ORB. La Factory
può ottenere un riferimento valido all’ORB invocando il metodo _orb() fornito da
org.omg.CORBA.Object.
Ecco la classe Factory.
package server;
import shopping.*;
import java.util.*;
public class ShoppingCartFactoryImpl extends _ShoppingCartFactoryImplBase {
private Dictionary allCarts = new Hashtable();
public ShoppingCartFactoryImpl() {
super();
}
public synchronized ShoppingCart getShoppingCart(String userID) {
// Cerca il carrello assegnato allo userID...
shopping.ShoppingCart cart
= (shopping.ShoppingCart) allCarts.get(userID);
// ...se non lo trova...
if(cart == null) {
35
// Crea un nuovo carrello...
cart = new ShoppingCartImpl();
// ...e lo attiva sull'ORB
_orb().connect(cart);
System.out.println("Created " + userID + "'s cart: " + cart);
// Salva nel dictionary associandolo allo userID
allCarts.put(userID, cart);
}
// Restituisce il carrello
return cart;
}
}
È da notare che la Factory sarà utilizzata in concorrenza da più client, di conseguenza sarà
opportuno sincronizzare il metodo getShoppingCart per ottenere un’esecuzione consistente.
Per quanto detto in precedenza, un client otterrà un oggetto remoto di tipo ShoppingCart interagendo con la Factory. Pertanto, l’unico oggetto registrato sul Naming Service sarà l’oggetto
Factory. La registrazione sarà effettuata dalla classe server con il nome ShoppingCartFactory con le stesse modalità viste nell’esempio precedente (il codice non viene qui mostrato).
Dopo aver ottenuto il reference allo ShoppingCart assegnato, il client potrà operare direttamente su questo senza interagire ulteriormente con la Factory.
package client;
import
import
import
import
shopping.*;
java.io.*;
org.omg.CosNaming.*;
org.omg.CORBA.*;
public class ShoppingCartClient {
public static void main(String args[]) {
if (args.length != 3) {
System.err.println("Uso corretto: java ShoppingCartClient
userId Autore Titolo");
return;
}
try{
// Crea e inizializza l'ORB
ORB orb = ORB.init(args, null);
// Root naming context
org.omg.CORBA.Object objRef
= orb.resolve_initial_references("NameService");
NamingContext ncRef = NamingContextHelper.narrow(objRef);
// Utilizzo il Naming per ottenere il
// riferimento all'oggetto Factory
NameComponent nc
= new NameComponent("ShoppingCartFactory", " ");
36
NameComponent path[] = {nc};
ShoppingCartFactory factory
= ShoppingCartFactoryHelper.narrow(ncRef.resolve(path));
// Ottengo dalla Factory un'oggetto ShoppingCart
ShoppingCart cart = factory.getShoppingCart(args[0]);
// Aggiungo un libro
cart.addBook(new Book(args[1],args[2]));
// Ottengo la lista dei libri e la stampo
Book[] list = cart.getBookList();
for(int i=0; i<list.length; i++)
System.out.println("Autore " + list[i].Author
+ " - Titolo " + list[i].Title);
} catch (Exception e) {
e.printStackTrace();
}
}
}
I passi necessari per l’esecuzione sono gli stessi visti nell’esempio precedente. Al client vanno
“passati” lo userId, l’autore e il titolo del libro da aggiungere al carrello.
L’adozione di un pattern è una scelta del tutto indipendente dalla tecnologia e quindi adattabile a
qualunque altro scenario (ad esempio RMI). Un design come quello visto rende l’architettura più
robusta e adattabile, consentendo di modificare e configurare il comportamento anche a start up
avvenuto senza alcun impatto sui client. In ottica Enterprise la resistenza ai cambiamenti è di
massimo interesse.
Come già accennato, le politiche relative alla gestione delle istanze sono configurabili anche utilizzando gli Object Adapter.
Utilizzo degli Object Adapter
L’Object Adapter è un componente molto importante dell’architettura CORBA. Uno dei suoi
compiti è quello di associare un riferimento a una specifica implementazione nel momento in cui
un oggetto è invocato. Quando un client effettua un’invocazione l’adapter collabora con l’ORB
e con l’implementazione per fornire il servizio richiesto.
Se un client chiama un oggetto che non è effettivamente in memoria, l’adapter si occupa anche
di attivare l’oggetto affinché questo possa rispondere all’invocazione. In molte implementazioni
l’adapter può occuparsi anche di disattivare un oggetto non utilizzato da lungo tempo. Dal punto di vista del client l’implementazione è sempre disponibile e caricata in memoria.
Formalmente le specifiche CORBA individuano per l’adapter sei funzionalità chiave:
— Generazione e interpretazione degli object references.
— Invocazione dei metodi attraverso lo skeleton.
37
— Sicurezza delle interazioni.
— Autenticazione alla chiamata (utilizzando un’entità CORBA detta Principal).
— Attivazione e disattivazione degli oggetti.
— Mapping tra reference e corrispondente implementazione.
— Registrazione dell’implementazione.
Queste funzionalità sono associate al componente logico adapter e nella pratica sono compiute
in collaborazione con il core dell’ORB ed eventualmente con altri componenti; alcune funzionalità sono delegate integralmente all’ORB e allo skeleton. L’adapter è comunque coinvolto in ogni
invocazione di metodo.
Le funzionalità dell’adapter rendono disponibile via ORB l’implementazione del CORBA object
e supportano l’ORB nella gestione del runtime environment dell’oggetto. Dal punto di vista del
client l’adapter è il componente che garantisce che le sue richieste siano recapitate a un oggetto
attivo in grado di soddisfare la richiesta.
Il meccanismo CORBA opera in modo tale da consentire l’utilizzo contemporaneo di più tipi di
adapter con differenti comportamenti (nelle specifiche gli adapter vengono definiti pluggable). A
livello di design, l’idea di individuare un’altra entità come l’adapter nasce dalla necessità di modellare in maniera flessibile alcuni aspetti senza estendere l’interfaccia dell’ORB, ma individuando nuovi moduli pluggable.
Il primo tipo di adapter introdotto da OMG è il Basic Object Adapter o BOA. Poiché le specifiche
del BOA erano lacunose (non definivano ad esempio i meccanismi di attivazione e disattivazione degli oggetti), i vari venditori finirono per realizzarne implementazioni proprietarie largamente incompatibili tra loro che minavano di fatto la portabilità lato server di CORBA. Per questa
ragione OMG decise di abbandonare il BOA specificando un nuovo tipo di adapter, il Portable
Object Adapter o POA.
Nel caso in cui l’ORB scelto supporti il POA sarà sicuramente opportuno utilizzarlo. Esistono
tuttavia molti ORB che attualmente non forniscono un’implementazione del POA. Per questa
ragione sarà esaminato anche il BOA nonostante il suo utilizzo sia formalmente deprecato.
Per gli esempi di questa sezione non sarà possibile utilizzare l’implementazione Sun che non
fornisce BOA e POA.
Basic Object Adapter (BOA)
Le specifiche CORBA elencano come compiti primari del BOA la creazione/distruzione degli
object reference e il reperimento delle informazioni a questi correlate. Nelle varie implementazioni il BOA, per compiere le sue attività, può accedere al componente proprietario Implementation Repository.
38
Come si è detto in precedenza, BOA va pensato come un’entità logica e in effetti alcuni dei suoi
compiti sono svolti in cooperazione con altri componenti (ad esempio la creazione e la distruzione di object reference sono a carico dello skeleton). Questo ha un impatto sulla sua implementazione che solitamente suddivide i suoi compiti tra il processo ORB, il codice generato dal
compilatore IDL e l’effettivo BOA.
Comunque il BOA fornisce l’interfaccia con i metodi necessari a registrare/deregistrare gli oggetti e ad avvertire l’ORB che l’oggetto è effettivamente pronto a rispondere alle invocazioni.
Si è già visto negli esempi precedenti il concetto di server: il server è un’entità eseguibile separata
che attiva l’oggetto e gli fornisce un contesto di esecuzione. Anche il BOA per attivare un oggetto si appoggia a un server.
Il server può essere attivato on demand dal BOA (utilizzando informazioni contenute
nell’Implementation Repository) oppure da qualche altra entità (ad esempio uno shell script). In
ogni caso il server attiverà l’implementazione chiamando il metodo obj_is_ready oppure il
metodo impl_is_ready definiti nel seguente modo
// PIDL
module CORBA {
interface BOA {
void impl_is_ready (in ImplementationDef impl);
void deactivate_impl (in ImplementationDef impl);
void obj_is_ready (
in Object obj, in ImplementationDef impl
);
void deactivate_obj (in Object obj);
//...altri metodi di generazione references e access control
};
};
In quasi tutti gli ORB, obj_is_ready è il metodo di registrazione del singolo oggetto
all’interno di un server e stabilisce un’associazione tra un’istanza e un’entità nell’Implementation
Repository.
Il metodo impl_is_ready è comunemente implementato come un loop infinito che attende le
request del client; il ciclo non cessa fino a quando non viene invocato il deactivate_impl.
Esistono molti modi di combinare un server process con l’attivazione di oggetti (un server registra un oggetto, un server registra n oggetti, …). Le specifiche CORBA individuano quattro differenti politiche di attivazione.
Shared Server: il processo server inizializza più oggetti invocando per ognuno obj_is_ready.
Al termine di queste inizializzazioni il server notifica al BOA, con impl_is_ready, la sua disponibilità e rimane attivo fino all’invocazione di deactivate_impl. Gli oggetti possono essere
singolarmente disattivati con deactivate_obj. La disattivazione è quasi sempre automatizzata
dal distruttore dello skeleton.
39
Unshared Server: ogni oggetto viene associato a un processo server differente.
L’inizializzazione avviene comunque con le due chiamate obj_is_ready e impl_is_ready.
Server-per-method: un nuovo processo viene creato ad ogni invocazione. Il processo termina
al terminare dell’invocazione e, poiché ogni invocazione implica un nuovo processo, non è necessario inviare una notifica al BOA (alcuni ORB richiedono comunque l’invocazione di
impl_is_ready).
Persistent Server: tipicamente è un processo avviato mediante qualche meccanismo esterno al
BOA (shell script o avvio utente) e va registrato mediante impl_is_ready. Dopo la notifica al
BOA si comporta esattamente come uno shared server.
A differenza di quanto indicato nelle specifiche, la maggior parte delle implementazioni fornisce
un sottoinsieme delle activation policy. In generale l’utilizzo dei metodi legati al BOA è differente tra i vari ORB e genera quasi sempre problemi di portabilità per quanto concerne l’attivazione
delle implementazioni.
BOA in pratica
Si provi ora a riscrivere l’applicazione ShoppingCart utilizzando l’implementazione BOA fornita da VisiBroker. Per quanto detto sugli adapter si dovrà intervenire sulle classi coinvolte
nell’attivazione e nell’invocazione degli oggetti CORBA, andranno quindi modificate le seguenti
classi: ShoppingCartServer, ShoppingCartFactoryImpl e ShoppingCartClient.
Si utilizzi il modello di server persistent, una classe Java con metodo main lanciata da linea di
comando o da script. La registrazione dell’oggetto Factory sarà effettuata via BOA.
Il BOA è un cosiddetto pseudo-object ed è possibile ottenere un riferimento valido ad esso mediante l’invocazione di un metodo dell’ORB: BOA_init. Nell’implementazione VisiBroker esistono due differenti metodi BOA_init che permettono di ricevere un BOA inizializzato con
differenti politiche di gestione dei thread (thread pooling o per session) e di trattamento delle
comunicazioni (utilizzo di Secure Socket Layer o no).
Invocando il metodo BOA_init senza parametri si otterrà un BOA con le politiche di default
(thread pooling senza SSL). Il codice completo della classe server è
package server;
import shopping.*;
public class ShoppingCartServer {
public static void main(String[] args) {
// Inizializza l'ORB
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args,null);
// Crea l'oggetto Factory
ShoppingCartFactoryImpl factory
= new ShoppingCartFactoryImpl("ShoppingCartFactory");
40
// Inizializza il BOA
// N.B. Utilizzo classi proprietarie
com.inprise.vbroker.CORBA.BOA boa
= ((com.inprise.vbroker.CORBA.ORB)orb).BOA_init();
// Esporta l'oggetto factory
boa.obj_is_ready(factory);
System.out.println(factory + " is ready.");
// Attende le requests
boa.impl_is_ready();
}
}
Si noti che istanziando l’oggetto Factory si è fornita al costruttore la stringa "ShoppingCartIn VisiBroker, specificando un object name quando si istanzia l’oggetto, si ottiene un
riferimento di tipo persistent. Il costruttore dell’oggetto dovrà comunque notificare il nome alla
superclasse.
Factory".
public ShoppingCartFactoryImpl(String name) {
super(name);
}
L’associazione tra un reference persistent e il nome viene registrata in un tool proprietario denominato OSAgent o ORB Smart Agent.
Pur essendo uno strumento proprietario OSAgent è molto utilizzato nella pratica. Fornisce una
versione semplificata di naming service (con VisiBroker viene fornita anche un’implementazione
standard del servizio) e implementa alcuni meccanismi proprietari di fault-tolerance e loadbalancing. Per una trattazione completa dell’OSAgent si faccia riferimento alla documentazione
VisiBroker.
Un riferimento persistent rimane vivo nell’OSAgent anche al termine del processo server (può
essere verificato utilizzando il tool osfind). Un riferimento transient è invece rigidamente associato al ciclo di vita del server e non può avvalersi dei meccanismi di load-balancing e faulttolerance forniti dall’OSAgent.
Nell’esempio, l’unico oggetto con riferimento persistent è la Factory. I vari carrelli hanno riferimenti transient, sono registrati solo dalla chiamata obj_is_ready della Factory e sono
quindi accessibili solo tramite questa.
Anche utilizzando il BOA, l’oggetto CORBA deve specializzare la classe <Interfaccia>ImlBase. Ecco il codice completo della Factory.
package server;
import shopping.*;
import java.util.*;
public class ShoppingCartFactoryImpl extends _ShoppingCartFactoryImplBase {
private Dictionary allCarts = new Hashtable();
41
// N.B. Registro nell'OSAgent
public ShoppingCartFactoryImpl(String name) {
super(name);
}
public synchronized ShoppingCart getShoppingCart(String userID) {
// Cerca il carrello assegnato allo userID...
shopping.ShoppingCart cart
= (shopping.ShoppingCart) allCarts.get(userID);
// se non lo trova...
if(cart == null) {
// crea un nuovo carrello...
cart = new ShoppingCartImpl();
// Rende l'oggetto disponibile sull'ORB
// N.B. _boa() è fornito dalla classe
// com.inprise.vbroker.CORBA.Object
_boa().obj_is_ready(cart);
System.out.println("Created " + userID "'s cart: " + cart);
// Salva il carrello nel dictionary associandolo allo userID
allCarts.put(userID, cart);
}
// Restituisce il carrello
return cart;
}
}
Il client può ottenere un riferimento alla Factory invocando il metodo bind fornito
dall’Helper che si preoccupa anche di eseguire l’opportuno narrow.
Ecco il codice completo della classe client.
package client;
import shopping.*;
import org.omg.CORBA.*;
public class ShoppingCartClient {
public static void main(String args[]) {
if (args.length != 3) {
System.err.println("Uso corretto: java ShoppingCartClient
userId Autore Titolo");
return;
}
// Crea e inizializza l'ORB
ORB orb = ORB.init(args, null);
// Localizza l'oggetto Factory
ShoppingCartFactory factory
= ShoppingCartFactoryHelper.bind(orb, "ShoppingCartFactory");
// Ottengo dalla Factory un oggetto ShoppingCart
ShoppingCart cart = factory.getShoppingCart(args[0]);
42
// Aggiungo un libro
cart.addBook(new Book(args[1], args[2]));
// Ottengo la lista dei libri e la stampo
Book[] list = cart.getBookList();
for(int i=0; i<list.length; i++)
System.out.println("Autore " + list[i].Author
+ " - Titolo " + list[i].Title);
}
}
Prima di avviare il server si attivi l’OSAgent (nel caso si lavori in ambiente distribuito è necessario attivare l’OSAgent sia sulla macchina client che sulla macchina server). Fatto questo, per
l’esecuzione dell’esempio si compiano i soliti passi. I riferimenti persistent registrati
nell’OSAgent sono controllabili usando il tool osfind.
Attivazione automatica con VisiBroker
Si è detto in precedenza che per l’attivazione automatica di un oggetto, l’adapter attiva i necessari processi server utilizzando le informazioni contenute nell’Implementation Repository. In VisiBroker questo meccanismo è fornito dall’Object Activation Daemon (OAD).
L’OAD è un repository che mantiene le informazioni sulle classi che un server supporta, sui loro
ID e sulle modalità con cui è necessario attivarle. Le informazioni presenti sull’OAD devono
essere registrate ed esistono più modalità di registrazione.
Poiché l’OAD è un oggetto CORBA, è possibile costruire un’applicazione che si preoccupi di
registrare/deregistrare le varie implementazioni utilizzando i metodi definiti dalla sua interfaccia
IDL (per maggior dettagli vedere la documentazione VisiBroker).
Nella pratica è più comune registrare le implementazioni da linea di comando, tipicamente con
script di registrazione di più oggetti (magari nella sequenza di boot di una macchina).
Comunque, indipendentemente dalle modalità di registrazione, non si dovrà scrivere codice differente rispetto a quanto fatto in precedenza. Si provi dunque a utilizzare l’attivazione mediante
OAD sull’esempio precedentemente scritto per il BOA.
Attivati gli OSAgent si avvii il processo OAD con il comando
oad -VBJprop JDKrenameBug
con alcune versioni di VisiBroker non sarà necessario utilizzare il flag.
L’OAD va attivato solo sulla macchina su cui si vuole eseguire l’oggetto servant. A questo punto
si proceda alla registrazione dell’implementazione sull’OAD.
Il tool da utilizzare è oadutil che permette di registrare un’interfaccia CORBA (flag -i),
con un preciso object name (flag -o), indicando il server che sarà utilizzato per l’attivazione
(flag -java). È anche possibile specificare l’activation policy con il flag -p.
43
Si esegua quindi il comando
oadutil reg -i shopping::ShoppingCartFactory –o ShoppingCartFactory java server.ShoppingCartServer -p shared
L’implementazione sarà a questo punto registrata, ma il server non sarà ancora attivo; è possibile
controllare il contenuto dell’OAD con il comando
oadutil list –full
che fornirà un output del tipo
oadutil list: located 1 record(s)
Implementation #1:
------------------repository_id
=
object_name
=
reference data
=
path_name
=
activation_policy =
args
=
env
=
IDL:shopping/ShoppingCartFactory:1.0
ShoppingCartFactory
vbj
SHARED_SERVER
(length=1)[server.ShoppingCartServer; ]
NONE
Nothing active for this implementation
Il server sarà attivato alla prima invocazione del client e, poiché si è specificata una politica shared, sarà condiviso anche dai client che successivamente effettueranno una request. Per verificarlo si avvii con le solite modalità l’applicazione client. Lo standard output del processo OAD dovrebbe notificare l’effettiva attivazione del server, in ogni caso è possibile verificare il contenuto
dell’OAD con il comando oadutil list visto in precedenza.
In un contesto reale l’attivazione con queste modalità semplifica decisamente le attività di gestione e manutenzione risultando preferibile rispetto all’attivazione manuale dei server.
Nel caso in cui si debbano attivare molti oggetti, può essere necessario attivare/disattivare in
maniera mirata i vari servant; un meccanismo del genere è realizzabile fornendo un cosiddetto
service activator.
In linea generale un service activator raggruppa n oggetti per i quali è in grado di determinare
l’attivazione/disattivazione ad ogni request. Per definire le operazioni di activate/deactivate,
l’Activator dovrà implementare l’interfaccia com.visigenic.vbroker.extension.Activator.
Per una trattazione dell’Activator si rimanda alla documentazione VisiBroker.
Per l’utilizzo di OAD valgono le stesse considerazioni viste in precedenza per OSAgent: fornisce un più semplice utilizzo e notevoli possibilità, ma limita la portabilità lato server. Per una
completa portabilità lato server è opportuno utilizzare POA.
44
Portable Object Adapter (POA)
Il POA entra a far parte delle specifiche CORBA nel 1997 e va a sostituire integralmente a livello funzionale le precedenti specifiche BOA.
La scelta di sostituire integralmente le API BOA è legata all’impossibilità di coniugare i complessi sviluppi presi dalle varie implementazioni proprietarie. Poiché l’ORB è pensato per supportare
un numero arbitrario di adapter, BOA e POA possono comunque coesistere.
Lo sviluppo del POA prende l’avvio e si basa sulle molteplici esperienze derivate dalle varie
implementazioni del BOA (spesso si dice che POA è semplicemente una versione corretta di
BOA), è quindi chiaro che molteplici sono i concetti comuni alle due tipologie di adapter.
Anche per POA valgono dunque le distinzioni effettuate sull’attivazione di implementazioni,
sulle tipologie di server e la distinzione tra oggetti persistent o transient.
Per ogni implementazione è definibile un servant manager che, invocato dal POA, crea, attiva o
disattiva i vari servant on demand. Il meccanismo dei servant manager aiuta il POA nella gestione degli oggetti server-side. Quasi sempre gli ORB forniscono dei servant manager di default
che implementano politiche definite.
È comunque possibile definire direttamente il set di politiche applicate al server senza utilizzare
un servant manager, ma operando direttamente sul POA. Nel caso in cui si utilizzi un servant
manager, sarà suo il compito di associare la request a un preciso servant, attivandolo o creandolo
se necessario.
Un servant manager implementa una delle due interfacce di callback ServantActivator e
ServantLocator. In generale l’Activator si riferisce a oggetti di tipo persistent, mentre il Locator si riferisce a oggetti di tipo transient.
Indipendentemente da quale interfaccia si utilizzi, le operazioni da definire sono due, una per
reperire e restituire il servant, l’altra per disattivarlo. Nel caso di ServantActivator le due operazioni di cui sopra sono incarnate ed etherealize, mentre nel caso di ServantLocator sono preinvoke e postinvoke.
Il POA mantiene una mappa (Active Object Map) dei servant attivi in ogni istante. All’interno
della mappa i servant sono associati a uno o più Object Id. Un riferimento a un oggetto sul lato
client incapsula l’Object Id e il riferimento al POA ed è utilizzato sul lato server da ORB, POA e
servant manager per recapitare la request a un preciso servant.
Non esiste una forma standard dell’Object Id che può essere generato dal POA oppure essere
assegnato dall’implementazione. In ogni caso l’Object Id deve essere unico nel namespace e
quindi nel POA su cui è mantenuto.
La struttura dei POA è ad albero a partire da un RootPOA. Il RootPOA è sempre disponibile e
possiede politiche di default. A partire da questo è possibile generare una gerarchia di nodi POA
child con politiche differenti. Sul RootPOA sono mantenuti solo riferimenti transient ed è per
questo che nella pratica gli oggetti si installano quasi sempre su child POA creati opportunamente.
45
Figura 8 — Funzionamento POA.
Un Child POA può essere creato invocando il metodo create_POA sul suo nodo padre. Per
definire le politiche del POA creato bisogna invocare il metodo di creazione fornendogli un oggetto di tipo Policy opportunamente inizializzato. Non è possibile modificare successivamente le politiche di un nodo POA.
Ecco la definizione dei metodi che gestiscono il ciclo di vita di un POA.
// IDL
module PortableServer {
//...
interface POA {
//...
// POA creation and destruction
POA create_POA(in string adapter_name,
in POAManager a_POAManager,
in CORBA::PolicyList policies) raises (AdapterAlreadyExists,
InvalidPolicy);
POA find_POA(in string adapter_name,
in boolean activate_it) raises (AdapterNonExistent);
void destroy(in boolean etherealize_objects,
in boolean wait_for_completion);
};
};
L’oggetto Policy va a coprire vari aspetti del runtime environment di un servant associato a
un preciso POA. Le specifiche CORBA individuano sette differenti aspetti (in corsivo sono indicati i default):
46
Thread: specifica le modalità di trattamento dei thread ovvero singolo thread (SINGLE_THREAD_MODEL) o multithread (ORB_CTRL_MODEL).
Lifespan: specifica il modello di persistenza (PERSISTENT o TRANSIENT).
Object Id Uniqueness: specifica se l’Id di un servant deve essere unico (UNIQUE_ID) o può essere multiplo (MULTIPLE_ID).
Id Assignment: specifica se l’Id deve essere assegnato dall’applicazione (USER_ID) o dal POA
(SYSTEM_ID).
Servant Retention: specifica se il POA mantiene i servant nell’Active Object Map (RETAIN) o si
affida a un servant manager (NON_RETAIN).
Activation: specifica se il POA supporta l’attivazione implicita dei servant (IMo NO_ IMPLICIT_ACTIVATION).
PLICIT_ACTIVATION
Request Processing: specifica come vengono processate le request; usando l’Active Object
Map (USE_ACTIVE_OBJECT_MAP_ONLY, da usare con RETAIN) o un servant manager (USE_DEFAULT_SERVANT o USE_SERVANT_MANAGER, da usare con NON_RETAIN).
Per ognuna di queste categorie il POA offre una Factory del tipo create_XXX_policy
(create_thread_policy, create_lifespan_policy, …); la costruzione di un oggetto di tipo Policy avviene utilizzando la Factory opportuna.
La scelta della combinazione da adottare è legata alla tipologia di applicazione che si sta realizzando. Ad esempio, una combinazione di RETAIN e USE_ACTIVE_OBJECT_MAP_ONLY (il default) può essere accettabile per applicazioni che gestiscono un numero finito di oggetti attivati all’avvio (come un Application Server che fornisca servizi continuativamente), ma è una
soluzione troppo limitata per un’applicazione che gestisca un gran numero di oggetti. Per applicazioni di questo tipo sono più opportune soluzioni RETAIN che utilizzino servant
manager. USE_SERVANT_MANAGER è più indicato per un gran numero di oggetti persistent,
mentre USE_DEFAULT_SERVANT per un gran numero di oggetti transient.
Esistono tre differenti tipologie di attivazione di un oggetto mediante POA: attivazione esplicita,
attivazione on demand utilizzando un servant manager, e attivazione implicita.
L’attivazione esplicita avviene con l’invocazione di alcuni metodi forniti dal POA.
// IDL
module PortableServer {
//...
interface POA {
//...
// object activation and deactivation
47
ObjectId activate_object(in Servant p_servant) raises (ServantAlreadyActive,
WrongPolicy);
void activate_object_with_id(in ObjectId id,
in Servant p_servant)
raises (ServantAlreadyActive,
ObjectAlreadyActive, WrongPolicy);
void deactivate_object(in ObjectId oid) raises (ObjectNotActive,
WrongPolicy);
};
};
Nel caso in cui si usi la registrazione esplicita, il server crea tutti i servant e li registra con uno dei
due metodi activate_object.
Nel caso di attivazione on demand, il server si limita a informare il POA su quale servant
manager utilizzare per l’attivazione degli oggetti. I metodi per la gestione dei servant manager
sono
// IDL
module PortableServer {
//...
interface POA {
//...
// Servant Manager registration
ServantManager get_servant_manager() raises (WrongPolicy);
void set_servant_manager(in ServantManager imgr) raises (WrongPolicy);
};
};
Si ha un’attivazione implicita effettuando su di un servant inattivo, senza Object Id, operazioni
che implichino la presenza di un Object Id nell’Active Map. È possibile solo per la combinazione IMPLICIT_ACTIVATION, SYSTEM_ID, RETAIN. Le operazioni che generano un’attivazione
implicita sono
// IDL
module PortableServer {
//...
interface POA {
//...
// Identity mapping operations
ObjectId servant_to_id(in Servant p_servant) raises (ServantNotActive,
WrongPolicy);
48
Object servant_to_reference(in Servant p_servant) raises (ServantNotActive,
WrongPolicy);
};
};
Anche il POA può essere attivato e disattivato; queste operazioni possono essere effettuate utilizzando il POAManager che è un oggetto associato a uno o più POA. Il POAManager permette
anche di bloccare e scartare le request in arrivo.
// IDL
module PortableServer {
//...
// POAManager interface
interface POAManager {
exception AdapterInactive {};
enum State {HOLDING, ACTIVE, DISCARDING, INACTIVE};
void activate() raises(AdapterInactive);
void hold_requests(in boolean wait_for_completion) raises(AdapterInactive);
void discard_requests(in boolean wait_for_completion) raises(AdapterInactive);
void deactivate(in boolean etherealize_objects,
in boolean wait_for_completion) raises(AdapterInactive);
State get_state();
};
};
POA in pratica
L’utilizzo di POA permette di configurare l’environment e il comportamento del servant in maniera indipendente dall’ORB. Sarà presentato il solito esempio dello Shopping Cart avendo come punto di riferimento VisiBroker. L’esempio sarà comunque utilizzabile con qualunque altro
ORB dotato di POA.
Il modo più semplice di utilizzare POA è quello di adottare l’attivazione esplicita senza definire
servant manager. Sarà quindi realizzato un server che attivi il servant ShoppingCartFactory
in modo persistent. La Factory sarà un servizio sempre disponibile, mentre i singoli carrelli, come già nella versione BOA, saranno oggetti transient. Il primo passo da compiere è quello di ottenere un riferimento al RootPOA.
POA rootPOA
= POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
Fatto questo è possibile creare un POA con le necessarie politiche. Non è possibile utilizzare direttamente il RootPOA in quanto non supporta la modalità persistent.
49
Un POA di default utilizza modalità multithread, riferimenti transient, Active Object Map.
Nell’esempio è quindi sufficiente modificare la proprietà Lifespan da TRANSIENT a PERSISTENT e, con queste politiche, creare il nuovo nodo POA assegnandogli un nome identificativo.
org.omg.CORBA.Policy[] policies = {
rootPOA.create_lifespan_policy(LifespanPolicyValue.PERSISTENT)
};
POA myPOA = rootPOA.create_POA("shopping_cart_poa",
rootPOA.the_POAManager(), policies);
A questo punto è possibile istanziare il servant e attivarlo sul POA. Poiché l’Id deve essere conosciuto dal client per l’invocazione, in uno scenario semplice è conveniente definirlo esplicitamente nel server.
byte[] factoryId = "ShoppingCartFactory".getBytes();
myPOA.activate_object_with_id(factoryId, factory);
Il servant non sarà in condizione di rispondere fino all’attivazione del POA che lo ospita.
L’attivazione è effettuata utilizzando il POAManager.
rootPOA.the_POAManager().activate();
Il ciclo di attesa infinita del server può essere implementato con un metodo dell’ORB (non più
impl_is_ready).
orb.run();
Ecco il codice completo della classe server.
package server;
import shopping.*;
import org.omg.PortableServer.*;
public class ShoppingCartServer {
public static void main(String[] args) {
try {
// Inizializza l'ORB.
org.omg.CORBA.ORB orb = org.omg.CORBA.ORB.init(args,null);
// Prende il reference al the root POA
POA rootPOA
= POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
// Crea le policies per il persistent POA
org.omg.CORBA.Policy[] policies = {
rootPOA.create_lifespan_policy(LifespanPolicyValue.PERSISTENT)
};
// Crea myPOA con le date policies
POA myPOA = rootPOA.create_POA("shopping_cart_poa",
rootPOA.the_POAManager(),
policies);
50
// Crea l'oggetto Factory
ShoppingCartFactoryImpl factory = new ShoppingCartFactoryImpl();
// Stabilsco l'ID del servant
byte[] factoryId = "ShoppingCartFactory".getBytes();
// Attiva il servant su myPOA con il dato Id
myPOA.activate_object_with_id(factoryId, factory);
// Attiva il POA
rootPOA.the_POAManager().activate();
System.out.println(myPOA.servant_to_reference(factory) + " is ready.");
// Si mette in attesa delle requests
orb.run();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
Utilizzando POA un servant non deve più specializzare _<NomeInterfaccia>ImplBase, bensì
La ShoppingCartFactoryImpl sarà quindi
<NomeInterfaccia>POA.
package server;
import org.omg.PortableServer.*;
import shopping.*;
import java.util.*;
public class ShoppingCartFactoryImpl extends ShoppingCartFactoryPOA {
private Dictionary allCarts = new Hashtable();
public synchronized ShoppingCart getShoppingCart(String userID) {
// Cerca il carrello assegnato allo userID...
shopping.ShoppingCart cart
= (shopping.ShoppingCart) allCarts.get(userID);
// se non lo trova...
if(cart == null) {
// crea un nuovo carrello...
ShoppingCartImpl cartServant = new ShoppingCartImpl();
try {
// Attiva l'oggetto sul default POA che
// è il root POA di questo servant
cart
= shopping.ShoppingCartHelper.narrow(
_default_POA().servant_to_reference(cartServant));
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Created " + userID + "'s cart: " + cart);
51
// Salva il carrello associandolo allo userID
allCarts.put(userID, cart);
}
// Restituisce il carrello
return cart;
}
}
Si noti che l’attivazione del singolo ShoppingCart è implicita. La generazione dell’Id e la sua
registrazione nell’Active Objec Map saranno in questo caso a carico del POA.
L’oggetto ShoppingCartImpl dovrà specializzare ShoppingCartPOA; il codice rimarrà inalterato rispetto all’esempio BOA.
Il client accederà alla Factory in modo molto simile a quanto visto nel BOA. Sul bind andrà specificato l’opportuno Id e il nome del POA su cui è registrato il servant.
byte[] factoryId = "ShoppingCartFactory".getBytes();
ShoppingCartFactory factory
= ShoppingCartFactoryHelper.bind (orb, "/shopping_cart_poa", factoryId);
Ecco il codice completo della classe client.
package client;
import shopping.*;
import org.omg.CORBA.*;
public class ShoppingCartClient {
public static void main(String args[]) {
if (args.length != 3) {
System.err.println("Uso corretto: java ShoppingCartClient
userId Autore Titolo");
return;
}
// Crea e inizializza l'ORB
ORB orb = ORB.init(args, null);
// ID del servant
byte[] factoryId = "ShoppingCartFactory".getBytes();
// Localizza l'oggetto Factory
// Devo usare il POA name e l'Id del servant
ShoppingCartFactory factory =
ShoppingCartFactoryHelper.bind(orb, "/shopping_cart_poa",
factoryId);
// Ottengo dalla Factory un oggetto ShoppingCart
ShoppingCart cart = factory.getShoppingCart(args[0]);
// Aggiungo un libro
cart.addBook(new Book(args[1], args[2]));
// Ottengo la lista dei libri e la stampo
52
Book[] list = cart.getBookList();
for(int i=0; i<list.length; i++)
System.out.println("Autore " + list[i].Author
+ " - Titolo " + list[i].Title);
}
}
Parametri per valore
Nel progettare applicazioni distribuite, tipicamente si ragiona suddividendo il dominio
dell’applicazione in layer. Il modello più celebre individua tre livelli: presentation, business logic
e data/resources. I differenti livelli comunicano attraverso un canale, ma non devono condividere alcun oggetto a livello d’implementazione. Anche per questa ragione middleware come RMI o
CORBA operano una netta separazione tra interfaccia e implementazione.
A titolo d’esempio, si immagini un client che si occupa solamente di presentation e un server
(magari su un’altra macchina) che gestisce la business logic. Le due applicazioni dovranno al più
condividere oggetti che incapsulino dati, ma non dovranno condividere alcun metodo (un metodo di presentation è necessario sul client, un metodo di business è necessario sul server). Per
questi scopi sono quindi sufficienti le strutture dati fornite da IDL (struct, enum, …).
Nonostante queste considerazioni, è spesso utile ricevere/restituire oggetti per valore. In molti
casi può essere comodo avere metodi utilizzabili localmente al client e al server.
Fino alle specifiche 2.3 non era possibile il passaggio per valore con oggetti CORBA; come si è
visto finora le interface sono sempre trattate per riferimento. A differenza di RMI, i metodi non
potevano quindi restituire/ricevere via serializzazione oggetti condivisi da client e server.
L’introduzione del concetto di valuetype ha ovviato a questa mancanza.
Data la recente introduzione del valuetype, alcuni ORB, tra cui quello del JDK 1.2, non supportano ancora questa specifica. Per questa ragione nelle sezioni successive si studieranno il
valuetype e alcuni approcci alternativi.
Una possibile soluzione
La prima soluzione è molto semplice e in realtà non è un vero passaggio per copia. L’idea è quella di avere una classe locale (sul server o sul client) che fasci la struttura definita nell’IDL. Questa
classe dovrà avere i metodi da utilizzare localmente e, per comodità, un costruttore che riceva in
input la struttura dati.
Si provi quindi a implementare una semplice (quanto fasulla) funzionalità di login.
// IDL
module authentication {
struct User {
string userId;
};
exception InvalidUserException{};
53
interface UserLogin {
User login(in string user,
in string pwd) raises (InvalidUserException);
};
};
Per semplicità si supponga di impostare, sul client e sul server, userId e password da linea di
comando (in un contesto reale i dati utente risiederebbero su repository quali basi dati, files o
directory server). Il server provvederà a impostare i valori di userId e password sul servant
UserLoginImpl login = new UserLoginImpl(args[0], args[1]);
Il server effettua le stesse procedure di attivazione e registrazione via Naming Service viste in
precedenza. Il codice completo della classe server non viene mostrato.
Ecco invece il codice completo della classe servant.
package server;
import authentication.*;
public class UserLoginImpl extends _UserLoginImplBase {
private String userId;
private String pwd;
public UserLoginImpl(String u, String p) {
super();
userId = u;
pwd = p;
}
// Metodo di Login
public User login(String u, String p) throws InvalidUserException {
if (userId.equals(u) && pwd.equals(p))
return new User(u);
else
throw new InvalidUserException();
}
}
È possibile ora definire il wrapper dello User client-side.
package client;
import authentication.*;
public class UserLocalWithMethods {
private User user;
public UserLocalWithMethods(User user) {
this.user = user;
}
54
// Metodo della classe locale che accede alla struct User
public String getUserId() {
return user.userId;
}
// Override del metodo toString
public String toString() {
return "#User : " + user.userId;
}
}
L’oggetto wrapper sarà creato incapsulando la classe User (che rappresenta la struct IDL).
Sull’oggetto sarà possibile invocare i metodi definiti localmente (nel caso in esame getUserId
e toString).
UserLogin uLogin
= UserLoginHelper.narrow(ncRef.resolve(path));
// Effettuo il login e creo l'oggetto wrapper
UserLocalWithMethods user
= new UserLocalWithMethods(uLogin.login(args[0], args[1]));
// Utilizzo il metodo della classe locale
System.out.println("Login UserId: " + user.getUserId());
// Utilizzo il metodo toString della classe locale
System.out.println(user);
Il client e il server non condivideranno l’implementazione di alcun metodo, ma si limiteranno a
condividere la rappresentazione della struttura IDL. La classe con i metodi andrà distribuita
semplicemente sul layer logicamente destinato alla sua esecuzione. Potranno esistere wrapper
differenti per layer differenti.
Questa soluzione, pur non essendo un effettivo passaggio per valore, rappresenta
l’implementazione formalmente più corretta e più vicina allo spirito originale CORBA.
Serializzazione
La seconda soluzione utilizza la serializzazione Java e quindi, non essendo portabile, non è molto in linea con lo spirito CORBA. Nel caso però in cui si affronti uno scenario che prevede Java
sui client e sui server è comunque una soluzione comoda, simile nell’approccio a RMI.
Si ridefinisca l’IDL vista in precedenza
module authentication {
typedef sequence <octet> User;
exception InvalidUserException{};
interface UserLogin {
User login(in string user,
in string pwd) raises (InvalidUserException);
};
};
55
In questo modo il tipo User, definito come sequence di octet, sarà effettivamente tradotto in
Java come array di byte. Il metodo login potrà quindi restituire qualunque oggetto serializzato.
L’oggetto condiviso da client e server dovrà essere serializzabile
package client;
import java.io.*;
public class User implements Serializable {
private String userId;
public User(String userId) {
this.userId = userId;
}
public String getUserId() {
return userId;
}
// Override del metodo toString
public String toString() {
return "#User : " + userId;
}
}
L’effettiva serializzazione sarà operata dal metodo login del servant modificato come di seguito
riportato.
public byte[] login(String u, String p) throws InvalidUserException {
if (userId.equals(u) && pwd.equals(p)) {
// Serializza un oggetto user in un array di byte
byte[] b = serializza(new client.User(u));
return b;
} else
throw new InvalidUserException();
}
Il metodo utilizzato per ottenere l’array di byte serializza un generico oggetto
public byte[] serializza(java.lang.Object obj) {
ByteArrayOutputStream bOut = null;
try {
bOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bOut);
out.writeObject(obj);
} catch(Exception e) {
e.printStackTrace();
}
56
return bOut.toByteArray();
}
Il client opererà in maniera speculare
UserLogin uLogin = UserLoginHelper.narrow(ncRef.resolve(path));
// Effettuo il login
byte[] b = uLogin.login(userId, pwd);
// Ottengo l'oggetto User serializzato
User user = (User) deserializza(b);
// Utilizzo il metodo della classe serializzata
System.out.println("Login UserId: " + user.getUserId());
// Utilizzo il metodo toString della classe serializzata
System.out.println(user);
Il metodo deserializza è definito come segue:
public java.lang.Object deserializza(byte[] b) {
java.lang.Object obj = null;
try {
ByteArrayInputStream bIn = new ByteArrayInputStream(b);
ObjectInputStream oIn = new ObjectInputStream(bIn);
obj = oIn.readObject();
} catch(Exception e) {
e.printStackTrace();
}
return obj;
}
Questa soluzione mina l’interoperabiltà con altri linguaggi e inoltre, trattando i parametri come
array di byte e non come tipi, diminuisce il livello espressivo di un’interfaccia.
Valuetype
Le soluzioni precedenti sono semplici approcci applicativi; le specifiche CORBA 2.3 hanno definito un approccio standard al passaggio di oggetti per valore. Questa parte di specifiche riveste
una grandissima importanza in quanto è uno degli elementi chiave di avvicinamento tra RMI e
CORBA. È uno dei meccanismi fondamentali per i tool che implementano RMI over IIOP (si
veda più avanti, per una discussione più approfondita).
L’idea chiave che sta alla base delle specifiche CORBA Object-by-Value (OBV) è quella di fornire
una sorta di serializzazione multipiattaforma.
La definizione di un oggetto serializzabile può essere suddivisa in stato e implementazione. La
componente stato è sostanzialmente riconducibile ai valori che hanno gli attributi ed è quindi
legata alla singola istanza (escludendo ovviamente attributi static), la seconda componente è
l’implementazione dei metodi ed è comune a tutte le istanze.
57
Anche in Java la serializzazione si limita a rendere persistente lo stato. In fase di deserializzazione l’oggetto viene ricostruito utilizzando la sua definizione (la classe) a partire dal suo stato.
Per la natura delle specifiche CORBA, la definizione di un meccanismo simile a quello descritto
comporta un’estensione del linguaggio IDL. La keyword valuetype consente di specificare un
nuovo tipo che utilizza il passaggio per valore.
Si modifichi l’IDL vista in precedenza definendo l’oggetto User come valuetype.
// IDL
module authentication {
valuetype User {
// Metodi locali
string getUserId();
// Stato
private string userId;
};
exception InvalidUserException{};
interface UserLogin {
User login(in string user,
in string pwd) raises (InvalidUserException);
};
};
La definizione mediante valuetype consente di specificare gli attributi con gli opportuni modificatori di accesso (private e public) e le signature dei metodi definite con le stesse modalità adottate nelle interface.
Compilando l’IDL si ottiene la seguente definizione del tipo User (l’esempio non è utilizzabile
con il JDK 1.2).
package authentication;
public abstract class User implements org.omg.CORBA.portable.StreamableValue {
protected java.lang.String userId;
abstract public java.lang.String getUserId ();
//...
}
È necessario fornire una versione concreta dell’oggetto User a partire dalla classe astratta ottenuta dalla precompilazione. Si definisca allora UserImpl come segue:
package authentication;
public class UserImpl extends User {
public UserImpl() {
58
}
public UserImpl(String userId) {
this.userId = userId;
}
public String getUserId() {
return userId;
}
public String toString() {
return "#User : " + userId;
}
}
Quando l’ORB riceve un valuetype deve effettuare l’unmarshalling e quindi creare una nuova
istanza dell’oggetto opportunamente valorizzata; per fare questo utilizza la Factory associata al
tipo in questione. Una factory di default viene creata per ogni valuetype dal processo di compilazione dell’IDL. Nel caso in esame sarà generata una classe UserDefaultFactory.
La classe generata può essere utilizzata come base per la definizione di Factory complesse o
semplicemente modificata per ottenere il comportamento voluto; in ogni caso l’ORB deve conoscere l’associazione valuetype–factory. L’associazione può essere stabilita esplicitamente utilizzando il metodo register_value_factory dell’ORB oppure implicitamente utilizzando le
naming convention che stabiliscono che, nel caso in cui non esista un’associazione esplicita,
l’ORB utilizzi la classe <valuetype>DefaultFactory.
Per semplicità si adotti il secondo approccio. Nel caso si utilizzi idlj di Sun la UserDefaultgenerata non necessita di modifiche. Invece, nel caso si utilizzi VisiBroker, la classe
generata dall’IDL sarà incompleta e dovrebbe presentare il codice seguente
Factory
package authentication;
public class UserDefaultFactory implements org.omg.CORBA.portable.ValueFactory {
public java.io.Serializable read_value(org.omg.CORBA_2_3.portable.InputStream is) {
// INSTANTIATE IMPLEMENTATION CLASS ON THE LINE BELOW
java.io.Serializable val = null;
// REMOVE THE LINE BELOW AFTER FINISHING IMPLEMENTATION
throw new org.omg.CORBA.NO_IMPLEMENT();
return is.read_value(val);
}
}
Con una versione di JDK differente dalla 1.2 il codice generato potrebbe essere diverso e presentare problemi di compilazione.
I commenti generati da idl2java indicano quali modifiche effettuare. Per il caso in esame la Facpuò limitarsi a istanziare UserImpl.
tory
package authentication;
public class UserDefaultFactory
implements org.omg.CORBA.portable.ValueFactory {
59
public java.io.Serializable read_value
(org.omg.CORBA_2_3.portable.InputStream is) {
java.io.Serializable val = new UserImpl();
return is.read_value(val);
}
}
La classe Factory dovrà comunque essere presente nell’ambiente destinato all’unmarshalling
(nell’esempio sul client). La restituzione dell’oggetto sarà effettuata dal metodo login di
UserLoginImpl modificato come segue
public User login(String u, String p) throws InvalidUserException {
if (userId.equals(u) && pwd.equals(p))
return new UserImpl(u);
else
throw new InvalidUserException();
}
Dal punto di vista del client il meccanismo valuetype è invece assolutamente trasparente.
User user = uLogin.login(args[0], args[1]);
System.out.println("Login UserId: " + user.getUserId());
Nel caso in cui l’ORB non riesca a individuare un’implementazione per il tipo ricevuto, potrà
provare a scaricare la corretta implementazione dal chiamante (la funzionalità è simile a quella
del codebase RMI). Questa funzionalità può essere disabilitata per ragioni di security.
Come già detto, l’implementazione del meccanismo object-by-value in CORBA ha grande importanza perché consente un semplice utilizzo di RMI over IIOP. È quindi significativo per le
specifiche EJB 1.1 che hanno indicato come standard l’adozione di RMI over IIOP. Limitandosi
invece allo scenario programmazione distribuita CORBA, valuetype è importante in quanto introduce una sorta di serializzazione multilinguaggio e la semantica null value.
CORBA runtime information
In Java esiste la possibilità di ottenere a runtime una serie d’informazioni sulla composizione di
un qualunque oggetto (costruttori, metodi, attributi, ecc.). Grazie a queste informazioni è possibile utilizzare un’istanza (invocare metodi, impostare attributi, ecc.) pur non conoscendo a priori
la sua classe. Questa potenzialità consente di realizzare soluzioni molto eleganti e flessibili a
problemi storicamente complessi quali la definizione/manipolazione di componenti.
CORBA fornisce meccanismi simili a quelli appena descritti e tutti i CORBA object possono
fornire a runtime informazioni sulla propria struttura. Nel gergo CORBA queste informazioni
sono dette metadata e pervadono l’intero sistema distribuito. Il repository destinato a contenere
i metadata è l’Interface Repository che consente di reperire e utilizzare un oggetto ottenendo dinamicamente la sua descrizione IDL.
60
Queste caratteristiche conferiscono una flessibilità notevole al modello CORBA. Nella pratica
risultano particolarmente attraenti per i venditori di tool che sviluppano componenti distribuiti.
L’invocazione dinamica, presentando minori performance e maggiori difficoltà di sviluppo, è
sicuramente meno interessante per un utilizzo medio.
Introspezione CORBA
Ogni oggetto CORBA specializza CORBA::Object; le capacità introspettive di ogni CORBA
object derivano da questa superclasse.
// IDL
module CORBA {
//...
interface Object {
//...
IRObject get_interface_def();
ImplementationDef get_implementation();
boolean is_nil();
boolean is_a(in string logical_type_id);
boolean is_equivalent(in Object other_object);
boolean non_existent();
};
};
Il metodo get_interface_def può essere visto come l’equivalente del getClass Java e
fornisce i dettagli relativi a un oggetto (attributi, metodi, ecc.). Il metodo restituisce un oggetto
generico di tipo IRObject, per ottenere i dettagli di cui sopra è necessario effettuare il narrow a
InterfaceDef. Fino alle specifiche 2.3 esisteva un metodo get_interface che restituiva direttamente un oggetto di tipo InterfaceDef: molti ORB lo supportano ancora per backward
compatibility.
I restanti metodi sono funzioni di test: is_nil è l’equivalente CORBA di "obj == null",
controlla la compatibilità dell’oggetto con l’interfaccia data, is_equivalent verifica
l’equivalenza tra due reference e non_existent verifica se l’oggetto non è più valido.
is_a
Va notato che le signature viste sopra rappresentano le signature IDL del CORBA::Object, ma
nella pratica tutti i metodi visti sopra sono forniti dal CORBA.Object con un “ _ ” davanti
(_is_a, _get_implementation, …).
Interface Repository (IR)
L’Interface Repository è il servizio che contiene e fornisce le informazioni sulla struttura IDL
degli oggetti. Si è visto nei paragrafi precedenti che, attraverso l’invocazione del metodo
get_interface_def, è possibile ottenere un oggetto di tipo InterfaceDef che fornisca tutte
le informazioni sulla struttura di una interfaccia.
L’IR contiene in forma persistente gli oggetti InterfaceDef che rappresentano le interfacce
IDL registrate presso il repository. L’IR può esser utilizzato anche dall’ORB per effettuare il
61
type-checking nell’invocazione dei metodi, per assistere l’interazione tra differenti implementazioni di ORB o per garantire la correttezza del grafo di derivazione.
Nel caso in cui si intenda utilizzare esplicitamente l’IR sarà necessario registrare l’IDL. La registrazione varia nelle varie implementazioni. VisiBroker prevede due comandi: irep (attiva l’IR)
e idl2ir (registra un IDL presso l’IR). Il JDK non fornisce attualmente alcuna implementazione dell’IR.
Interrogando l’IR è possibile ottenere tutte le informazioni descrivibili con un’IDL. La navigazione di queste informazioni avviene tipicamente a partire dalla classe InterfaceDef e coinvolge un complesso insieme di classi che rispecchiano l’intera specifica IDL: ModuleDef, InterfaceDef, OperationDef, ParameterDef, AttributeDef, ConstantDef, …
Dynamic Invocation Interface (DII)
Negli esempi visti finora, per invocare i metodi sugli oggetti CORBA si è utilizzata l’invocazione
statica. Questo modello di invocazione richiede la precisa conoscenza, garantita dalla presenza
dello stub, dell’interfaccia dell’oggetto.
In realtà in CORBA è possibile accedere a un oggetto, scoprire i suoi metodi ed eventualmente
invocarli, senza avere alcuno stub precompilato e senza conoscere a priori l’interfaccia esposta
dall’oggetto CORBA.
Questo implica la possibilità di scoprire le informazioni di interfaccia a runtime. Ciò è possibile
utilizzando una delle più note caratteristiche CORBA: la Dynamic Invocation Interface (DII).
La DII opera soltanto nei confronti del client; esiste un meccanismo analogo lato server (Dynamic Skeleton Interface, DSI) che consente a un ORB di dialogare con un’implementazione senza
alcuno skeleton precompilato.
Purtroppo, anche se DII fa parte del core CORBA, le sue funzionalità sono disperse su un gran
numero di oggetti. Per invocare dinamicamente un metodo su di un oggetto i passi da compiere
sono:
Ottenere un riferimento all’oggetto: anche con DII per utilizzare un oggetto è necessario ottenere un riferimento valido. Il client otterrà un riferimento generico ma, non avendo classi precompilate, non potrà effettuare un narrow o utilizzare meccanismi quali il bind fornito
dall’Helper.
Ottenere l’interfaccia: invocando il metodo get_interface_def sul riferimento si ottiene
un IRObject e da questo, con narrow, l’oggetto navigabile di tipo InterfaceDef. Questo oggetto è contenuto nell’IR e consente di ottenere tutte le informazioni di interfaccia. Con i metodi lookup_name e describe di InterfaceDef è possibile reperire un metodo e una sua
completa descrizione.
Creare la lista di parametri: per definire la lista di parametri viene usata una struttura dati particolare, NVList (Named Value List). La creazione di una NVList è effettuata o da un metodo
62
dell’ORB (create_operation_list) o da un metodo di Request (arguments). In ogni caso
con il metodo add_item è possibile comporre la lista di parametri.
Creare la Request: la Request incapsula tutte le informazioni necessarie all’invocazione di un
metodo (nome metodo, lista argomenti e valore di ritorno). Comporre la Request è la parte più
pesante e laboriosa della DII. Può essere creata invocando sul reference dell’oggetto il metodo
create_request o la versione semplificata _request.
Invocare il metodo: utilizzando Request esistono più modi di invocare il metodo. Il primo modo è quello di invocarlo in modalità sincrona con invoke. Il secondo modo è quello di invocarlo in modalità asincrona con send_deferred e controllare in un secondo momento la risposta
con poll_response o get_response. Il terzo modo è l’invocazione senza response con
send_oneway.
Come si può osservare, un’invocazione dinamica può essere effettuata seguendo percorsi differenti. Lo scenario più complesso implica un’interazione con l’Interface Repository (il secondo e
il terzo passo dell’elenco precedente) mediante la quale è possibile ottenere l’intera descrizione di
un metodo.
Scenari più semplici non prevedono l’interazione con l’IR e sono a esempio adottati quando ci si
limita a pilotare l’invocazione dinamica con parametri inviati da script.
Poiché l’invocazione dinamica è un argomento complesso e di uso non comune, verrà proposto
un semplice esempio che non utilizzi l’IR, ma sia pilotato da linea di comando.
Si definisca una semplice interfaccia come segue
// IDL
module dii {
interface DynObject {
string m0();
string m1(in string p1);
string m2(in string p1, in string p2);
string m3(in string p1, in string p2, in string p3);
};
};
Come si è detto in precedenza, l’uso di DII non ha alcuna influenza sul lato server. Il server si
limiterà a istanziare l’oggetto e a registrarlo col nome di DynObject sul Naming Service.
L’implementazione dell’oggetto è molto semplice e ha come unico scopo quello di consentire il
controllo dei parametri e del metodo invocato
package server;
import dii.*;
public class DynObjectImpl extends _DynObjectImplBase {
public DynObjectImpl() {
super();
}
63
public String m0() {
return "Metodo 0 # nessun parametro";
}
public String m1(String p1) {
return "Metodo 1 # " + p1;
}
public String m2(String p1, String p2) {
return "Metodo 2 # " + p1 + " - " + p2;
}
public String m3(String p1, String p2, String p3) {
return "Metodo 3 # " + p1 + " - " + p2 + " - " + p3;
}
}
Il client deve identificare dall’input fornito da linea di comando il metodo da invocare e i suoi
eventuali parametri; il primo passo significativo da compiere è l’accesso al Naming Service.
//...
org.omg.CORBA.Object obj = ncRef.resolve(path);
Poiché stub, skeleton, Helper e Holder saranno distribuiti solo sul server, non è possibile effettuare un narrow.
A questo punto è possibile iniziare a costruire la Request con il metodo più semplice
org.omg.CORBA.Request request = obj._request(args[0]);
Si noti che il parametro passato args[0] è il nome del metodo che si intende utilizzare, letto
da linea di comando.
Ora è possibile costruire la lista di argomenti; nel caso in esame la lista è costruita dinamicamente effettuando un parsing dell’array di input.
org.omg.CORBA.NVList arguments = request.arguments();
for (int i = 1; i < args.length; i++) {
org.omg.CORBA.Any par = orb.create_any();
par.insert_string(args[i]);
arguments.add_value("p" + 1, par, org.omg.CORBA.ARG_IN.value);
}
Ogni valore della NVList è rappresentato da un oggetto di tipo Any; questo è uno speciale tipo
CORBA che può incapsulare qualunque altro tipo e ha un’API che fornisce specifiche operazioni di inserimento ed estrazione di valori (nell’esempio si usano insert_string ed extract_string).
Per invocare il metodo è ancora necessario impostare il tipo di ritorno. Per fare questo si utilizza
l’interfaccia TypeCode che è in grado di rappresentare qualunque tipo IDL. Le costanti che identificano i TypeCode dei vari tipi IDL sono fornite da TCKind.
request.set_return_type(orb.get_primitive_tc(org.omg.CORBA.TCKind.tk_string));
64
request.invoke();
L’invocazione utilizza la normale chiamata sincrona di metodo data da invoke. Terminata
l’invocazione è possibile ottenere e visualizzare la stringa di ritorno.
org.omg.CORBA.Any method_result = request.return_value();
System.out.println(method_result.extract_string());
L’esecuzione dell’esempio è possibile con VisiBroker utilizzando i consueti passi, in particolare
si faccia riferimento all’esempio visto in precedenza sul Naming Service.
DII fornisce un meccanismo estremamente elastico e flessibile che prospetta un sistema assolutamente libero in cui tutti gli oggetti interrogano un Trader Service, individuano e utilizzano dinamicamente i servizi di cui necessitano.
Malauguratamente l’invocazione dinamica presenta alcuni limiti che ne condizionano l’utilizzo.
In primo luogo l’invocazione dinamica è decisamente più lenta dell’invocazione statica poiché
ogni chiamata a metodo implica un gran numero di operazioni remote.
In secondo luogo la costruzione di Request è un compito complesso e richiede uno sforzo supplementare in fase di sviluppo (lo sviluppatore deve implementare i compiti normalmente svolti
dallo stub). L’invocazione dinamica è inoltre meno robusta in quanto l’ORB non può effettuare
il typechecking prima dell’invocazione, questo può causare anche un crash durante
l’unmarshalling.
Callback
Esistono situazioni in cui un client è interessato al verificarsi di un particolare evento sul server
(cambiamento di uno stato, occorrenza di un dato errore, …). In un ambiente distribuito, in cui
tipicamente non è possibile conoscere a priori il numero di client, soddisfare questa necessità è
problematico.
Il meccanismo più semplice implementabile è il polling: il client periodicamente controlla il valore cui è interessato. Questo approccio è poco robusto e implica un notevole spreco di risorse.
Approcci migliori sono forniti da meccanismi di notifica asincrona quali CORBA Event Service
o sistemi di messaggistica.
Un’ultima possibilità è quella di utilizzare una callback: sarà il server a invocare direttamente un
metodo di notifica sul client o su un oggetto ad esso associato. Questo approccio ha notevoli
vantaggi computazionali, ma può presentare difficoltà nel caso in cui la topologia di rete veda il
client e il server separati da un firewall.
Tralasciando momentaneamente i problemi di rete, l’utilizzo di callback si presta bene a risolvere
alcuni classici problemi di programmazione distribuita. A titolo d’esempio s’immagini la costruzione di una semplice chat.
65
Figura 9 — Il pattern Observer
I client devono ricevere una notifica nel caso in cui si connetta un nuovo utente, venga inviato
un messaggio o un utente lasci la chat. Si può organizzare l’applicazione adottando un meccanismo simile a quello della gestione degli eventi Java: i client sottoscrivono presso il server chat un
servizio di notifica.
La soluzione classica per problemi di questo tipo è data dal pattern Observer. Questo pattern è
pensato proprio per quelle situazioni in cui un cambiamento di stato su un oggetto (Subject) ha
potenziali impatti su un numero imprecisato di altri oggetti (Observers). In queste situazioni solitamente è necessario inviare agli Observers una generica notifica.
Il numero di Observers deve poter variare a runtime e il Subject non deve conoscere quali informazioni sono necessarie al generico Observer. Per questa ragione la formulazione classica del
pattern prevede che siano definiti:
— una classe astratta Subject che fornisca i metodi subscribe, unsubscribe e notify;
— una classe ConcreteSubject che definisca i metodi di accesso alle proprietà interessate;
— una classe Observer che fornisca il metodo di ricezione notifica;
— una classe ConcreteObserver che mantenga un riferimento al ConcreteSubject e
fornisca il metodo update utilizzato per riallineare il valore delle proprietà;
Quando un Subject cambia stato invia una notifica a tutti i suoi Observer (quelli che hanno invocato su di lui il metodo subscribe) in modo tale che questi possano interrogare il Subject
per ottenere le opportune informazioni e riallinearsi al Subject.
Il pattern Observer, anche conosciuto come Publish/Subscribe, è molto utilizzato nei casi in cui
è necessario implementare meccanismi “ 1 a n ” di notifica asincrona. Anche il CORBA Event
Service adotta il pattern Observer.
66
Tornando alla chat, l’implementazione del pattern può essere leggermente semplificata facendo
transitare direttamente con la notifica il messaggio cui i client sono interessati.
Nell’esempio in esame ogni client si registrerà come Observer presso il server chat (in uno scenario reale probabilmente si definirebbe un oggetto Observer separato). Il server invocherà il
metodo di notifica su tutti gli Observer registrati inviando loro il messaggio opportuno. Nello
scenario in esame anche l’oggetto Observer, essendo remoto, dovrà essere attivato sull’ORB e
definito via IDL.
// IDL
module chat {
// Forward declaration
interface SimpleChatObserver;
struct User {
string userId;
SimpleChatObserver callObj;
};
struct Message {
User usr;
string msg;
};
// OBSERVER
interface SimpleChatObserver {
// N.B. Il server non aspetta alcuna
// risposta dai vari client
oneway void callback(in Message msg);
};
// SUBJECT
interface SimpleChat {
void subscribe(in User usr);
void unsubscribe(in User usr);
void sendMessage(in Message msg);
};
};
Nell’esempio non vengono utilizzati gli adapter e l’attivazione viene effettuata mediante connect (quindi è utilizzabile anche con Java IDL). Sarà il client stesso a registrarsi presso l’ORB.
package client;
import
import
import
import
import
import
chat.*;
org.omg.CORBA.*;
org.omg.CosNaming.*;
javax.swing.*;
java.awt.*;
java.awt.event.*;
public class SimpleChatClient extends _SimpleChatObserverImplBase {
private SimpleChat chat = null;
private User user = null;
private JTextField tMsg = new JTextField();
67
private JButton bSend = new JButton("Send");
private JTextArea taChat = new JTextArea();
public SimpleChatClient() {
super();
// qui il codice di inizializzazione UI
}
public void init(String userId) throws Exception {
// Crea e inizializza l'ORB
ORB orb = ORB.init((String[])null, null);
// Root naming context
org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");
NamingContext ncRef = NamingContextHelper.narrow(objRef);
// Utilizzo il Naming per ottenere il riferimento
NameComponent nc = new NameComponent("SimpleChat", " ");
NameComponent path[] = {nc};
chat = SimpleChatHelper.narrow(ncRef.resolve(path));
// Si registra presso l'ORB
// N.B. Il server deve essere in grado di effettuare
// un'invocazione remota del metodo callback
orb.connect(this);
// Crea e registra user
user = new User(userId, this);
chat.subscribe(user);
}
// Metodo remoto di notifica invocato dal server
public void callback(Message msg) {
taChat.append("#" + msg.usr.userId + " - " + msg.msg + "\n");
tMsg.setText(" ");
}
// Lo userId del client è passato da linea di comando
public static void main(String args[]) throws Exception {
SimpleChatClient sc = new SimpleChatClient();
sc.init(args[0]);
}
}
Il riferimento al client viene fatto transitare nell’oggetto User e registrato presso la chat dal metodo subscribe. Per semplicità la deregistrazione del client è associata all’evento di chiusura
della finestra (in un contesto reale sarebbe necessario un approccio più robusto).
JFrame f = new JFrame("SIMPLE CHAT");
f.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
try {
chat.unsubscribe(user);
System.exit(0);
} catch (Exception ex) {}
}
});
68
L’oggetto SimpleChatImpl è invece avviato e registrato presso l’ORB da un oggetto server
con le modalità consuete. SimpleChatImpl definisce il funzionamento della chat vera e propria.
package server;
import chat.*;
import java.util.Hashtable;
import java.util.Enumeration;
public class SimpleChatImpl extends _SimpleChatImplBase {
Hashtable h = new Hashtable();
public SimpleChatImpl() {
super();
}
// Aggiunge un utente alla chat
public synchronized void subscribe(User user) {
h.put(user.userId, user);
Message msg = new Message(user, " has joined the chat");
this.sendMessage(msg);
System.out.println("Added: " + user.userId);
}
// Rimuove un utente dalla chat
public synchronized void unsubscribe(User user) {
h.remove(user.userId);
Message msg = new Message(user, " left the chat");
this.sendMessage(msg);
System.out.println("Removed: " + user.userId);
}
// Invia il messaggio a tutti gli utenti
public void sendMessage(Message msg) {
User user = null;
for (Enumeration e = h.elements(); e.hasMoreElements();) {
user = (User) e.nextElement();
// Invoca la callback
try {
user.callObj.callback(msg);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
L’esecuzione dell’esempio segue i consueti passi. Come detto in precedenza, un possibile problema è legato all’eventuale presenza di firewall che inibiscano l’invocazione da server a client.
69
Figura 10 — Il client chat in esecuzione.
CORBA e i firewall
Per ragioni di sicurezza tutte le Intranet aziendali utilizzano firewall per limitare il traffico di rete
da e verso un certo host. Il firewall agisce come una barriera bidirezionale limitando il traffico
sulla base di valutazioni sull’origine/destinatario della richiesta e sul tipo di protocollo utilizzato.
Ogni tecnologia di comunicazione distribuita deve quindi fare i conti con l’eventuale presenza di
firewall. Il problema è complesso sia perché non esiste uno standard realmente accettato, sia
perché la presenza di un firewall può riguardare tanto il lato server quanto il lato client.
Una tipica configurazione aziendale limita il traffico alla porta 80 riservata al traffico HTTP. Per
questa ragione una soluzione comune, adottata anche da RMI, è quella di incapsulare il traffico
client/server, JRMP o IIOP, in pacchetti HTTP request/response (HTTP tunneling). In questo
modo le richieste/risposte possono attraversare il firewall come le normali attività Web.
Nel caso si utilizzi il tunneling, il Web Server deve essere in grado di riconoscere il particolare
tipo di richiesta, estrarre i dati dal pacchetto HTTP e ridirigere la richiesta verso la risorsa opportuna. In questo modo il Web Server agisce sostanzialmente da router.
Per effettuare il routing, il Web Server utilizza CGI o Servlet con ovvi impatti negativi sulle performance. Inoltre, data la natura unidirezionale di HTTP, adottando il tunneling non è possibile
utilizzare invocazioni da server a client (notifiche di eventi e callback in genere).
Per queste ragioni il tunneling non si è mai affermato nel mondo CORBA e storicamente ad esso sono sempre state preferite soluzioni alternative proprietarie. Le più celebri soluzioni software fornite dal mercato sono Inprise GateKeeper e IONA WonderWall legate agli ORB VisiBroker e OrbixWeb.
Il funzionamento dei due prodotti è similare: ambedue lavorano come Proxy IIOP tra client e
server che abbiano restrizioni di security tali da impedire loro di comunicare direttamente. In
modo trasparente al client si occupano di ricevere, controllare e “forwardare” il traffico IIOP da
e verso l’oggetto remoto opportuno. Tutte le informazioni necessarie al forward transitano con i
reference CORBA. Poiché lavorano in ambedue le direzioni, non presentano i limiti della soluzione tunneling sull’utilizzo di callback.
Opportunamente utilizzati questi prodotti consentono anche di utilizzare CORBA con Applet
non certificate. Le restrizioni date dalla sandbox non consentono infatti comunicazioni con host
70
differenti da quello da cui l’Applet è stata scaricata e questo è in evidente contrasto con il principio della location transparency CORBA.
Ponendo Gatekeeper o WonderWall sullo stesso host del Web Server, la comunicazione con
l’Applet non violerà i limiti della sandbox. A livello TCP l’applet comunicherà direttamente con
un oggetto proxy contenuto nei due prodotti. Sarà questo oggetto a effettuare l’invocazione reale al servant CORBA e a gestire in maniera similare la risposta.
Prodotti come quelli citati forniscono soluzioni decisamente superiori al tunneling, ma non appartengono allo standard. CORBA 3.0 ha finalmente fornito specifiche esaustive per i GIOP
Proxy Firewall.
Nel caso sia consentito è comunque possibile aprire una porta sul firewall e vincolare le applicazioni CORBA a comunicare soltanto su di essa (le modalità variano da ORB a ORB).
CORBA e J2EE
Tipicamente CORBA viene presentato come middleware alternativo alle altre tecnologie per lo
sviluppo di applicazioni distribuite: RMI, EJB, DCOM, ecc.
Poiché però le specifiche danno grande risalto alle necessità di integrazione poste dalle applicazioni Enterprise, CORBA consente in varia misura di interagire con le tecnologie di cui sopra.
Nel caso di DCOM le possibilità di integrazione pongono alcuni problemi e tipicamente si limitano all’utilizzo di bridge che convertono le invocazioni CORBA in opportune invocazioni
DCOM.
Completamente differente è invece il ruolo che CORBA si trova a giocare nella piattaforma
J2EE. Grazie agli sforzi congiunti di OMG e Sun sono state definite alcune specifiche (object-by
value, RMI/IDL, EJB 1.1, CORBA/EJB interop) che non si limitano a portare CORBA a buoni
livelli di interoperabilità con la piattaforma Java Enterprise, ma ne fanno un fondamentale elemento infrastrutturale.
La piattaforma J2EE fornisce inoltre funzionalità di chiara ispirazione CORBA. Java Naming e
Directory Interface sono correlate al CORBA Naming Service. Java Transaction API e Java
Transaction Service sono correlate al CORBA Object Transaction Service. Molte similitudini
possono essere inoltre individuate in altri ambiti J2EE: gestione della Security, gestione della
persistenza e utilizzo di Messaging Oriented Middleware (MOM).
CORBA vs RMI
Come si è avuto modo di osservare esistono notevoli somiglianze tra l’utilizzo di CORBA e
quello di RMI. In realtà è evidente che molte delle conclusioni e dei dettami OMG sono confluiti nella definizione dell’architettura RMI.
Ad alto livello l’utilizzo di RMI è più semplice di quello CORBA. La ragione di questo va ricercata nell’implicita semplificazione che si ha dovendo fornire specifiche monolinguaggio. Inoltre
RMI può contare su un notevole insieme di funzionalità base offerte dal linguaggio Java e non
71
supportate dagli altri linguaggi utilizzabili con CORBA (si pensi alla serializzazione e all’introspezione).
In generale CORBA può offrire performance superiori a RMI e migliori soluzioni di clustering e
load balancing. Le differenze possono essere più o meno significative a seconda dell’ORB utilizzato.
La scelta tra l’utilizzo dell’una o dell’altra tecnologia è quindi legata alle esigenze d’utilizzo. In
uno scenario semplicemente Java senza alcuna necessità di integrazione con altri linguaggi la
scelta RMI potrà essere accettabile. In uno scenario Enterprise eterogeneo o con significative
esigenze di tuning e performance sarà da preferire l’adozione di CORBA.
Sebbene CORBA e RMI vengano spesso presentate come tecnologie alternative, esiste la concreta possibilità di farli cooperare utilizzando RMI con il protocollo CORBA IIOP.
RMI-IIOP
L’utilizzo tipico di RMI prevede l’adozione del protocollo di trasporto proprietario Java Remote
Method Protocol (JRMP). Nel 1998 specifiche prodotte da Sun e IBM hanno introdotto la possibilità di utilizzare RMI sul protocollo IIOP. L’elemento fondamentale di RMI-IOOP è costituito
dalle specifiche Object-by-value che consentono di adottare con CORBA il passaggio di parametri per valore tipico di RMI.
La definizione di RMI-IIOP fornisce allo sviluppatore i vantaggi di semplicità RMI uniti alle caratteristiche di portabilità/interoperabilità del modello CORBA. La soluzione è adottata anche
dall’infrastruttura EJB.
Per lo sviluppatore Java si ha inoltre il vantaggio di non dover imparare il linguaggio IDL. La
definizione delle interfacce viene operata direttamente in Java con modalità RMI. A partire dalle
interfacce Java un compilatore apposito genererà tutte le classi di infrastruttura CORBA.
Con VisiBroker vengono forniti due compilatori: java2iiop si occupa di generare stub, skeleton,
Helper e Holder, java2idl consente di ottenere la rappresentazione IDL.
Anche il JDK fornisce un completo supporto per RMI/IIOP. Utilizzando il JDK 1.3 questo è
già fornito con l’SDK, mentre nel caso si utilizzi un JDK precedente sarà necessario scaricarlo a
parte insieme ai tool RMI/IIOP (si veda RMI over IIOP in bibliografia). In ogni caso andrà utilizzata la nuova versione del compilatore rmic con i flag -iiop o -idl.
Le specifiche EJB 1.1 indicano RMI-IIOP come API standard di comunicazione. L’uso di RMIIIOP è la chiave della compatibilità tra CORBA ed EJB. Già prima di queste specifiche alcuni
Application Server fornivano il layer EJB sopra una infrastruttura CORBA (Inprise Application
Server, ecc.). Altri produttori utilizzavano invece implementazioni proprietarie dell’API RMI
(Bea WebLogic, ecc.).
Bibliografia
la homepage di OMG
http://www.omg.org
72
la specifica
Java IDL
VisiBroker
prodotti CORBA
RMI over IIOP
ftp://ftp.omg.org/pub/docs/ptc/96-03-04.pdf
http://java.sun.com/products/jdk/idl
http://www.inprise.com/visibroker/
http://www.infosys.tuwien.ac.at/Research/Corba/software.html
http://java.sun.com/products/rmi-iiop/index.html
© 2001 – proprietà di MokaByte® s.r.l.
tutti i diritti riservati
è vietata la riproduzione non autorizzata anche parziale
73