SeamFramework.orgCommunity Documentation
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.
Dal momento che i web service devono essere implementati come stateless session bean oppure come POJO, per web service conversazionali si raccomanda che fungano da facade ad un componente Seam conversazionale.
Se il web service è scritto come session bean stateless, allora è pure possibile farlo diventare un componente Seam dandogli un nome, @Name
. Ciò abilita la bijection di Seam ed altre caratteristiche che possono essere utilizzate nella classe stessa del web service.
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à:
Integrazione trasparente della configurazione e del bootstrap RESTEasy, rilevamento automatico di risorse e providers.
Gestione di richieste HTTP/REST con SeamResourceServlet, senza richiedere alcuna configurazione del servlet esterna o nel file web.xml.
Scrivere risorse come componenti Seam, con gestione completa del ciclo di vita e della interception (biJection) di Seam.
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:
L'URI inizia con il pattern mappato in web.xml
per il servlet SeamResourceServlet
, ad esempio /seam/resource
, se si seguono gli esempi comuni. Modificate questa impostazione per esporre le risorse RESTful con un diverso percorso di base. Si noti che questo cambiamento è globale e anche altre risorse Seam (ad esempio s:graphicImage
) saranno servite sotto questo percorso di base.
L'integratzione RESTEasy di Seam appende allora una stringa configurabile al percorso base, di default /rest
. Quindi, il percorso completo delle risorse sarebbe, ad esempio, /seam/resource/rest
. Si raccomanda di modificare questa stringa nella vostra applicazione. Ad esempio, si potrebbe aggiungere un numero di versione per prepararsi a futuri aggiornamenti di versione delle API REST dei vostri servizi (i vecchi client manterrebbero il vecchio URI di base): /seam/resource/restv1
.
Infine, la risorsa vera e propria è disponibile sotto il@Path
definito, ad esempio una risorsa mappata con @Path("/customer")
sarebbe raggiungibile sotto /seam/resource/rest/customer
.
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!
Si può abilitare il filtro di autenticazione per l'autenticazione HTTP Basic e Digest in components.xml
:
<web:authentication-filter url-pattern="/seam/resource/rest/*" auth-type="basic"/>
Si veda il capitolo sulla sicurezza di Seam per sapere come scrivere la routine di autenticazione.
Dopo che l'autenticazione ha avuto successo, hanno effetto le regole di autorizzazione con le annotazioni @Restrict
e @PermissionCheck
. Si può anche accedere Identity
del client, lavorare con la mappatura dei permessi, e così via. Sono disponibili tutte le caratteristiche di sicurezza di Seam per l'autorizzazione.
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.