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
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
@WebService
@WebParam
HelloWorld
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:
La classe ReflectionServiceFactoryBean
ServerImpl
Un’altra forma con cui possiamo avere maggiore controllo (e quindi usare strumenti avanzati di CXF) è la seguente, dove esplicitamente definiamo l’oggetto JaxWsServerFactoryBean
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
@GET
@PUT
@DELETE
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
javax.xml.bind.annotation
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
GET
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:
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
@Produces
JSON
Poster
L’invocazione dei metodi PUT
POST
Customer
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.
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).