Abbiamo visto finora come http (trasporto) e xml (data representation) possano essere intuitivamente usati per realizzare applicazioni web. Queste semplici modalità applicative possono essere utilizzate per sviluppare applicazioni ad hoc, ma non sono sufficienti ad indirizzare gli aspetti di interoperabilità tra applicazioni sviluppate indipendentemente.
È questo l'obiettivo dei Web Services, un insieme di architetture e specifiche condivise finalizzato a risolvere i problemi di interoperabilità nella cooperazione applicativa.
Abbiamo presentato nella parte introduttiva del corso che esistono più linguaggi e protocolli utilizzabili per la realizzazione di applicazioni di tipo web services. In questo corso faremo riferimento al linguaggio XML ed al protocollo SOAP, ma tenendo ben presente che la maggior parte dei concetti sono completamente interscambiabili passando ad altre tecnologie come il linguaggio JSon ed il protocollo REST.
SOAP (Simple Object Access Protocol) è un protocollo basato su XML per lo scambio d'informazioni in un ambiente distribuito e definisce un formato comune per trasmettere dati tra client e service. SOAP prevede l'imbustamento dei contenuti applicativi da scambiare all'interno di un formato di busta XML ed e' indipendente dal protocollo di trasporto utilizato per la consegna dei messaggi, che teoricamente potrebbe anche avvenire off-line.
Il namespace degli elementi SOAP è http://www.w3.org/2001/12/soap-envelope
.
L'elemento base di un messaggio SOAP è il SOAP Envelope.
<?xml version="1.0"?> <soap:Envelope xmlns:soap="http://www.w3.org/2001/12/soap-envelope"> ... </soap:Envelope>
L'Envelope
ha due figli: l'Header
, opzionale, e il Body
, obbligatorio.
L'elemento opzionale SOAP Header
estende il messaggio e contiene metadati, informazioni utili al
processamento del messaggio,
come ad esempio l'identità dell'utente, informazioni riguardo la cifratura del documento, o informazioni
per il routing del messaggio.
<?xml version="1.0"?> <soap:Envelope xmlns:soap="http://www.w3.org/2001/12/soap-envelope"> <soap:Header> <myHeader soap:actor="..." soap:mustUnderstand="..."> ... </myHeader> </soap:Header> <soap:Body ...> ... </soap:Body> </soap:Envelope>
Un elemento dell'Header può avere alcuni attributi speciali:
MustUnderstand: Talvolta alcuni header devono essere processati affinchè l'applicazione possa procedere oltre. Si consideri ad esempio l'header contenente le informazioni di cifratura del messaggio. Se non viene processato (e quindi il messaggio rimane cifrato) il processamento non può andare avanti. Per indicare che un header DEVE essere analizzato o il processamento interrotto si usa l'attibuto MustUnderstand settandolo a "1" (di default è "0").
Actor: Un messaggio SOAP può viaggiare dal mittente al destinatario attraversando differenti endpont lungo il percorso. Non tutti gli header del messaggio sono necessariamente indirizzati al destinatario finale, ma anche a nodi intermedi. L'attributo opzionale
actor
serve appunto a specificare l'endpoint al quale è indirizzato l'elemento.
Il SOAP Body, obbligatorio, infine contiene i dati veri e propri del messaggio, chiamato Payload.
<?xml version="1.0"?> <soap:Envelope xmlns:soap="http://www.w3.org/2001/12/soap-envelope"> <soap:Body xmlns="http://www.bank.org/ns"> <pagamento> <da>cliente</da> <a>shop</a> <importo>100</importo> </pagamento> </soap:Body> </soap:Envelope>
Il livello di messaggio è indipendente dal livello di trasporto, quindi la busta SOAP può essere impacchettata per essere inviata via HTTP, SMTP, etc. , ma la struttura e il contenuto della busta SOAP rimarrà immutato.
Finora abbiamo visto come modellare (XSD) i messaggi scambiati dalle applicazioni, ma gli aspetti quali l'indirizzo fisico del servizio, le operazioni supportate, tipologia dei messaggi per ogni operazione risultano ancora cablati nelle applicazioni. Il Web Service Definition Language (WSDL) è un linguaggio basato su XML per specificare tali aspetti dei servizi. Mediante WSDL può essere quindi descritta l'interfaccia pubblica di un Web Service fornendo le informazioni necessarie per poter interagire con un determinato servizio: un "documento" WSDL contiene infatti, relativamente al Web Service descritto, informazioni su:
cosa può essere utilizzato (le "operazioni" messe a disposizione dal servizio);
come utilizzarlo (il protocollo di comunicazione da utilizzare per accedere al servizio, il formato dei messaggi accettati in input e restituiti in output dal servizio ed i dati correlati) ovvero i "vincoli" (bindings in inglese) del servizio;
dove utilizzare il servizio (cosiddetto endpoint del servizio che solitamente corrisponde all'indirizzo - in formato URI - che rende disponibile il Web Service)
Il WSDL puo' essere suddiviso tra definizione logica e concreta. La prima descrive le interfacce, le operazioni ed i messaggi, mentre la seconda definisce il trasporto, il binding e gli endpoint. La struttura principale del WSDL apparirà grossomodo così
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"> <!-- abstract definitions --> <wsdl:types> .... </wsdl:types> <wsdl:message> .... </wsdl:message> <wsdl:portType> .... </wsdl:portType> <!-- concrete definitions --> <wsdl:binding> .... </wsdl:binding> <wsdl:service> .... </wsdl:service> </wsdl:definitions>
Vediamo come costruire un WSDL che descriva il comportamento della servlet che abbiamo implementato. Per adesso possiamo dire che il nostro servizio espone un'operazione che riceve i dati di un'ordine come messaggio XML/SOAP su HTTP, senza fornire risposta SOAP al client.
Il primo passo per la costruzione di un documento wsdl di un Web Service è quello di definirsi i tipi degli elementi in esso contenuti. Abbiamo già definto lo schema XSD di messaggio di ordine:
<?xml version="1.0"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:complexType name="ordineType"> <xs:sequence maxOccurs="unbounded"> <xs:element name="articolo" type="articoloType"/> </xs:sequence> </xs:complexType> <xs:complexType name="articoloType"> <xs:sequence> <xs:element name="nome" type="nomeType"/> <xs:element name="quantita" type="integer"/> </xs:sequence> </xs:complexType> <xs:simpleType name="nomeType"> <xs:restriction base="xs:string"> <xs:pattern value="[A-Za-z]{10}"/> </xs:restriction> </xs:simpleType> </xs:schema>
Importiamolo e aggiungiamo i tipi mancanti per l'operazione di notifica
Nota | |
---|---|
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ord="http://www.rivenditore.org/ordine" targetNamespace="http://www.rivenditore.org/rivenditore" <wsdl:import namespace="http://www.rivenditore.org/ordine" location="submitOrdine.xsd" /> <wsdl:types> <xsd:schema targetNamespace="http://www.rivenditore.org/ordine" <xsd:element name="ordine" type="ordineType"/> </xsd:schema> </wsdl:types> ... </wsdl:definitions> |
Descritti i tipi, possiamo definire i messaggi scambiati. Il nostro servizio ha un solo messaggio SOAP scambiato, quello di consegna dell'ordine.
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.rivenditore.org/ordine" ...> <wsdl:types> ... </wsdl:types> <wsdl:message name="ordineMessage"> <wsdl:part name="ordine" element="ele:ordine"/> </wsdl:message> </wsdl:definitions>
Il passo successivo sarà definire le interfacce. L'elemento portType
definisce un gruppo di operazioni.
Il nome portType
è sviante ci riferiremo a questo elemento spesso con il nome "interfaccia".
Ogni wsdl:operation
dell'interfaccia contiene una combinazione di elementi wsdl:input
e
wsdl:output
e opzionalmente un wsdl:fault
. L'ordine di questi elementi definisce il
Message Exchange Pattern (MEP) dell'operazione.
MEP | wsdl:operation |
---|---|
Request-Response |
<wsdl:output ..>
|
OneWay |
<wsdl:input ..>
|
Solicit-Response |
<wsdl:input ..>
|
Notification |
<wsdl:output ..>
|
Tabella 5. Message Exchange Pattern
Avvertimento | |
---|---|
La specifica WS-Interoperability fornisce una serie di line guida per garantire una maggiore interoperabilità tra diverse piattaforme, sistemi operativi e linguaggi di programmazione. Una di queste regole dice di non utilizzare i MEP Solicit-Response e Notification. |
Come detto in precedenza, l'operazione esposta dal nostro servizio prevede di ricevere un messaggio contenente un ordine, ma di non inviare messaggi SOAP di risposta, quindi un MEP di tipo Oneway. Vediamo come descrivere tale operazione.
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.rivenditore.org/ordine" ...> <wsdl:types> ... </wsdl:types> <wsdl:message> ... </message> <wsdl:portType name="ordineInterface"> <wsdl:operation name="ordine"> <wsdl:input message="ord:ordineMessage"/> </wsdl:operation> </wsdl:portType> </wsdl:definitions>
L'interfaccia è sempre considerata astratta da momento che non sa
come saranno rappresentati i messaggi "sul cavo" finchè non sarà
definito il binding
che specifica, tra le altre cose, il protocollo
di trasporto (nel nostro caso HTTP).
L'elemento wsdl:binding
descrive i dettagli concreti per utilizzare una particolare interfaccia
(portType
) con uno specifico protocollo.
La struttura di base dell'elemento binding è la seguente:
<wsdl:definitions .... > <wsdl:binding name=".." type=".."> <wsdl:operation name=".."> <wsdl:input name=".." > .. </wsdl:input> <wsdl:output name=".." > .. </wsdl:output> <wsdl:fault name=".."> .. </wsdl:fault> </wsdl:operation> </wsdl:binding> </wsdl:definitions>
L'attributo name, come per gli altri elementi, specifica un nome per riferirsi al binding nel resto del documento. Il type
deve specificare un portType
precedentemente definito. Nel nostro esempio possiamo inserire un binding
chiamato ordineInterfaceBinding
per l'interfaccia ordineInterface
:
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.rivenditore.org/ordine" ...> ... <wsdl:binding name="ordineInterfaceBinding" type="ordineInterface"> ... <wsdl:operation name="ordine"> ... <wsdl:input> ... </wsdl:input> </wsdl:operation> </wsdl:binding> ... </wsdl:definitions>
L'elemento wsdl:binding
è generico. Definisce solamente il framework per descrivere
i dettaggli di binding. Questi dettagli sono forniti utilizzando degli elementi estensivi.
Le specifiche WSDL forniscono alcuni elementi predifiniti per descrivere il binding SOAP,
anche se sono in un diverso namespace (http://schemas.xmlsoap.org/wsdl/soap/).
Vediamo come fare un SOAP/HTTP binding per la nostra interfaccia:
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.rivenditore.org/ordine" ...> ... <wsdl:binding name="ordineInterfaceBinding" type="ordineInterface"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name="ordine"> <soap:operation soapAction=""/> <wsdl:input> <soap:body use="literal"/> </wsdl:input> </wsdl:operation> </wsdl:binding> ... </wsdl:definitions>
l'elemento soap:binding
indica lo stile usato per la rappresentazione del messaggio (i valori possibili sono
document
o rpc
) rispetto al protocollo di trasporto richiesto (HTTP nel nostro caso).
L'elemento soap:operation
definisce il valore dell'header HTTP SOAPAction
.
Infine l'elemento soap:body
descrive come i part
del messaggio devono
essere serializzati all'interno del messaggio (valori possibili literal
o encoded
).
Maggiori informazioni su queste caratteristiche sono presentate nella lezione sui web services.
L'elemento wsdl:service
definisce una collezione di porte, o endpoint, che espone un particolare binding:
<wsdl:definitions .... > <wsdl:service name="..."> <wsdl:port name="..." binding="..."> ... </wsdl:port> </wsdl:service> </wsdl:definitions>
Per ogni porta dobbiamo fornire un nome da utilizzare come riferimento ed un binding tra quelli definiti in precedenza. Poi possiamo aggiungere elementi estensivi che definiscono i dettagli per l'indirizzamento specifici del binding.
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.rivenditore.org/ordine" ...> ... <wsdl:service name="ordineService"> <wsdl:port name="ordineInterfaceEndpoint" binding="ord:ordineInterfaceBinding"> <soap:address location="http://www.rivenditore.org/ordine"/> </wsdl:port> </wsdl:service> </wsdl:definitions>
Una volta completato il wsdl possiamo verificarne la correttezza sintattica e l'aderenza alle specifiche WS-I. Ci sono vari tools utilizzabili a questo scopo, come quello distribuito con CXF, il Web Services Framework parte del progetto Apache, che utilizzeremo come base per le nostre attività di esercitazione. Per eseguire la validazione con CXF basta invocare il comando:
wsdlvalidator service.wsdl
Esistono diversi approcci per implementare un Web Service, anche in funzione degli specifici framework e linguaggi utilizzati. La principale differenza comunque risiede nella scelta di scrivere la logica applicativa dei propri servizi (lato client e server) in maniera trasparente rispetto al linguaggio di programmazione utilizzato, ad esempio invocando ed implementando direttamente metodi delle classi applicative, o piuttosto con una visibilità esplicita dei messaggi scambiati, utilizzando quindi approsite API per la gestione dei messaggi SOAP, e parser xml per la gestione dei contenuti applicativi dei messaggi.
Nel seguito di questa sezione vedremo come usare questi due approcci usando il linguaggio Java.
Ci sono due possibilità per scrivere applicazioni di questo tipo:
si scrive il servizio come una classe Java, si usa JAX-WS per l'annotazione della classe e si genera poi il WSDL del servizio, tramite appositi tool (es: Java2WSDL in CXF);
nel caso in cui sia già disponibile il WSDL del servizio, si può invece generare le classi java da usare per la sua implementazione: gli "stub" per il lato client e gli "skeleton" sul lato server.
In entrambi questi casi, è quindi possibile usare i Web Services in maniera trasparente dal linguaggio di programmazione, in modo tale che per ogni operazione supportata dal servizio sia semplicemente necessario invocare un metodo java sul lato client e implementare un metodo java sul lato server.
Il JAX-WS fornisce un mapping completo dalla definizione dei Web Service in WSDL alle classi Java che implementano quel servizio.
L'interfaccia logica, definita dall'elemento wsdl:portType
, e' mappata in un service endpoint interface (SEI).
Ogni tipo complesso definito in un WSDL viene mappato in classi Java che seguono il mapping definito dalla specifica
Java Architecture for XML Binding (JAX-B). L'endpoint definito dall'elemento wsdl:service
viene generato
in una classe Java usata dai fruitori per accedere agli endpoint che implementano il servizio.
Il tool wsdl2java automatizza la generazione di questo codice. Inoltre fornisce opzioni per la generazione del codice di partenza per l'implementazione del servizio ed altre funzionalita'.
E' possibile generare il codice necessario allo sviluppo del servizio con il seguente comando
wsdl2java -ant -impl -server -d outputDir myService.wsdl
Le opzioni hanno le seguenti funzioni:
L'argomento -ant genera un makefile per Ant, chiamato build.xml per eseguire e compilare l'applicazione
L'argomento -impl genera una classe che implementa ogni portType specificato dal WSDL
L'opzione -server genera un semplice main() per l'esecuzione del servizio come applicazione stand alone
L'opzione -d specifica la directory dove posizionare il codice generato
myService.wsdl e' il WSDL del servizio da implementare
Una volta eseguito il comando ci troveremo una serie di classi generate:
portTypeName.java il SEI. Questo file contiene l'interfaccia che il servizio implementa.
serviceName.java l'Endpoint. Questa classe verra' usata dai fruitori per effettuare richieste al servizio.
portTypeNameImpl.java lo Skeleton. Qeusta classe e' l'implementazione del servizio.
portTypeName_portTypeNameImplPort_Server.java un semplice main() che consente il deploy del servizio in un server stand alone.
L'altro approccio per sviluppare Web Service e' quello che prevede lo sviluppo del codice Java annotato in standard Jax-WS e di li generarne il WSDL. Vediamo un esempio. Cominciamo dal SEI:
public interface QuoteReporter{ public Quote getQuote(String ticker); }
ed un esempio di implementazione:
import java.util.*; public class StockQuoteReporter implements QuoteReporter { ... public Quote getQuote(String ticker) { Quote retVal = new Quote(); retVal.setID(ticker); retVal.setVal(Board.check(ticker));[1] Date retDate = new Date(); retVal.setTime(retDate.toString()); return(retVal); } }
Non rimane che annotare il codice con le annotazioni fornite dallo standard JAX-WS e fornire le seguienti informazioni
Il target namespace del servizio
La classe che racchiude il messaggio di richiesta
La classe che racchiude il messaggio di risposta
Se l'operazione e' di tipo Oneway
Il tipo di stile di Binding
Il nome della classe usata per le eccezioni
Il namespace dei tipi usati dal servizio
Nota | |
---|---|
Molte annotazioni hanno valori predefiniti, ma specificare completamente ogni dettaglio del servizio fornisce un maggior controllo sul WSDL generato e ne migliora l'interoperabilita'. |
Ecco alcune delle annotazioni introdotte dalla specifica Jax-WS.
@XmlAccessorType
: Con questa annotazione specifichiamo quali campi o proprieta' devono essere serializzati. Il valoreFIELD
indica che vanno serializzati tutti i campi pubblici e privati.@XmlType
Mappa una classe al un tipo XML Schema. Se il tipo e' complesso della speciexs:all
oxs:sequence
si puo' specificare gli elementi inclusi e il loro ordine con la proprieta'propOrder()
@XmlElement
: Indica che il campo va serializzato e permette di specificarne il nome locale ed il namespace.@XmlSchemaType
: Mappa un tipo Java ad un tipo semplice predefinito.-
@WebService
(Required): Specifica che il metodo seguente e' l'implementazione di un endpoint.name
: il nome delwsdl:portType
targetNamespace
: il namespace del WSDL e degli elementi generati (salvo diversamente imposto dalee regole di mapping di JAXB).serviceName
: il nome delwsdl:service
portName
: ilwsdl:portName
@SOAPBinding
: indica il tipo di binding. Di default utilizza il Wrapped.@WebResult
: Indica il nome dell'elemento della risposta. Se non e' specificato utilizza [nomeoperazione]Response
@Oneway
: Indica che il metodo e' di tipo Oneway.
Per una lista esaustiva con utili esempi di utilizzo, consultare le specifiche.
import javax.jws.*; @WebService(targetNamespace = "http://www.rivenditore.org/Ordine", name = "OrdineInterface") @SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE) public interface OrdineInterface { @SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE) @Oneway @WebMethod(operationName = "Notifica") public void notifica( @WebParam(partName = "parameter", name = "notifica", targetNamespace = "http://www.rivenditore.org/ordiniElements") java.lang.String parameter); ... }
Una volta terminata l'implementazione e' possibile verificarne la bonta' generando il WSDL associato con l'apposito comando java2wsdl
Abbiamo visto nella sezione precedente come realizzare servizi web in maniera piu' o meno trasparente rispetto ai linguaggi di programmazione utilizzati. In alcuni casi pero' questo puo' non essere conveniente, ma si puo' preferire agire direttamente sui documenti xml contenuti nei messaggi, anziche' essere costretti a convertire i messaggi in oggetti java.
Abbiamo gia' visto nelle precedenti sezioni su xml qualcosa di simile, in particolare come realizzare dei client e servlet in grado di inviare e ricevere messaggi XML su HTTP. Allo stesso modo potremmo quindi far viaggiare messaggi SOAP su HTTP. Tuttavia per il trattamento di messaggi SOAP esiste una specifica standard in java, che ne ottimizza la gestione (SAAJ: SOAP with Attachments API for Java). Vediamo quindi come servirci di questa libreria per imbustare il nostro ordine in un messaggio SOAP.
In precedenza abbiamo implementato un servizio in base alle operazioni che espone, ovvero un servizio che lavora a livello di operazioni. Se vogliamo implementare un servizio che lavori direttamente sul messaggio ricevuto possiamo implementarlo in questo modo:
@WebServiceProvider @ServiceMode(value=Service.Mode.MESSAGE) public class OrdiniService implements Provider<SOAPMessage>{ public SOAPMessage invoke(SOAPMessage request) { SOAPMessage response = null; ... return response; } }
Il servizio puo' quindi essere implementato agendo sull'oggetto SOAPMessage usando la API SAAJ ed un normale parser xml:
SOAPPart sp = request.getSOAPPart(); SOAPEnvelope se = sp.getEnvelope(); SOAPBody sb = se.getBody(); SOAPHeader sh = se.getHeader() org.w3c.dom.Document dom = sb.extractContentAsDocument(); NodeList node = dom.getElementsByTagName("ordine"); ...
Anche sul versante client puo' essere usato SOAP, come segue:
Si instanzia una
javax.xml.soap.MessageFactory
Con la MessageFactory si costruisce un
javax.xml.soap.SOAPMessage
Si popola il SOAPMessage coi nodi necessari
Si invia la busta SOAP usando un SOAPConnector
Vediamo nel seguito il codice da utilizzare.
//Istanziamo una MessageFactory MessageFactory mf = MessageFactory.newInstance(); //Costruiamo il SOAPMessage vuoto SOAPMessage msg = mf.createMessage(); //Popoliamo il body del SOAPMessage con i nodi necessari SOAPElement pagamento = msg.getSOAPBody().addChildElement("pagamento", "pay", "http://www.bank.org/ns"); pagamento.addChildElement("da","pay").setNodeValue("cliente"); pagamento.addChildElement("a","pay").setNodeValue("shop"); pagamento.addChildElement("importo","pay").setNodeValue("100"); //Scriviamo il SOAPMessage nel messaggio HTTP e riceviamo la risposta SOAPConnectionFactory scFactory = SOAPConnectionFactory.newInstance(); SOAPConnection con = scFactory.createConnection(); java.net.URL endpoint = new URL("http://rivenditore.com/ordina"); SOAPMessage response = con.call(message, endpoint); ...