MokaByte 55 - 7mbre  2001
Foto dell'autore non disponibile
di
Gianluca Morello
Corso CORBA
VIII Parte
Passaggio di parametri per valore
Questo mese presenteremo tre differenti approcci al passaggio di parametri per valore in uno scenario CORBA
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

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


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