Apache CXF è un framework sviluppato e mantenuto dalla fondazione Apache. Il suo nome deriva dalla fusione di due progetti (Celtix e Xfire) e l’obiettivo è quello di fornire delle astrazioni e strumenti di sviluppo per esporre varie tipologie si servizi web.
In questo articolo, ci concentreremo sul paradigma REST, ma vedremo inizialmente anche come sia possibile utilizzare il framework per lo sviluppo di Web Service classici, tramite l'uso di annotazioni della specifica Java per i webservices JAX-WS.
Le potenzialitá del framework si possono riassumere nelle seguenti:
- Netta separazione dall’interfaccia JAX-WS e l’implementazione
- Semplicitá di definire servizi web e client tramite annotazioni
- Alte performance
- Alta modularitá dei suoi componenti (che rende possibile esporre servizi standalone o su servlet container)
Prima di iniziare con degli esempi concreti, è bene scaricare il set di librerie dal sito ufficiale di CXF, dal quale è possibile scaricare la distribuzione ufficiale e naturalmente approfondire aspetti più avanzati non coperti nel presente articolo.
Primo esempio: SOAP Web service
Il primo esempio che vedremo ha l’obiettivo di mostrare la semplicitá di implementazione di un Web Service classico, che, nel nostro caso sará un endpoint standalone (senza container).
Seguendo le best practice di Object Oriented, iniziamo con la definizione dell’interfaccia, opportunamente annotata.
package it.html.cxf.jaxws.simple;
// imports...
@WebService
public interface HelloWorld {
String hello(@WebParam(name="text") String text);
}
Si tratta di un semplice metodo helloWorld
in cui possiamo identificare due annotazioni. @WebService
e @WebParam
. La prima è fondamentale per dire al framework di considerare l’interfaccia HelloWorld
come un insieme di metodi (in questo caso un solo metodo) da esporre come servizi. La seconda definisce invece il parametro ed il nome che verrá definito nella specifica XML del web service (WSDL).
Fatto ció, creiamo una semplice implementazione della classe:
package it.html.cxf.jaxws.simple;
// imports...
@WebService(
endpointInterface = "it.html.cxf.jaxws.simple.HelloWorld",
serviceName = "HelloWorld"
)
public class HelloWorldConcrete implements HelloWorld {
@Override
public String hello(@WebParam(name = "text") String text) {
System.out.println("Receiving text... "+text);
return "HELLO: "+text;
}
}
Fino ad ora non abbiamo usato nessuna classe di CXF, solo le annotazioni di JAX-WS. Il framework realmente interviene nella fase di pubblicazione del servizio, creando per reflection tutta l’infrastruttura logica del web service (wsdl ed xml di supporto) e pubblicando il servizio tramite servlet o altre forme.
Di seguito la classe che gestisce la pubblicazione del servizio.
package it.html.cxf.jaxws.simple.publish;
// imports...
public class HelloWorldPublisher {
public static void main(String[] args) {
System.out.println("Starting Server");
HelloWorld service = new HelloWorldConcrete();
String address = "http://localhost:9000/helloWorld";
Endpoint.publish(address, service);
}
}
Questa è una prima forma, dove non è visibile in maniera esplicita la presenza del framework CXF.
Il metodo statico Endpoint.publish(...)
si occuperá del binding del servizio alla logica applicativa prima definita e qui richiamata tramite l’istanza HelloWorld
service. La classe Endpoint
è astratta e lo stesso JavaDoc del metodo chiaramente specifica che sará il framework concreto (CXF nel nostro caso) a prendersi cura dell’implementazione:
Creates and publishes an endpoint for the specified implementor object at the given address. The necessary server infrastructure will be created and configured by the JAX-WS implementation using some default configuration. In order to get more control over the server configuration, please use the javax.xml.ws.Endpoint#create(String,Object) and javax.xml.ws.Endpoint#publish(Object) method instead.
Questo possiamo vederlo nel log del server appena avviato:
Starting Server 21-feb-2013 15:04:21 org.apache.cxf.service.factory.ReflectionServiceFactoryBean buildServiceFromClass INFO: Creating Service {http://simple.jaxws.cxf.html.it/}HelloWorld from class it.html.cxf.jaxws.simple.HelloWorld 21-feb-2013 15:04:21 org.apache.cxf.endpoint.ServerImpl initDestination INFO: Setting the server's publish address to be http://localhost:9000/helloWorld 2013-02-21 15:04:21.766:INFO:oejs.Server:jetty-8.1.7.v20120910 2013-02-21 15:04:21.811:INFO:oejs.AbstractConnector:Started SelectChannelConnector@localhost:9000
La classe ReflectionServiceFactoryBean
si occupa di costruire il servizio a partire dalla classe (e delle sue annotazioni). In maniera automatica, la classe ServerImpl
del framework viene invocata come incaricata di pubblicare l’endpoint.
Un’altra forma con cui possiamo avere maggiore controllo (e quindi usare strumenti avanzati di CXF) è la seguente, dove esplicitamente definiamo l’oggetto JaxWsServerFactoryBean
che si occuperá del binding alla stessa maniera, ma con la possibilitá di configurare sulla base delle nostre esigenze.
package it.html.cxf.jaxws.simple.publish;
// imports...
public class HelloWorldPublisher {
public static void main(String[] args) {
System.out.println("Starting Server");
HelloWorld service = new HelloWorldConcrete();
JaxWsServerFactoryBean factory = new JaxWsServerFactoryBean();
factory.setServiceClass(HelloWorld.class);
factory.setAddress("http://localhost:9000/helloWorld");
factory.setServiceBean(service);
factory.create();
}
}
Per la pubblicazione CXF fa uso del web container Jetty in modalità embedded, quindi sarà necessario importare diverse librerie (già incluse nella distribuzione di CXF), o meglio ancora gestire le dipendenze via maven.
Una volta pubblicato il servizio, per poter validare il funzionamento della classe, possiamo digitare nel browser l’indirizzo dove è presente il WSDL:
http://localhost:9000/helloWorld?wsdl
Se tutto va per il meglio dovreste vedere l’xml relativo (Nell'esempio utilizziamo come porta predefinita per jetty la porta 9000, altrimenti il container utilizzerà la usuale poorta 8080, come tomcat).
un client per testare il servizio
Per poter usare il web service, invece, dovrete creare un client che, a partire dal WSDL o dalla classe, faccia la richiesta remota. Segue un semplice esempio di come possiate farlo con CXF.
package it.html.cxf.jaxws.simple;
// imports...
public class HelloWorldUnitTest {
public static void main(String[] args) {
JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
factory.setServiceClass(HelloWorld.class);
factory.setAddress("http://localhost:9000/helloWorld");
HelloWorld client = (HelloWorld) factory.create();
String reply = client.hello("Testing it!");
System.out.println("Server said: " + reply);
System.exit(0);
}
}
Come è possibile vedere, la base per creare il cliente è la stessa usata per il server, cioé la classe JaxWsServerFactoryBean
.
Nella prossima parte definiremo un esempio basato su REST, e mostreremo come esporre servizi in modalità stand-alone (tramite il core di Jetty), e all'interno di un web-container (Tomcat)
Secondo esempio (Rest web service)
La facilità di creazione di un web service classico vista nell’esempio del paragrafo precedente si applica anche alla definizione di servizi in stile REST: anche qui una serie di annotazioni ci renderanno la vita molto facile. Prima di vedere l’esempio concreto, capiamo come convertire le operazioni CRUD nella coniugazione dei verbi HTTP
, ossia, GET
, POST
, UPDATE
e DELETE
.
@POST
: create method@GET
: read method@PUT
: update method@DELETE
: delete method
Il package javax.ws.rs
ci da la possibilitá di usare tali annotazioni per i diversi metodi di logica applicativa in modo che il framework si occupi di effettuare l’associazione in maniera trasparente.
Altre annotazioni fontamentali sono @Path
, che definisce la URI relativa a cui viene associata tale richiesta (sia a livello di classe che di metodo) e @Produces
e @Consumes
, che permette di definire il tipo di dato che il metodo è disposto a produrre o a consumare (Json, xml, testo semplice, etc).
Per capire bene, simuliamo la presenza di un semplice bean POJO che gestisca dei clienti, i cui dati sono mappati nella classe DTO Customer. Chiamiamo tale classe CustomerManager
.
package it.html.cxf.rest;
// imports...
@Path("/customermanager/")
public class CustomerManager {
static long currentId=0;
static Map customers = new HashMap();
public CustomerManager() {
initialize();
}
private void initialize() {
Customer tmp = new Customer();
tmp.setId(++currentId);
tmp.setName("Pasquale");
customers.put(tmp.getId(), tmp);
Customer tmp2 = new Customer();
tmp2.setId(++currentId);
tmp2.setName("Marco");
customers.put(tmp2.getId(), tmp2);
Customer tmp3 = new Customer();
tmp3.setId(++currentId);
tmp3.setName("Matteo");
customers.put(tmp3.getId(), tmp3);
}
@POST
@Path("/customers/")
public Response create(Customer customer) {
System.out.println("Method create");
customer.setId(++currentId);
customers.put(customer.getId(), customer);
return Response.ok(customer).build();
}
@GET
@Path("/customers/{id}/")
@Produces({"application/json", "text/xml"})
public Customer read(@PathParam("id") String id) {
System.out.println("Method read");
long idNumber = Long.parseLong(id);
Customer c = customers.get(idNumber);
return c;
}
@PUT
@Path("/customers/")
public Response update(Customer customer) {
System.out.println("Method update");
Customer c = customers.get(customer.getId());
Response r;
if (c != null) {
c.setName(customer.getName());
r = Response.ok().build();
} else {
r = Response.notModified().build();
}
return r;
}
@DELETE
@Path("/customers/{id}/")
public Response delete(@PathParam("id") String id) {
System.out.println("Method delete");
long idNumber = Long.parseLong(id);
Customer c = customers.get(idNumber);
Response r;
if (c != null) {
r = Response.ok().build();
customers.remove(idNumber);
} else {
r = Response.notModified().build();
}
return r;
}
@GET
@Path("/customers/")
@Produces({"application/json", "text/xml"})
public Collection readAll() {
System.out.println("Method readAll");
return customers.values();
}
}
La classe presenta i metodi CRUD e un metodo readAll()
(per semplicitá abbiamo omesso la presenza di un’interfaccia). Come si puó osservare abbiamo annotato ogni metodo, con il rispettivo tipo di operazione HTTP e una specifica URI. Tale URI puó presentare delle variabili (raccolte da parentesi graffe come nel caso del parametro {id}).
Nell'esempio proposto, per semplicità abbiamo simulato la base dati tramite dati in memoria, ma è decisamente semplice integrare una sorgente dati come un database, o un altro webservice. L’ideale nel caso di accesso a database sarebbe poter disporre di uno o piú DAO.
La classe Customer
è molto semplice, basicamente si tratta di un contenitore di informazioni strutturate (identificazione e nome del cliente):
package it.html.cxf.rest;
// imports...
@XmlRootElement
public class Customer {
long id;
String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String toString(){
return "#"+id+" - "+name;
}
}
Fondamentale per evitarsi un lavoro di formattazione del risultato è annotare l’intera classe con @XmlRootElement
.
L'annotazione XMLRootElement fa parte dell’astrazione che permette il binding di un oggetto a xml e viceversa. È possibile, attraverso le diverse annotazioni del package javax.xml.bind.annotation
gestire il risultato finale, quindi il nome dei diversi parametri, il loro ordine eccetera. A noi va bene il risultato di default, che è il mapping per Reflection dell’oggetto in questione.
Se volete personalizzare le operazioni di marshalling/unmarshalling, tanto in XML
quanto in JSON
, potrete configurare opportunamente tramite iniezioni di dipendenza in Spring come spiegato nella sezione dedicata ai binding della documentazione di CXF.
Nei prossimi paragrafi vedremo come sia possibile pubblicare il servizio standalone, e come poterlo fare in un container (Tomcat nel nostro caso) senza la necessità di ricorrere a nessun framework specifico.
Standalone service
Partendo dagli esempi precedenti vediamo ora come pubblicare un webservice in modalità standalone, e senza dover ricorrere a framework specifici. La pubblicazione del servizio in maniera standalone è molto simile a quanto visto nel primo esempio di web service classico.
package it.html.cxf.rest.publish;
// import...
public class CustomerManagerPublisher {
public static void main(String args[]){
JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
sf.setResourceClasses(CustomerManager.class);
sf.setAddress("http://localhost:9001/");
// NOTA: Se non definiamo questa classe come Singleton, avremmo ogni volta una nuova implementazione
sf.setResourceProvider(CustomerManager.class, new SingletonResourceProvider(new CustomerManager()));
Server myServer = sf.create();
}
}
Qui la classe che interviene è la JAXRSServerFactoryBean
che si occupa di recuperare la classe di servizio (o piú di una se è il caso) e per reflection definire tutta l’infrastruttura per poter rispondere alle varie richieste che il nostro server riceverá (in questo caso non solo GET
, ma anche il resto di metodi esposti). Di nuovo, la base è il progetto Jetty, che ci fornisce una struttura solida su cui costruire il servizio.
La cosa piú semplice sarà fare un test con il browser, tramite una richiesta GET
. Nel nostro caso abbiamo due metodi che rispondono alla GET
, precisamente:
http://localhost:9001/customermanager/customers/ # TEST del metodo readAll() http://localhost:9001/customermanager/customers/1/ # TEET del metodo get, con id=1
Il risultato della prima chiamata sarà:
{ "customer":[ {"id":1,"name":"Pasquale"}, {"id":2,"name":"Marco"}, {"id":3,"name":"Matteo"} ] }
É molto importante che la richiesta contenga un header di tipo accept
che definisca il tipo di ritorno. Se mandiamo una richiesta normale (senza header specifico), verrà restituito il primo tipo definito dall’annotazione @Produces
(quindi nel nostro caso JSON
). Per forzare e manipolare le richieste (e quindi provare anche gli altri metodi) vi consigliamo di usare qualche plugin. Per questo esempio abbiamo usato in particolare Poster.
L’invocazione dei metodi PUT
o POST
(Update o Insert nella nostra logica) ha la necessitá di passare lo stesso tipo di dato per costruire la rappresentazione dell’oggetto Customer
, quindi dovrete passare come parametro della chiamata una sorta di allegato simile a quello che segue:
{ "customer":{ "id":1, "name":"Pasquale Modificato" } }
Tomcat service
L’uso di servizi standalone non è affatto una pratica poco comune, anzi è consigliabile per limitare l’uso di risorse del web server alla sola pubblicazione di servizi. Ad ogni modo, molto piú spesso può essere necessario pubblicare i servizi all'interno di un container: sia per la presenza di altri servizi, sia per riutilizzare configurazioni e librerie già esistenti (o per usare logiche di divisione di network piú avanzate e/o con balancer).
Senza tirare quindi in ballo Spring o altri framework, cerchiamo di capire come poter pubblicare un servizio fatto tramite CXF in Tomcat.
Tutto è fatto tramite descrittore di configurazione xml (web-inf/web.xml
):
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>CXFNonSpringJaxrsServlet</servlet-name>
<servlet-class>org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet</servlet-class>
<init-param>
<param-name>jaxrs.serviceClasses</param-name>
<param-value>it.html.cxf.rest.CustomerManager</param-value>
</init-param>
<init-param>
<param-name>jaxrs.extensions</param-name>
<param-value>
xml=application/xml
json=application/json
</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>CXFNonSpringJaxrsServlet</servlet-name>
<url-pattern>/*</url-pattern>
<!--
URL Base: http://localhost:8080/CXF-War-Container/*
for instance http://localhost:8080/CXF-War-Container/customermanager/customers/
-->
</servlet-mapping>
</web-app>
Il file in precedenza deve essere usato per una web application (un war quindi) che è praticamente vuoto, a meno di questo file e delle librerie che dovremo includere - le stesse librerie viste nel primo esempio (web-inf/lib
).
La pubblicazione del servizio giá sviluppato in precedenza si limita alla configurazione di una servlet, CXFNonSpringJaxrsServlet
, chè è presente nel package CXF. Basterà specificare la classe di servizio tramite il parametro jaxrs.serviceClasses
. Sarà possibile configurare il servizio in maniera piú complessa, utilizzando i diversi parametri di configurazione della stessa servlet.
Con questa configurazione di base, il servizio sarà disponibile per essere usato direttamente da Tomcat. Per poterlo testare, possiamo fare le stesse chiamate viste in precedenza, preoccupandoci di cambiare la porta a cui il server risponde (8080, default di Tomcat) e l’URI, che adesso dovrá avere anche il percorso della web-application.
http://localhost:8080/CXF-War-Container/customermanager/customers/ #TEST del metodo readAll() http://localhost:8080/CXF-War-Container/customermanager/customers/1/ # TEST del metodo get, con id=1
Il resto dei metodi funzionerà alla stessa maniera, sarà la servlet di base a prendersi cura del binding delle URI e delle funzioni definite nella classe di logica applicativa sviluppata in precedenza (un ottimo esempio di scarso accoppiamento tra componenti).