Introduzione
In
questa puntata concluderemo la serie di tre puntate dedicate alla serializzazione
ed invio di oggetti attraverso il protocollo HTTP. Questa puntata sarà
dedicata all'invocazione di metodi di oggetti remoti.
La
tecnica presentata consentirà, come avviene per RMI, di avere un
modo semplice per invocare il metodo di un oggetto che stia sul server
(o sia invocato via RMI dal server). Contrariamente a quanto avviene per
RMI, nel nostro caso utilizzeremo il protocollo HTTP e la tecnica sviluppata
nelle puntate precedenti per realizzare questa funzionalità.
Nel
seguito dell'articolo avremo un'occasione unica di avere un piccolo assaggio
di una funzionalità poco conosciuta ma incredibilmente potente di
Java: l'introspection.
Lo
schema di base
Grazie
al codice sviluppato nelle puntate precedenti disponiamo di un meccanismo
per inviare un oggetto da un cliente ad un servlet attraverso il
protocollo HTTP. Assumiamo, per il momento, che anche il
servlet
sia in grado di inviare un oggetto al cliente, sempre attraverso HTTP.
Disponendo di un tale meccanismo potremmo incapsulare in un oggetto la
chiamata di metodo con i relativi parametri e inviarla al servlet
che si occuperà di eseguire il metodo e invierà al cliente
il risultato dell'esecuzione del metodo, sempre contenuto all'interno di
un oggetto.
Come
è possibile realizzare un oggetto che invochi il metodo di una classe
senza che si sappia a priori quale siano? La risposta è semplice:
si usa una caratteristica di Java chiamata introspection accessibile
mediante il package java.lang.reflect. Attraverso questo package
è infatti possibile, dato un oggetto, sapere qual'è la sua
classe e come è composta: si possono infatti sapere quali sono gli
attributi, i metodi e i costruttori. Il comando javap è realizzato
utilizzando questa parte della libreria di sistema.
Utilizzando
l'introspection saremo in grado dinamicamente di caricare la classe
appropriata ed invocarne i metodi direttamente nel
servlet. Vediamo
innanzitutto la classe MethodRequest che il cliente invierà al servlet
e descriverà il metodo da invocare e i parametri dell'invocazione.
La
classe MethodRequest
L'invocazione
di un metodo richiede quattro informazioni:
il
nome della classe a cui appartiene il metodo
l'oggetto
su cui va eseguito il metodo
il
nome del metodo
i
parametri dell'invocazione
Secondo
lo schema di RMI assumiamo che vi sia una sola istanza di cui si possono
invocare i metodi. Lo schema può essere semplicemente esteso per
supportare più oggetti ma in questo modo potremo utilizzare direttamente
RMI all'interno del servlet.
Assumendo
che vi sia un'istanza per classe utilizzeremo una Hashtable per associare
ad ogni classe l'istanza utilizzata per eseguire i metodi. Poiché
la tabella sarà utilizzata da tutte le richieste è dichiarata
static.
Gli
attributi della classe saranno quindi i seguenti:
public
class MethodRequest implements Serializable {
public static Hashtable classes = new Hashtable();
private String className;
private String methodName;
private Serializable[] params;
...
}
// MethodRequest
Oltre
alla tabella delle istanze di cui abbiamo appena parlato troviamo: il nome
della classe a cui appartiene il metodo; il suo nome e i parametri con
cui va invocato. I parametri devono essere serializzabili poiché
dovranno essere serializzati insieme alla richiesta.
Osserviamo
infine come anche la classe implementi l'interfaccia java.io.Serializable
per garantire che possa essere serializzata dal metodo send.
Il
costruttore della classe si limita semplicemente ad inizializzare i tre
attributi:
...
public MethodRequest(String name,
String method,
Serializable[] p) {
className = name;
methodName = method;
params = p;
}
...
Esiste
infine il metodo invoke, invocato dal servlet che si occuperà
di invocare il metodo in base al valore degli attributi. Sarà questo
metodo a fare uso dei meccanismi di
introspection offerti da Java.
Il
primo passo consiste nel controllare se la classe è già stata
caricata o meno. Nel secondo caso è necessario procedere al suo
caricamento:
...
if (classes.get(className) == null) {
if (className.startsWith("\\")) {
Object o = Naming.lookup(className);
classes.put(className, o);
} else {
Class c = Class.forName(className);
Object o = c.newInstance();
classes.put(className, o);
}
}
...
Se
il nome della classe inizia per '\' si assume allora che si vuole invocare
un metodo remoto attraverso RMI e si procede quindi all'esecuzione del
metodo lookup della classe Naming appartenente al package java.rmi.
In caso contrario si assume che className contenga il nome di una classe
locale e se ne crea un'instanza.
In
entrambi i casi si ottiene un oggetto su cui verranno poi invocati i metodi
e viene memorizzato nella tabella che tiene traccia di questi oggetti.
Nel seguito del metodo si potrà quindi ottenere tale oggetto da
tale tabella.
Il
passo successivo consiste nell'ottenere l'oggetto di tipo Class che descrive
la classe a cui appartiene l'oggetto. Mediante questo oggetto otterremo
un riferimento ad un oggetto di tipo Method che ci consentirà finalmente
di invocare il metodo desiderato.
Poiché
un metodo in Java è identificato dal nome, dal tipo e dal numero
dei parametri, è necessario costruire un array che contiene
tanti oggetti di tipo Class che indicano il tipo dei parametri del metodo
desiderato:
...
Object o = classes.get(className);
Class c = o.getClass();
Class[] sign = (params == null) ? null : new Class[params.length];
if (params != null)
for (int i = 0; i < params.length; i++)
sign[i] = params[i].getClass();
Method m = c.getMethod(methodName, sign);
...
Il
tipo e il numero dei parametri viene dedotto dalla lista dei parametri
ricevuti dal cliente. Una volta costruita la lista dei tipi è possibile
richiedere all'oggetto Class il metodo il cui nome è methodName
e la segnatura è data dalla lista costruita. Se la lista è
null si intende che il metodo desiderato non ha parametri.
Una
volta ottenuto l'oggetto di tipo Method che descrive il metodo desiderato
utilizziamo il suo metodo invoke per eseguirlo. Poiché un metodo
va eseguito su un oggetto il primo parametro di questa funzione è
l'oggetto su cui va invocato e il secondo è l'array contenente il
valore dei parametri:
...
ret = (Serializable)m.invoke(o, params);
} catch(Exception e) {
return new MethodResponse(e.toString());
}
return new MethodResponse(ret);
...
Il
valore restituito dal metodo invoke della classe Method viene memorizzato
in un oggetto di tipo MethodResponse che descriveremo tra breve. Nel caso
in cui sia sollevata un'eccezione (ad esempio perché il metodo non
esiste) viene costruito un oggetto di tipo MethodResponse con la stringa
che descrive l'eccezione.
La
classe MethodResponse
Questa
classe contiene semplicemente l'oggetto restituito dall'esecuzione del
metodo oppure la stringa che descrive un'eventuale eccezione. Il codice
è il seguente:
public
class MethodResponse implements java.io.Serializable {
private
Serializable ret = null;
private String msg = null;
public MethodResponse(Serializable r) {
ret = r;
}
public MethodResponse(String err) {
msg = err;
}
public Serializable getResponse() {
return ret;
}
public String getException() {
return msg;
}
}
// MethodResponse
Osserviamo
nuovamente che questa classe implementa l'interfaccia java.io.Serializable
poiché dovrà essere serializzata per inviare il risultato
dell'esecuzione al cliente che ha richiesto l'esecuzione del metodo.
Le
modifiche al servlet
Abbiamo
detto in precedenza che, per poter disporre di un meccanismo di invocazione
di metodi remoti, avremmo bisogno di un oggetto inviato dal cliente al
servlet,
che contenga le informazioni relative al metodo da invocare, e di un oggetto
inviato dal servlet al cliente che contenga il valore restituito
dall'esecuzione del metodo.
Il
metodo doPost del servlet diviene quindi il seguente:
public
void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
ObjectInputStream in = new ObjectInputStream(request.getInputStream());
try {
MethodRequest r = (MethodRequest)in.readObject();
MethodResponse ret = r.invoke();
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(buffer);
out.writeObject(ret);
// Prepare the header
byte[] serobj = buffer.toByteArray();
response.setContentLength(serobj.length);
response.getOutputStream().write(serobj);
} catch(Exception e) {
System.out.println(e);
}
}
In
sostanza si legge l'oggetto dalla richiesta, assumendo che sia di tipo
MethodRequest. Si usa il metodo invoke per eseguire il metodo e si ottiene
l'oggetto di tipo MethodResponse contenente la risposta da inviare al cliente.
Utilizzando
un codice molto simile a quello del metodo send descritto negli articoli
precedenti si prepara la risposta del servlet e si invia utilizzando
l'oggetto response.
Le
modifiche al metodo send
Il
metodo send dell'applet di esempio deve quindi ora ricevere l'oggetto contenente
il risultato dell'esecuzione del metodo. È quindi necessario leggere
la risposta del servlet e usare un
ObjectInputStream:
public Object send(InetAddress addr, int p,
String path, Serializable obj) throws IOException {
// Save the object's instance
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(buffer);
out.writeObject(obj);
// Prepare the header
byte[] serobj = buffer.toByteArray();
StringBuffer outb = new StringBuffer();
outb.append("POST ");
outb.append(path);
outb.append(" HTTP/1.0\r\nUser-Agent: JavaObjectTunnel\r\n");
outb.append("Content-Type: application/java-object\r\n");
outb.append("Content-Length: ");
outb.append(serobj.length);
outb.append("\r\n\r\n");
// Open the connection and send the data
Socket s = new Socket(addr, p);
OutputStream sout = s.getOutputStream();
sout.write(outb.toString().getBytes());
sout.write(serobj);
sout.write("\r\n".getBytes());
// Read the response
InputStream in = s.getInputStream();
BufferedReader bin = new BufferedReader(new InputStreamReader(in));
// Skip the header
while (!"".equals(bin.readLine()));
Object ret = null;
ObjectInputStream oin = new ObjectInputStream(s.getInputStream());
try {
ret = oin.readObject();
} catch(Exception e) {}
s.close();
return ret;
}
Il
codice che legge l'oggetto è simile a quello utilizzato nel
servlet.
Proviamo
il tutto...
Per
provare il nostro nuovo sistema utilizzeremo la seguente classe:
public
class Hello {
public String helloWorld() {
return "Hello World";
}
}
// Hello
Che
andrà messa insieme alle classi MethodRequest e MethodResponse nella
directory classes del
servlet in modo che siano visibili sul server.
Nell'applet
di esempio il metodo actionPerformed diverrà come il seguente:
...
public void actionPerformed(ActionEvent evt) {
try {
MethodRequest req = new MethodRequest("Hello",
"helloWorld",
null);
MethodResponse resp =
(MethodResponse)send(InetAddress.getByName(
getCodeBase().getHost()),
8080,"/objser/servlet/ObjectServlet", req);
if (resp.getException() != null)
System.out.println("Exception: " + resp.getException());
else
System.out.println(resp.getResponse());
} catch(IOException e) {
}
}
...
Si
prepara un oggetto MethodRequest indicando la classe Hello e il metodo
da invocare helloWorld. Il terzo parametro rappresenta la lista dei parametri
del metodo che nel nostro caso vuota.
Il
metodo send restituisce un oggetto di tipo MethodResponse utilizzato per
stampare sulla JavaConsole il risultato dell'esecuzione (la stringa "Hello
World") oppure l'errore che si è verificato.
Conclusioni
In
questo articolo abbiamo presentato una tecnica per realizzare un meccanismo
analogo a RMI sfruttando la serializzazione di oggetti Java attraverso
HTTP. Si potrebbero fare numerose considerazioni ma è arrivato il
momento di concludere. È importante però sottolineare che
consentire ad un applet di eseguire metodi remoti può introdurre
rischi di sicurezza. È quindi necessario analizzare attentamente
i metodi eseguiti dal server, magari controllando esplicitamente i nomi
nel metodo invoke.
Gli
esempi si possono scaricare cliccando qui |