Introduzione
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 ed implementazione.
A titolo
d’esempio, si immagini un client che si occupa solamente di presentation
ed un server (magari su un’altra macchina) che gestisce la business logic.
Le due applicazioni dovranno al più condividere oggetti che incapsulano
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 ed al server.
Fino
alle specifiche 2.3 non era possibile il passaggio per valore con oggetti
CORBA; come si è visto negli articoli precedenti 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 ed alcuni approcci alternativi.
Una possibile
soluzione
La
prima soluzione è molto semplice ed 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.
Implementiamo
una semplice (quanto fasulla) funzionalità di login
//
IDL
module
authentication {
struct
User {
string userId;
};
exception
InvalidUserException{};
interface
UserLogin {
User login(in string user, in string pwd)
raises (InvalidUserException);
};
};
Per
semplicità supponiamo 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à
ad 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 negli articoli precedenti. 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();
}
}
E’
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;
}
//
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 ed il 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 ed 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 si
affronti uno scenario che prevede Java sui client e sui server è
comunque una soluzione comoda, simile nell’approccio ad RMI.
Ridefiniamo
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);
};
};
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 segue
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();
}
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. E’ uno dei meccanismi fondamentali per l’implementazione di RMI
over IIOP (si veda più avanti il paragrafo RMI/IDL).
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 ed 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.
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.
Modifichiamo
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 ();
//….
}
E’
necessario fornire una versione concreta dell’oggetto User a partire dalla
classe astratta ottenuta dalla precompilazione. Definiamo allora UserImpl
come segue
package
authentication;
public
class UserImpl extends User {
public
UserImpl() {
}
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à adottiamo il secondo approccio. Nel caso si utilizzi
idlj di Sun la UserDefaultFactory generata non necessita di modifiche.
Invece, nel caso si utilizzi Visibroker, la classe generata dall’IDL sarà
incompleta e dovrebbe presentare il codice seguente
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 Factory può limitarsi ad istanziare UserImpl
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) {
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 ad 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. E’ 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.
Conclusioni
In
questo articolo abbiamo visto tre possibili approcci CORBA al passaggio
di parametri per valore. Il prossimo mese studieremo le CORBA run-time
information.
Allegati
Gli
esempi completi si possono scaricare qui |