Visualizzazione post con etichetta WSDL. Mostra tutti i post
Visualizzazione post con etichetta WSDL. Mostra tutti i post

martedì 19 giugno 2012

Web services: request asincrone

Salve di nuovo :)

In questo post voglio parlare di come invocare un web service facendo richieste asincrone.
Partiamo dal presupposto di aver già implementato un servizio e di aver a disposizione il file WSDL che lo descrive.
Per la generazione del client ovviamente non si parte da zero, ma si può utilizzare il plugin per Eclipse WSDL2Java per generare la maggior parte del codice necessario.

Con questo tool si possono generare automaticamente le classi per fare request sincrone e asincrone contemporaneamente. Dandogli in pasto il WSDL del servizio da utilizzare, questo genera tutte le classi per eventuali tipi di dati custom che le operazioni del servizio accettano come parametri o che restituiscono.

Supponiamo che il servizio implementato sia MyService, con una sola operazione, echo, che prende in input  una stringa e la restituisce. Il servizio non lancia eccezioni, per non complicare le cose :)
Le classi che vengono generate per il client sono:

  • L'interfaccia MyService
  • MyServiceResponse
  • La classe astratta MyServiceCallbackHandler
  • Echo (una classe per ogni operazione definita)
  • MyService*Exception (una classe per ogni eccezione che può lanciare il servizio)
  • MyServiceStub
Come per le richieste sincrone, la classe MyServiceStub viene utilizzata per settare i parametri della request e per invocare il servizio. La classe Echo (o qualsiasi altra classe corrispondente ad un'operazione) viene utilizzata per contenere i parametri dell'operazione, utilizzati poi dallo stub. Infine la classe MyServiceResponse ovviamente viene utilizzata per immagazzinare la risposta restituita dal servizio.

Per poter effettuare una request asincrona, è necessario utilizzare MyServiceCallbackHandler.
Questa è una classe astratta contenente dei metodi con implementazione vuota, uno per ogni operazione del servizio ed uno per ogni eccezione che questo può lanciare.
Quando la richiesta asincrona viene effettuata, viene lanciato un nuovo thread che si mette in attesa del completamento della request e viene eseguito il metodo associato all'operazione richiamata (o ad una condizione di eccezione) per gestire la risposta del servizio.
Infatti, tutti i metodi associati alle operazioni accettano come parametro un oggetto di tipo Response.
Invece, i metodi corrispondenti alle condizioni di eccezione, come si può immaginare, accettano un parametro di tipo Exception.
Bisogna quindi estendere tale classe ed implementare i metodi che ci interessano. Fortunatamente non c'è bisogno di implementarli tutti: i metodi di MyServiceCallbackHandler hanno tutti un'implementazione vuota, quindi se il nostro client utilizza una sola delle operazioni offerte da un servizio si può fare l'override dei metodi appropriati.

Supponiamo che la classe che implementa il callback handler si chiami CallbackImplementation (accorciamo un po il nome :P). 
Giusto per scrivere un po' di codice riporto l'implementazione di questa classe :) 

public class CallbackImplementation extends clientstubpackage.MyServiceCallbackHandler {

   private boolean complete;

   public CallbackImplementation() {
      complete = false
   }


   @Override
   public void receiveResultecho(
            clientstubpackage.MyServiceResponse result
                ) {
      System.out.println(result.getReturn());
      complete = true;
   }
}


L'implementazione del client che chiama il servizio è praticamente uguale ad un client sincrono, cambiano un paio di cose. I passi da seguire per richiamare il servizio sono:

  • Istanziare un client stub
  • Istanziare un oggetto per l'operazione da eseguire
  • Istanziare un callback handler
  • Settare eventuali parametri per l'operazione
  • Richiamare il servizio con il metodo startOperation dello stub (invece di stub.operazione)
Ecco qui il codice che implementa il client ed esegue tutti i passi descritti:



public class ClientImplementation {

   public static void main(String[] args) {
  
      try {
         //inizializzazione stub
         MyServiceStub stub = new MyServiceStub();
         //inizializzazione handler
         CallbackImplementation handler = new CallbackImplementation();

        //inizializzazione dell'oggetto per l'operazione e settaggio parametri
        Echo echo = new Echo();
        echo.setContent("Stringa da restituire");
   
        long start = System.currentTimeMillis();
   
        stub.startecho(echo, handler); //inizio della chiamata asincrona
   
        while(!handler.hasCompleted()) {
           Thread.sleep(1000);
           long now = System.currentTimeMillis();
           System.out.println("Waiting.. " + ((now - start)/1000));
        }
   
        System.out.println("Done!");
       } catch (IOException e) {
          e.printStackTrace();
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
   }

}

E questo è quanto.
Ci sono giusto 2 cose da mettere in evidenza, riguardante il metodo startecho.
La prima è che, come si vede dal codice, oltre ai parametri dell'operazione esso ha bisogno di un'istanza del callback handler.
La seconda, molto importante, è che si deve tener presente che al contrario delle request sincrone, quando viene fatta una request asincrona, richiamando il metodo startOperation dello stub viene lanciato un nuovo thread e l'esecuzione del metodo chiamante continua parallelamente.
Per questo c'è bisogno di controllare che questo finisca. In questo caso molto semplicemente l'handler quando completa il suo lavoro setta un flag a true, ed il metodo chiamante non fa altro che ciclare e controllare ogni secondo se la richiesta è stata completata (magari l'echo non ha bisogno di aspettare "secondi".. :P).

Ed anche questo post è finito, era da un po' che non postavo qualcosa..
Per i prossimi post ancora non ho deciso che scriverò, ho un paio di idee..vedrò il tempo e la voglia cosa mi porteranno a fare :)

Alla prossima!

mercoledì 30 maggio 2012

Invio e ricezione di file da web services

Eccomi qui con un nuovo posto..
Ultimamente sto tenendo una bella media, chissà se riuscirò a tenere questa costanza.. :)

Bene, oggi volevo scrivere un post sull'invio e la ricezione di file verso e da un web service, visto che è una cosa che mi sono ritrovato a fare solo recentemente.

In questo post sia i servizi1 che i client saranno scritti in Java. Il web service engine che utilizzato è Apache Axis (installato su Tomcat 7) ed il protocollo di comunicazione è SOAP.

Per la generazione del pacchetto .aar contenente le classi per "deployare" il servizio in Axis è stato utilizzato il plugin per Eclipse "Service Archiver", mentre per creare il file WSDL dal codice del servizio e generare il client dal WSDL creato sono stati utilizzati Java2WSDL e WSDL2Java, entrambi contenuti nel plugin "Code Generator". Questi tool sono disponibili all'indirizzo: http://axis.apache.org/axis2/java/core/tools/index.html.

Prenderemo in considerazione un servizio, FileService che mette a disposizione 2 operazioni: UploadFile e GetFile.
La cosa importante affinchè le cose funzionino è definire la firma dei metodi corrispondenti alle operazioni affinchè il Code Generator possa generare un file WSDL con il giusto mapping tra i tipi.

Una prima prova potrebbe essere fatta specificando come parametro dell'operazione UploadFile e come tipo restituito dall'operazione GetFile semplicemente java.io.File. 
Purtroppo questa non è una buona soluzione. Se si prova ad utilizzare Java2WSDL con i metodi così definiti, verrà generato un correttamente il file WSDL desiderato. Il problema, però, nasce quando si utilizza WSDL2Java per generare il client. Dalle classi create si vede che il mapping dei tipi è abbastanza sballato, infatti ci sono delle classi come java.io.xsd.File che per essere utilizzate come minimo dovrebbero essere incluse in un package diverso, in quanto l'uso del nome java.io per un package è ovviamente proibito.
Non so se sia possibile utilizzare comunque java.io.File per il trasferimento dei dati, probabilmente definendo a mano i tipi complessi nel WSDL e costruento i messaggi SOAP tramite codice è possibile, ma se uno non ha tempo, o non ha voglia, o una combinazione delle 2, allora sarebbe preferibile percorrere un'altra strada :)

Bene, dopo aver visto questa non-soluzione passiamo alle 2 che ho sperimentato:
  • trasferire dati tramite array di byte e DataHandler
  • trasferire dati tramite DataHandler utilizzando MTOM per gli allegati SOAP

Array di Byte
Questo metodo è abbastanza intuitivo, per fare l'upload di un file si passa un'array di byte che rappresenta il contenuto del file e il web service che lo riceve lo scrive su file. Lato client, però, non si può passare semplicemente un array di byte, ma dev'essere passato un DataHandler.

Il servizio può essere per esempio il seguente:

public class ImportService { 
  public String importSingleFile(String name, byte[] content) {
     try {
       File f = new File(name); 
       BufferedWriter out = new BufferedWriter(new FileWriter(f));
       out.write(s);
       out.close();

       return "Import Successful!";
    } 
    catch(IOException e) {
       e.printStackTrace();
    }
  } 
 }  

Con Java2WSDL si può creare il file che descrive il servizio e con WSDL2Java si può generare il client che lo interroga. Il client sarà composto da una serie di classi,tra cui ImportStub e ImportSingleFile che corrispondono rispettivamente al servizio ed all'operazione da richiamare.

Il client, per poter trasferire il file al servizio, dovrà:
  • aprire il file che vuole trasferire
  • associare il file ad un DataSource (presente in javax.activation)
  • associare il DataSource ad un DataHandler (sempre in javax.activation)
  • richiamare il servizio
Ecco il codice:


public class Client { 
  public static void main(String args[]) {
     try {
       ImportStub stub = new ImportStub(); 
       ImportSingleFile single = new ImportSingleFile();
       
       File f = new File("upload.txt");
       FileDataSource ds = new FileDataSource(f);
       DataHandler dh = new DataHandler(ds);

       single.setName(f.getName());
       single.setContent(dh);

       ImportSingleFileResponse res = stub.importSingleFile(single);

       System.out.println(res.get_return());
    } 
    catch(IOException | AxisFault e) {
       e.printStackTrace();
    }
  } 
 }  

 E questo è quanto.
Il problema con questa soluzione è che i dati binari vengono codificati come testo per poi venire spediti in messaggi SOAP. Per ovviare a questo problema è possibile utilizzare MTOM.


MTOM e DataHandlers
MTOM  (Message Transmission Optimization Mechanism) è una raccomandazione W3C per inviare e ricevere dati binari da web services. Altri meccanismi possono essere utilizzati per lo stesso scopo, come SwA (SOAP with Attachments) e i DIME Attachments, ma a MTOM sembra sia migliore di questi ultimi.

Per poterlo utilizzare in Axis è necessario abilitarlo: bisogna editare il file di configurazione axis/WEB-INF/conf/axis2.xml e settare a true il parametro enableMTOM.

Fatto ciò si può scrivere il servizio. In realtà cambia molto poco: invece di un array di byte come parametro prende un DataHandler. Ecco il codice:


public class ImportService { 
  public String importSingleFile(String name, DataHandler dh) {
     try {
       File f = new File(name); 
       FileOutputStream fos = new FileOutputStream(f);

       dh.writeTo(fos);
       fos.close();

       return "Import Successful!";
    } 
    catch(IOException e) {
       e.printStackTrace();
    }
  } 
 }  

Il client invece resta esattamente uguale.

E così si conclude questo post..spero di scrivere qualche altra cosa presto! :)