MokaByte 49 - Febbraio 2001
Foto dell'autore non disponibile
di
Antonio Cisternino
Metodi remoti via HTTP
Attraversare i firewall per invocare metodi remoti 
Invocare metodi remoti da un applet sfruttando il meccanismo di serializzazione degli oggetti attraverso HTTP visto nelle puntate precedenti

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

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