SeamFramework.orgCommunity Documentation

Capitolo 24. Web Service

24.1. Configurazione ed impacchettamento
24.2. Web Service conversazionali
24.2.1. Una strategia raccomandata
24.3. Esempio di web service
24.4. Webservice RESTful HTTP con RESTEasy
24.4.1. Configurazione RESTEasy e gestione delle richieste
24.4.2. Risorse e provider come componenti Seam
24.4.3. Sicurezza della risorse
24.4.4. Mappare eccezioni e risposte HTTP
24.4.5. Test delle risorse e dei provider

Seam si integra con JBossWS per consentire allo standard JEE web service di sfruttrare pienamente il framework contestuale di Seam, includendo il supporto ai web service conversazionali. Questo capitolo passa in rassegna tutti i passi richiesti per consentire ai web service di funzionare in ambiente Seam.

Per consentire a Seam di intercettare le richieste web service in modo tale da creare i contesti Seam necessari per la richiesta, deve essere configurato uno speciale handler SOAP; org.jboss.seam.webservice.SOAPRequestHandler è un'implementazione SOAPHandler che esegue il lavoro di gestione del ciclo di vita di Seam durante lo scope di una richiesta web service.

Uno speciale file di configurazione, standard-jaxws-endpoint-config.xml, deve essere collocato nella directory META-INF del file jar che contiene le classi web service. Questo file contiene la seguente configurazione handler SOAP:


<jaxws-config xmlns="urn:jboss:jaxws-config:2.0" 
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
              xmlns:javaee="http://java.sun.com/xml/ns/javaee"
              xsi:schemaLocation="urn:jboss:jaxws-config:2.0 jaxws-config_2_0.xsd">
   <endpoint-config>
      <config-name
>Seam WebService Endpoint</config-name>
      <pre-handler-chains>
         <javaee:handler-chain>
            <javaee:protocol-bindings
>##SOAP11_HTTP</javaee:protocol-bindings>
            <javaee:handler>
               <javaee:handler-name
>SOAP Request Handler</javaee:handler-name>
               <javaee:handler-class
>org.jboss.seam.webservice.SOAPRequestHandler</javaee:handler-class>
            </javaee:handler>
         </javaee:handler-chain>
      </pre-handler-chains>
   </endpoint-config>
</jaxws-config
>

Quindi come vengono propagate le conversazioni tra le richieste web service? Seam usa un elemento di intestazione SOAP presente in entrambi i messaggi di richiesta e di risposta SOAP per portare l'ID della conversazione dal consumatore al servizio, e viceversa. Ecco un esempio di richiesta web service che contiene un ID di conversazione:


<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:seam="http://seambay.example.seam.jboss.org/">
  <soapenv:Header>
    <seam:conversationId xmlns:seam='http://www.jboss.org/seam/webservice'
>2</seam:conversationId>
  </soapenv:Header>
  <soapenv:Body>
    <seam:confirmAuction/>
  </soapenv:Body>
</soapenv:Envelope
>    
    

Come si può vedere nel messaggio SOAP sovrastante, dentro l'header SOAP c'è un elemento conversationId che contiene l'ID della conversazione di appartenenza della richiesta, in questo caso 2. Purtroppo, poiché i web services possono essere utilizzati da una varietà di client scritti in diversi linguaggi, spetta allo sviluppatore implementare la propagazione dell'ID della conversazione tra i distinti web service che si intende usare nell'ambito di una singola conversazione.

E' importante notare che l'elemento conversationId dell'header deve essere qualificato con il namespace http://www.jboss.org/seam/webservice, altrimenti Seam non sarà in grado di leggere l'ID della conversazione dalla richiesta. Ecco un esempio di una risposta al messagio della richiesta di cui sopra:


<env:Envelope xmlns:env='http://schemas.xmlsoap.org/soap/envelope/'>
  <env:Header>
    <seam:conversationId xmlns:seam='http://www.jboss.org/seam/webservice'
>2</seam:conversationId>
  </env:Header>
  <env:Body>
    <confirmAuctionResponse xmlns="http://seambay.example.seam.jboss.org/"/>
  </env:Body>
</env:Envelope
>    
    

Come si può vedere, il messaggio di risposta contiene lo stesso elemento conversationId della richiesta.

Esaminiamo un web service di esempio. Il codice di questa sezione proviene tutto dall'applicazione di esempio seamBay nella directory /examples di Seam, e segue la strategia raccomandata nella precedente sezione. Diamo innanzitutto un'occhiata alla classe del web service e a uno dei suoi metodi esposti come web service:

@Stateless

@WebService(name = "AuctionService", serviceName = "AuctionService")
public class AuctionService implements AuctionServiceRemote
{
   @WebMethod
   public boolean login(String username, String password)
   {
      Identity.instance().setUsername(username);
      Identity.instance().setPassword(password);
      Identity.instance().login();
      return Identity.instance().isLoggedIn();
   }
   // snip
}

Come si può notare, il nostro web service è un session bean stateless, ed è annotato usando l'annotazione JWS del package javax.jws, come specificato dalla JSR-181. L'annotazione @WebService comunica al container che questa classe implementa un web service, e l'annotazione @WebMethod sul metodo login() lo identifica come metodo di tipo web service. Gli attributi name e serviceName dell'annotazione @WebService sono opzionali.

Come richiesto dalle specifiche, ogni metodo che deve essere esposto come web service deve essere dichiarato anche nell'interfaccia remota della classe del web service (quando il web service è un session bean stateless). Nell'esempio suddetto, l'interfaccia AuctionServiceRemote deve dichiarare il metodo login() poiché esso è annotato come @WebMethod.

Come si può notare nel codice sovrastante, il web service implementa un metodo login() che delega l'esecuzione al componente Identity di Seam. Attenendoci alla strategia da noi raccomandata, il web service è scritto come un semplice facade, che inoltra il lavoro vero e proprio ad un componente Seam. Questo permette il massimo riutilizzo di business logic tra web service e altri clients.

Vediamo un altro esempio. Questo metodo web service inizia una nuova conversazione delelgando l'esecuzione al metodo AuctionAction.createAuction():

   @WebMethod

   public void createAuction(String title, String description, int categoryId)
   {
      AuctionAction action = (AuctionAction) Component.getInstance(AuctionAction.class, true);
      action.createAuction();
      action.setDetails(title, description, categoryId);
   }

Ed ecco il codice di AuctionAction:

   @Begin

   public void createAuction()
   {
      auction = new Auction();
      auction.setAccount(authenticatedAccount);
      auction.setStatus(Auction.STATUS_UNLISTED);        
      durationDays = DEFAULT_AUCTION_DURATION;
   }

Da ciò si può notare come i web service possano partecipare a conversazioni long running, svolgendo il ruolo di facade e delegando il vero lavoro a un componente Seam conversazionale.

Seam integra l'implementazone RESTEasy delle specifiche JAX-RS (JSR 311). E' possibile decidere quanto l'integrazione alla vostra applicazione debba spingersi in profondità:

Innanzitutto, si prendano le librerie RESTEasy e jaxrs-api.jar, e le si installi con le altre librerie della vostra applicazione. Si installi anche la libreria di integrazione, jboss-seam-resteasy.jar.

All'avvio, saranno automaticamente individuate e registrate come risorse HTTP tutte le classi annotate con @javax.ws.rs.Path. Seam accetta e gestisce automaticamente richieste HTTP col proprio componente SeamResourceServlet. L'URI di una risorsa è costruito come segue:

Come esempio, la seguente definizione di risorsa restituirebbe una rappresentazione puramente testuale for ogni richiesta GET diretta all'URI http://your.hostname/seam/resource/rest/customer/123:

@Path("/customer")

public class MyCustomerResource {
    @GET
    @Path("/{customerId}")
    @Produces("text/plain")
    public String getCustomer(@PathParam("customerId") int id) {
         return ...;
    }
}

Non è richiesta alcuna configurazione addizionale, non è necessario editare il file web.xml o nessun altra configurazione se questi valori di default sono accettabili. Comunque, è possibile procedere alla configurazione RESTEasy in un'applicazione Seam. Innanzitutto, occorre importare il namespace resteasy nell'header del file di configurazione XML:


<components
   xmlns="http://jboss.com/products/seam/components"
   xmlns:resteasy="http://jboss.com/products/seam/resteasy"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation=
     http://jboss.com/products/seam/resteasy
         http://jboss.com/products/seam/resteasy-2.1.xsd
     http://jboss.com/products/seam/components
         http://jboss.com/products/seam/components-2.1.xsd"
>

Allora è possibile modificare il prefisso /rest come accennato in precedenza:


<resteasy:application resource-path-prefix="/restv1"/>

Il percorso completo alle risorse è ora /seam/resource/restv1/{resource} - si noti che che le definizioni e le mappature di tipo path @Path NON cambiano. Questo è uno "switch" a livello di applicazione di solito usato per la gestione delle versioni delle API HTTP.

E' possibile disabilitare il troncamento del percorso base, qualora nelle risorse si voglia mappare il percorso completo:


<resteasy:application strip-seam-resource-path="false"/>

Il percorso di una risorsa è ora mappato, per esempio, con @Path("/seam/resource/rest/customer"). Non è raccomandabile disabilitare questa caratteristica, poiché le mappature delle classi delle risorse risultano allora legate al particolare scenario di deploy.

Seam scandaglierà il classpath alla ricerca delle risorsa @javax.ws.rs.Path e di ogni classe @javax.ws.rs.ext.Provider. E' possibile disabilitare la ricerca e configurare queste classi manualmente:


<resteasy:application
     scan-providers="false"
     scan-resources="false"
     use-builtin-providers="true">

     <resteasy:resource-class-names>
         <value
>org.foo.MyCustomerResource</value>
         <value
>org.foo.MyOrderResource</value>
         <value
>org.foo.MyStatelessEJBImplementation</value>
     </resteasy:resource-class-names>

     <resteasy:provider-class-names>
         <value
>org.foo.MyFancyProvider</value>
     </resteasy:provider-class-names>

 </resteasy:application
>

L'interruttore use-built-in-providers abilita (default) o disabilita i provider RESTEasy precostituiti. Si raccomanda di lasciarli abilitati, poiché essi forniscono automaticamente la gestione del "marshalling" testuale, JSON, e JAXB.

RESTEasy supporta gli EJB semplici (EJB che non sono componenti Seam) alla stregua di risorse. Invece di configurare i nomi JNDI in modo non portabile nel fileweb.xml (si veda la documentazione RESTEasy), è possibile elencare semplicemente le classi di implementazione degli EJB, non le interfacce di business, nel file components.xml, come mostrato sopra. Si noti che si deve annotare l'interfaccia @Local dell'EJB con @Path, @GET, e così via - non la classe di implementazionedel bean. Ciò permette di mantenere l'applicazione portabile rispetto al tipo di deploy con lo switch globale di Seam jndi-pattern su <core:init/>. Si noti che le risorse EJB non verrano trovate anche se la ricerca delle risorse è abilitata, bisogna sempre elencarle manualmente. Di nuovo, ciò è rilevante solo per risorse EJB che non sono anche componenti Seam e che non hanno l'annotazione @Name.

Infine, è possibile configurare le estensioni degli URI dei tipi di media e dei linguaggi:


<resteasy:application>

    <resteasy:media-type-mappings>
       <key
>txt</key
><value
>text/plain</value>
    </resteasy:media-type-mappings>

    <resteasy:language-mappings>
       <key
>deutsch</key
><value
>de-DE</value>
    </resteasy:language-mappings>

</resteasy:application
>

Questa definizione mapperebbe il suffisso dell'URI di .txt.deutsch sui valori aggiuntivi text/plain and de-DE ripettivamente degli header Accept e Accept-Language.

Qualunque istanza di risorsa e di provider è gestita da RESTEasy di default. Ciò significa che la classe di una risorsa sarà istanziata da RESTEasy e servirà una singola richiesta, dopo di ché sarà distrutta. I provider sono istanziati una sola volta per tutta l'applicazione e in effetti sono dei singletons che sono supposti stateless.

E' possibile scrivere risorse e provider come componenti Seam e trarre beneficio dalla più ricca gestione del ciclo di vita di Seam e dall'interception, dalla bijection, dalla sicurezza e così via. Occorre semplicemente rendere la classe della risorsa un componente Seam:

@Name("customerResource")

@Path("/customer")
public class MyCustomerResource {
    @In
    CustomerDAO customerDAO;
    @GET
    @Path("/{customerId}")
    @Produces("text/plain")
    public String getCustomer(@PathParam("customerId") int id) {
         return customerDAO.find(id).getName();
    }
}

Quando una richiesta raggiunge il server, un'istanza di customerResource viene ora gestita da Seam. Si tratta di un componente JavaBean di Seam con scope EVENT, quindi in nulla diverso dal ciclo di vita del JAX-RS di default. Si ottiene il completo supporto di Seam per la bijection e tutti gli altri componenti e contesti di Seam sono disponibili. Attualmente sono supportati anche i componenti Seam che sono risorse di tipo APPLICATION e STATELESS. Questi tre scope permettono di creare un'applicazione Seam middle-tier che processa le richieste HTTP in modo stateless.

Si può annotare un'interfaccia e mantenere l'implementazione libera da annotazioni JAX-RS:

@Path("/customer")

public interface MyCustomerResource {
    @GET
    @Path("/{customerId}")
    @Produces("text/plain")
    public String getCustomer(@PathParam("customerId") int id);
}
@Name("customerResource")

@Scope(ScopeType.STATELESS)
public class MyCustomerResourceBean implements MyCustomerResource {
    @In
    CustomerDAO customerDAO;
    public String getCustomer(int id) {
         return customerDAO.find(id).getName();
    }
}

E' possibile utilizzare componenti Seam con scope SESSION. Di default, la sessione sarà comunque ridotta ad una singola richiesta. In altre parole, mentre una richiesta HTTP viene processata dal codice di integrazione RESTEasy, viene creata una sessione HTTP in modo che i componenti Seam possano utilizzare tale contesto. Dopo che la richiesta è stata processata, Seam esamina la sessione e decide se è stata creata soltanto per servire quella singola richiesta (nessun identificatore di sessione è stato fornito con la richiesta o nessuna sessione esiste per la richiesta). Se la sessione è stata creata solo per servire la richiesta corrente, essa sarà distrutta al termine della richiesta!

Assumendo che l'applicazione Seam usi solo componenti evento, applicazione o stateless, questa procedura previene l'esaurimento delle sessioni HTTP sul server. L'integrazione RESTEasy di Seam assume di default che le sessioni non siano usate, poiché si aggiungerebbero sessioni anemiche all'avvio di una sessione da parte di ciascuna richiesta REST, sessione che sarà rimossa solo alla scadenza.

Se l'applicazione RESTful di Seam deve preservare lo stato della sessione fra richieste HTTP REST, occorre disabilitare questo comportamento nel file di configurazione:


<resteasy:application destroy-session-after-request="false"/>

Tutte le richieste HTTP RESTful creeranno ora una nuova sessione che sarà rimossa soltanto alla sua scadenza o per invalidazione esplicita da parte del codice dell'applicazione usando Session.instance().invalidate(). E' responsabilità dello sviluppatore passare un identificatore di sessione valido insieme a ciascuna richiesta HTTP, se si vuole utilizzare il contesto di sessione tra una richiesta e l'altra.

Risorse che siano componenti con scope CONVERSATION e la mappatura delle conversazioni su risorse e percorsi HTTP temporanei è nei piani ma non ancora supportato.

Sono supportati i componenti EJB Seam. Si annoti sempre l'interfaccia locale di business, non la classe di implementazione EJB, con le annotazioni JAX-RS. L'EJB deve essere STATELESS.

Le classi del provider possono essere componenti Seam, vengono supportati solo i componenti provider con scope APPLICATION. Si può annotare l'interfaccia bean o l'implementazione con le annotazioni JAX-RS. I componenti EJB Seam come provider NON sono attualmente supportati, solo i POJO!

La sezione 3.3.4 delle specifiche JAX-RS definisce come le eccezioni di tipo checked o unchecked debbano essere trattate dall'implementazione JAX-RS. Oltre all'utilizzo di un provider della mappatura delle eccezioni come definito dalle JAX-RS, l'integrazione di RESTEasy con Seam permette di mappare le eccezioni con i codici di risposta HTTP all'interno del file pages.xml di Seam. Se si stanno già utilizzando dichiarazioni di pages.xml, ciò è più semplice da manutenetere delle numerose potenziali classi JAX RS di mappatura delle eccezioni.

In Seam la gestione delle eccezioni richiede che il filtro di Seam intercetti le richieste HTTP. Assicuratevi di filtrare tutte le richieste nel file web.xml, e non - come mostrato in alcuni esempi di Seam - quelle corrispondenti ad URI di richiesta che non coprono i percorsi delle richieste REST. L'esempio seguente intercetta tutte le richieste HTTP e abilita la gestione delle eccezioni di Seam:


<filter>
    <filter-name
>Seam Filter</filter-name>
    <filter-class
>org.jboss.seam.servlet.SeamFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name
>Seam Filter</filter-name>
    <url-pattern
>/*</url-pattern>
</filter-mapping
>

Per convertire l'eccezione unchecked UnsupportedOperationException lanciata dai metodi delle risorse nel codice di riposta HTTP 501 Not Implemented , occorre aggiungere ciò che segue al descrittore pages.xml:


<exception class="java.lang.UnsupportedOperationException">
    <http-error error-code="501">
        <message
>The requested operation is not supported</message>
    </http-error>
</exception
>

Le eccezioni di tipo custom e di tipo checked sono gestite allo stesso modo:


<exception class="my.CustomException" log="false">
    <http-error error-code="503">
        <message
>Service not available: #{org.jboss.seam.handledException.message}</message>
    </http-error>
</exception
>

Se si verifica un'eccezione, non è necessario inviare al client un codice di errore HTTP. Seam permette di mappare l'eccezione con la redirezione ad una vista dell'applicazione Seam . Poichè questa caratteristica è tipicamente usata per i client umani (browser web) e non per i client remoti dell'API REST, occorre prestare un'attenzione particolare a mappature di eccezioni in conflitto fra loro in pages.xml.

Si noti che la risposta HTTP continua a passare attraverso il servlet container, così che è possibile apportare una mappatura aggiuntiva se nel file web.xml vi sono delle mappature <error-page>. Il codice di risposta HHTP sarebbe in questo caso mappato con una pagina HTML di errore con codice 200 OK!

Seam include una superclasse estesa per l'unit testing che agevola la creazione di test d'unità per un architettura RESTful. Si estenda la classe ResourceSeamTest per emulare i cicli richiesta/risposta HTTP:

import org.jboss.seam.resteasy.testfwk.ResourceSeamTest;

import org.jboss.seam.resteasy.testfwk.MockHttpServletResponse;
import org.jboss.seam.resteasy.testfwk.MockHttpServletRequest;
public class MyTest extends ResourceSeamTest {
   @Override
   public Map<String, Object
> getDefaultHeaders()
   {
      return new HashMap<String, Object
>()
      {{
            put("Accept", "text/plain");
      }};
   }
   @Test
   public void test() throws Exception
   {
      new ResourceRequest(Method.GET, "/my/relative/uri)
      {
         @Override
         protected void prepareRequest(MockHttpServletRequest request)
         {
            request.addQueryParameter("foo", "123");
            request.addHeader("Accept-Language", "en_US, de");
         }
         @Override
         protected void onResponse(MockHttpServletResponse response)
         {
            assert response.getStatus() == 200;
            assert response.getContentAsString().equals("foobar");
         }
      }.run();
   }
}

Questo test esegue soltanto chiamate locali, non comunica con SeamResourceServlet attraverso TCP. La richiesta mock viene passata attraverso il servlet ed i filtri Seam e la risposta è poi disponibile per asserzioni di test. L'override del metodo getDefaultHeaders() consente di impostare gli header di richiesta per ogni metodo di test nella classe di test.

Si noti che ResourceRequest deve essere eseguita in un metodo @Test o in una callback @BeforeMethod. Si può, ma non si dovrebbe eseguirla in altre callback, come @BeforeClass. (Questa è una limitazione che verrà rimossa in futuro.)

Si noti anche che gli oggetti mock importati non sono gli stessi degli oggetti mock usati in altri unit test che sono nel pacchetto org.jboss.seam.mock. org.jboss.seam.resteasy.testfwk simula richieste e risposte in modo molto preciso.