Tra le caratteristiche più particolari di Java un posto di rilievo è occupato sicuramente da RMI. Il significato dell'acronimo è invocazione remota di metodi. Come è facile intuire, si fa riferimento ad applicazioni distribuite nelle quali viene resa possibile la comunicazione tra oggetti remoti (ovvero non necessariamente localizzati sulla medesima macchina) attraverso l'invocazione di metodi tra gli oggetti stessi.
Più precisamente, quando parliamo di oggetti remoti (remote objects) facciamo riferimento ad oggetti creati su Java Virtual Machine differenti. I remote objects, in RMI, hanno il vincolo di dover implementare una o più interfacce che contengano la dichiarazione dei metodi che si desidera esportare (ovvero dei metodi che si intende "remotizzare"). Tali interfacce, a loro volta, devono derivare dall'interfaccia java.rmi.Remote
.
La potenza di RMI consiste nel fatto che è possibile utilizzare tranquillamente la medesima sintassi Java e tutte le potenzialità offerte dalla progettazione orientata agli oggetti anche quando si invocano i metodi appartenenti agli oggetti remoti.
Prima di scendere nei particolari, vediamo una sorta di detailed-deployment diagram che illustra le interazioni e le entità coinvolte quando si utilizza RMI:
Come si vede dal diagramma, RMI si basa sull'interazione tra tre entità distinte:
- Uno o più Server RMI (per semplicità ne considereremo uno)
- Il Java RMI Registry (localizzato sul server)
- Uno o più Client RMI (per semplicità ne considereremo uno)
Il Server RMI implementa un'interfaccia relativa ad un particolare oggetto RMI e registra tale oggetto nel Java RMI Registry. Il Java RMI Registry è, semplicemente, un processo di tipo daemon che tiene traccia di tutti gli oggetti remoti disponibili su un dato server.
Il Client RMI effettua una serie di chiamate al registry RMI per ricercare gli oggetti remoti con cui interagire.
A questo punto ci si potrebbe porre un paio di domande:
- Come fa un oggetto che si trova su una macchina client ad invocare dei metodi che sono definiti su un'altra macchina (server) ?
- Chi si occupa di gestire le problematiche legate alla comunicazione di rete?
Il trucco è tutto da ricercare nei due componenti che nel diagramma precedente sono denominati Skeleton e Stub.
Lo Skeleton, sul server, si occupa di interagire direttamente con l'oggetto RMI che espone i metodi "remotizzati", inviando a quest'ultimo tutte le richieste provenienti dallo Stub.
Lo Stub, invece, rappresenta una sorta di classe "clone" che ripropone e mette a disposizione del client tutti i metodi che sul server sono stati definiti e implementati come remoti. In altre parole, lo Stub fa le veci di una classe spesso denominata "proxy class".
Sia lo Skeleton sia lo Stub si occupano, infine, in modo trasparente all'utente (e al programmatore) della gestione della comunicazione tra il client ed il server.
Vediamo ora di dettagliare, passo per passo, il meccanismo descritto nel diagramma iniziale, utilizzando la medesima numerazione riportata in figura.
- Viene creata sul server una istanza dell'oggetto remoto e passata in forma di stub al Java RMI registry. Tale stub viene, quindi registrato all'interno del registry stesso.
- L'applicazione client richiede al registry RMI una copia dell'oggetto remoto da utilizzare.
- Il Java RMI registry restituisce una copia serializzata dello stub al client
- L'applicazione client invoca uno dei metodi dell'oggetto remoto utilizzando la classe "clone" fornita dallo stub
- Lo stub richiama lo skeleton che si trova sul server chiedendogli di invocare sull'oggetto remoto lo stesso metodo che il client ha invocato sullo stub
- Lo skeleton invoca il metodo richiesto sull'oggetto remoto
- L'invocazione del metodo sull'oggetto remoto restituisce il risultato allo skeleton
- Lo skeleton comunica il risultato allo stub sul client
- Lo stub fornisce il risultato all'applicazione client iniziale
Esempio pratico: la radice quadrata
Vediamo in pratica le nozioni descritte fin'ora. Implementiamo un oggetto remoto che espone un metodo che consenta di calcolare la radice quadrata di un numero. Scriviamo, quindi, il codice di un client che invochi tale metodo e mostri, alla fine, il risultato a video.
Creazione dell'interfaccia remota
Anzitutto creiamo un'interfaccia remota che contenga al suo interno la definizione dei metodi che l'oggetto sul server espone ai client. Un'interfaccia remota deve sottostare ai seguenti vincoli:
- Derivare dalla interfaccia java.rmi.Remote
- Definire ogni metodo con la clausola: throws java.rmi.RemoteException
Vediamo il codice:
Listato 1. Interfaccia remota
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface ISquareRoot extends Remote
{
double calculateSquareRoot(double aNumber) throws RemoteException;
}
Creazione della applicazione Server
Per ogni interfaccia remota definita (nel nostro semplice caso, una soltanto) è necessario creare una classe sul server che contenga l'implementazione dell'oggetto remoto vero e proprio. Tale classe deve:
- Derivare dalla classe
java.rmi.server.UnicastRemoteObject
- Implementare tutti i metodi definiti nell'interfaccia remota (ma questa è una regola sempre valida in Java)
- Tutti i costruttori definiti nella classe devono "lanciare" un'eccezione del tipo
java.rmi.RemoteException
. Si noti che se si decidesse di utilizzare soltanto il costruttore di default, sarà comunque necessario scriverne il codice per gestire proprio la throw dell'eccezioneRemoteException
.
Vediamo il codice:
Listato 2. Applicazione server
import java.net.MalformedURLException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.Naming;
import java.rmi.RemoteException;
public class RMISquareRootServer extends UnicastRemoteObject
implements ISquareRoot
{
public RMISquareRootServer()throws RemoteException
{
}
public double calculateSquareRoot(double aNumber)
{
return Math.sqrt( aNumber);
}
public static void main(String[] args)
{
try
{
ISquareRoot server = new RMISquareRootServer();
Naming.rebind("//localhost/RMISquareRoot",server);
}
catch (RemoteException e){e.printStackTrace( );}
catch (MalformedURLException e) {e.printStackTrace( );}
}
}
Molto importante è l'istruzione evidenziata in rosso con la quale, infatti, viene effettuato il bind dell'oggetto server (di tipo ISquareRoot
) con il nome "RMISquareRoot
".
Si noti che per semplicità abbiamo inserito il main all'interno della classe
RMISquareRootServer
ma, se avessimo voluto scrivere in modo più "pulito", avremmo potuto implementare una classe a parte solo per eseguire il main.
Si faccia, altresì, attenzione al fatto che è stato utilizzato l'indirizzo localhost
che, nel caso in cui l'applicazione Server si fosse trovata su una workstation separata, avrebbe dovuto essere sostituito dall'indirizzo IP di tale macchina.
Creazione dell'applicazione Client
Il compito dell'applicazione client è ricercare l'applicazione server che espone gli oggetti remotizzati messi a disposizione attraverso RMI ed invocarne, quindi, i metodi opportuni (nel nostro caso l'unico metodo è calculateSquareRoot()
.
Nel caso del client è importante notare l'istruzione in rosso che si occupa proprio di effettuare il lookup (la ricerca) dell'oggetto remotizzato denominato RMISquareRoot sulla macchina il cui indirizzo è localhost (come si sarà intuito, per semplicità, stiamo eseguendo le applicazioni server e client sul medesimo PC).
Vediamo il codice:
Listato 3. Applicazione client
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.net.MalformedURLException;
public class RMISquareRootClient
{
public static void main(String[] args) {
int x = Integer.parseInt(args[0]);
try
{
ISquareRoot squareServer =
(ISquareRoot) Naming.lookup ("rmi://localhost/RMISquareRoot");
double result = squareServer.calculateSquareRoot(x) ;
System.out.println(result);
}
catch(NotBoundException e)
{
e.printStackTrace( );
}
catch(RemoteException e)
{
e.printStackTrace( );
}
catch(MalformedURLException e)
{
e.printStackTrace( );
}
}
}
Esecuzione dell'Applicazione
Per poter eseguire l'applicazione sarà necessario aprire 3 finestre di Prompt dei Comandi e procedere nell'ordine seguente:
- Avviare il Java RMI registry attraverso il comando: rmiregistry. Si noti che tale comando non restituisce nulla.
- Eseguire il server attraverso il comando: java RMISquareRootServer
- Eseguire il client attraverso il comando: java RMIClientRootServer 576 ( dove 576 è il numero in input del quale si vuole calcolare la radice quadrata).
Se tutti i passaggi sono stati eseguiti correttamente verrà visualizzata a video la radice quadrata del valore fornito in input.
Se si vuole testare l'applicazione su più macchine (cosa che ovviamente ha più senso se si sceglie di utilizzare RMI per scopi non meramente didattici) si provi a implementare il server ed il client su workstation differenti.
Note
Nell'esempio precedente si è utilizzato il J2SE 1.5.0_06 che solleva il programmatore dal compito di creare lo skeleton e lo stub necessari alla tecnologia RMI. Qualora si utilizzasse una versione più vecchia di JRE, bisognerà, invece, avvalersi del comando rmic per produrre queste due classi.
Nel caso si utilizzino due macchine distinte per l'applicazione server e per quella client, sarà necessario copiare l'interfaccia remota ISquareRoot anche sul client.