mercoledì 18 aprile 2012

Web services REST: tutorial ed esempi

In questo periodo sto studiando i Web Services di tipo REST.
Dopo la prima fase di studio e comprensione del tema, mi sono stupito della semplicità con cui sia possibile mettere rapidamente in piedi dei WebServices di tipo RESTFull.

Il tutorial che vedremo presuppone l’utilizzo di un ambiente Java 1.6, Tomcat 7 e il framework Jersey di Glassfish che è una delle implementazioni più diffuse della JSR-311 relativa appunto ai Web Services RESTFull (ovvero che seguono le specifiche architetturali di tipo REST).

Non vorrei dilungarmi troppo nel definire l’architettura REST dato che in coda, tra i riferimenti, c’è abbastanza documentazione da rendere ridondante qualunque cosa io possa decidere di scrivere in questa sede.
Per semplicità di lettura dell’articolo richiamiamo soltanto alcuni concetti fondamentali che potranno risultare utili per una rapida comprensione durante la lettura:

  • All’interno di un’architettura REST ogni oggetto con cui si interagisce è una “Risorsa” identificata univocamente da una URI (quindi nel caso di un WebServices avremo sempre URL distinte e ben precise)
  • Un’architettura REST abbraccia i concetti legati al protocollo HTTP ovvero i quattro comandi fondamentali che consentono di eseguire le operazioni CRUD (Create, Read, Update e Delete):
    • POST - crea nuovi dati
    • GET - visualizza i dati
    • PUT - modifica i dati
    • DELETE - cancella i dati
  • Un WebServices RESTFull deve garantire l’adesione ai seguenti vincoli tipici dell’architettura REST:
    • Interazione di tipo Client/Server
    • Comunicazione di tipo stateless
    • Possibilità di implementare meccanismi di cache sulle richieste
    • Possibilità di costruire ulteriori layer applicativi in base alle esigenze a partire dal servizio di base.

Con questi concetti in mente passiamo quindi a vedere il nostro esempio.



Progetto e configurazione



Ho creato un Dynamic Web Project utilizzando Eclipse e avendo cura di caricare tutti i jar del framework Jersey di Glassfish all’interno della cartella WEB-INF/lib. (Scarica solo i JAR di Jersey, oppure vai al sito di riferimento).

Per rendere operativo il Jersey framework dovrete modificare il vostro web.xml aggiungendo il mapping della ServletContainer di Jersey:




1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<servlet>
    <servlet-name>Jersey REST Service</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>it.lcianci.test.rest.webservice</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Jersey REST Service</servlet-name>
    <url-pattern>/rest/*</url-pattern>
  </servlet-mapping>

Il parametro definito nel nostro file contiene il nome del package all’interno del quale andremo a scrivere i nostro WebServices.
Questa configurazione ci dice che la ServletContainer di Jersey è un Front Controller che andrà ad intercettare tutte le chiamate che presentano “rest/” all’interno della url.

Il primo WebService

Giusto per riscaldarci, creiamo un semplice WebService che risponde “Ciao Mondo”.
Che ci crediate o no, vi basta semplicemente scrivere una classe come questa:



/**
* Questa classe rappresenta un WebService di tipo REST
* Grazie alle annotation definisco il path con il quale lo andrò ad invocare (@Path)
* e il tipo di chiamata che andrò ad intercettare (@Get)
* Vado anche a definire con @Produces il Content-type da restituire in base
* ad una richiesta specifica.
*
* In questo caso ho 3 metodi che restituiscono testo, xml o html a seconda di cosa
* viene richiesto. La chiamata da browser viene sempre gestita con il metodo
* per HTML
*
* @author lcianci
*
*/
@Path("/simple")
public class SimpleService {

@GET
@Produces(MediaType.TEXT_PLAIN)
public String sayPlainTextHello() {
return "Servizio base: Ciao mondo versione 1.0";
}

@GET
@Produces(MediaType.TEXT_XML)
public String sayXMLHello() {
return "" + " Servizio base: Ciao mondo versione 1.0" + "";
}

@GET
@Produces(MediaType.TEXT_HTML)
public String sayHtmlHello() {
return "
"+ "Servizio base: Ciao mondo versione 1.0" + "
";
}

}



Le annotation di JAX-WS mi permettono di definire rapidamente il comportamento dei vari metodi e quando questi devono entrare in gioco. In particolare:


  • @Path, definisce la url relativa per richiamare il nostro WebServices
  • @GET, definisce che quel particolare metodo deve essere eseguito a fronte di una chiamata in GET del nostro WebService
  • @Produces, definisce il tipo di output che daremo in risposta alla chiamata


Se lanciamo tomcat, possiamo chiamare il nostro web service anche da browser usando il seguente indirizzo: http://localhost:8080/RESTJerseyWS/rest/simple/



  • RESTJerseyWS è il nome della mia WebApp
  • rest è il mapping che ho impostato per la JerseyServlet
  • simple è la url relativa che abbiamo definito con la annotation @Path



Tutto chiaro?


Passiamo ora a qualcosa di più significativo per un tutorial che si rispetti.



Anagrafica Utenti: il modello dati



Ho realizzato un WebService di test che permette di interagire con una ipotetica anagrafica utenti.
Le mie classi POJO per la gestione dell’anagrafica sono la classe User e la classe Address e utilizzano al loro interno delle semplici annotation di JAXB che permettono il bind automatico in formato XML o JSON semplicemente restituendo i POJO come risposta del nostro WebService:



/*
* Questa annotazione di JAXB permette il binding automatico della mia classe in formato XML o JSON usando
* il nome classe e i nomi di variabile per generare i tag dell'xml in output (o del json)
*/
@XmlRootElement
@XmlType(propOrder = { "id", "surname", "name", "address", "otherAddress" })
public class User {

private Integer id;
private String name;
private String surname;
private Address address;

private ArrayList otherAddress;

public User() {}

public User(Integer id, String nome, String surname) {
this.id = id;
this.name = nome;
this.surname = surname;

otherAddress = new ArrayList();
}

public void setId(Integer id) {
this.id = id;
}

public Integer getId() {
return id;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setSurname(String surname) {
this.surname = surname;
}
public String getSurname() {
return surname;
}
public void setAddress(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}

@XmlElementWrapper(name = "otherAddress")
@XmlElement(name = "address")
public void setOtherAddress(ArrayList otherAddress) {
this.otherAddress = otherAddress;
}
public ArrayList getOtherAddress() {
return otherAddress;
}

}

@XmlRootElement
@XmlType(propOrder = { "place", "cap", "city" })
public class Address {

private String place;
private Integer cap;
private String city;

public Address() {}

public Address(String place, Integer cap, String city) {
this.place = place;
this.cap   = cap;
this.city  = city;
}

public void setPlace(String place) {
this.place = place;
}
public String getPlace() {
return place;
}
public void setCap(Integer cap) {
this.cap = cap;
}
public Integer getCap() {
return cap;
}
public void setCity(String city) {
this.city = city;
}
public String getCity() {
return city;
}

}


Per il momento fidatevi delle Annotation JAXB e andiamo avanti. Spiegheremo dopo a cosa servono e quando entrano in gioco.

Per semplicità di gestione di questa anagrafica realizziamo una semplice classe mock che simula un DAO per l’accesso ai dati usando una Enumeration:


/**
* Questa è una classe MOCK-DAO realizzata come oggetto enumeration.
*
* @author lcianci
*
*/
public enum UserDAO {
myDatabase;

private Map mySchema = new HashMap();

private UserDAO() {

ArrayList al = new ArrayList();
al.add(new Address("Via ABC 123", 20100, "Milano2")); al.add(new Address("Via ABC 123", 20100, "Milano3")); User user = new User(1, "Luca", "Cianci"); user.setAddress(new Address("Via ABC 123", 20100, "Milano")); user.setOtherAddress(al); mySchema.put("1", user);
user = new User(2, "Luca", "Cianci"); user.setAddress(new Address("Via DEF 456", 20100, "Milano")); user.setOtherAddress(al); mySchema.put("2", user);
}
public Map getModel(){ return mySchema; } }

Anagrafica Utenti: il WebService
A questo punto non ci resta che creare la nostra classe che rappresenta il WebService, chiamiamola UsersDataService:

/** * Questo WebService mi permette di gestire una ipotetica anagrafica utenti * con le relative interfacce per le operazioni di CRUD * * @author lcianci * */
@Path("/users") public class UsersDataService {
/* * Questa annotation @Context mi permette di caricare all'interno della classe
* oggetti esterni come ad esempio: ServletContext, Request, Response, UriInfo
*/
@Context
UriInfo uriInfo;
@Context
Request request;
/*
* Questi due metodi che seguono restituiscono la lista degli utenti
* in anagrafica in formato testo, tipicamente per il browser o in formato
* XML/JSON per chiamate applicative
*/
@GET
@Produces(MediaType.TEXT_XML)
public List getUsersBrowser() {
List users = new ArrayList();
users.addAll( UserDAO.instance.getModel().values() );
return users;
}
@GET
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public List getUsers() {
List users = new ArrayList();
users.addAll( UserDAO.instance.getModel().values() );
return users;
}
/*
* Questo metodo si può pensare come un metodo di interfaccia
* esposto all'esterno per chi chiama il WebService.
* Chiamando "../rest/users/count" viene restituito direttamente
* il numero di utenti in anagrafica
*/
@GET
@Path("count")
@Produces(MediaType.TEXT_PLAIN)
public String getCount() {
int count = UserDAO.instance.getModel().size();
return String.valueOf(count);
}
/*
* Questo metodo consente il salvataggio di nuovi record
* a fronte di una chiamata POST attraverso il form di inserimento
* dati
*/
@POST
@Produces(MediaType.TEXT_HTML)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void newUser(
@FormParam("id") String id,
@FormParam("name") String name,
@FormParam("surname") String surname,
@FormParam("place") String place,
@FormParam("cap") String cap,
@FormParam("city") String city,
@Context HttpServletResponse servletResponse
) throws IOException {
User user = new User(new Integer(id),name,surname);
user.setAddress(new Address(place,new Integer(cap),city));
UserDAO.instance.getModel().put(id, user);
servletResponse.sendRedirect("../create_user.html");
}
/*
* Questo metodo mi permette di definire un path parametrico rappresentato
* in questo caso dall'id utente seguito dal path principale del mio
* web service. Quindi in particolare "../rest/users/1" restituirà l'anagrafica
* del primo utente passando attraverso il controllo dello UserDataService che è
* un WebService interno alla mia applicazione e non esposto come end-point
*/
@Path("{id}")
public UserDataService getUser(@PathParam("id") String id) {
return new UserDataService(uriInfo, request, id);
}
}


In pratica il nostro WebService non è altro che una semplice classe java con dei metodi annotati. 
Potete vedere come i metodi sono annotati con @GET, @POST, @PUT e @DELETE. Questo ci permette di definire con quale tipo di chiamate devono entrare in gioco.
Guardando il codice nulla lascia pensare ad un servizio remoto. I metodi restituiscono semplici oggetti appartenenti al mio modello dati.
Infine ecco il codice per il WebService “di servizio” non esposto all’esterno e invocato nell’ultimo metodo della precedente classe:
/**
* Questa classe rappresenta il WebService per la gestione CRUD
* sui singoli oggetti User ma di fatto non viene però esposto all'esterno come
* WebService di end point dato che non definiamo il path con cui richiamarlo.
* Sarà lo UsersDataService ad invocarlo all'occorrenza.
*
* @author lcianci
*
*/

public class UserDataService {
@Context
UriInfo uriInfo;
@Context
Request request;
String id;
public UserDataService(UriInfo uriInfo, Request request, String id) {
this.uriInfo = uriInfo;
this.request = request;
this.id = id;
}
@GET
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public User getUser() {
User user = UserDAO.instance.getModel().get(id);
if(user==null)
throw new RuntimeException("Get: User con " + id +  " non presente in anagrafica");
return user;
}

@GET
@Produces(MediaType.TEXT_XML)
public User getTodoHTML() {
User user = UserDAO.instance.getModel().get(id);
if(user==null)
throw new RuntimeException("Get: User con " + id +  " non presente in anagrafica");
return user;
}
@PUT
@Consumes(MediaType.APPLICATION_XML)
public Response putUser(JAXBElement user) {
User c = user.getValue();
return putAndGetResponse(c);
}

@DELETE
public void deleteUser() {
User c = UserDAO.instance.getModel().remove(id);
if(c==null)
throw new RuntimeException("Delete: User con " + id +  " non presente in anagrafica");
}
private Response putAndGetResponse(User user) {
Response res;
if(UserDAO.instance.getModel().containsKey(user.getId().toString())) {
res = Response.noContent().build();
} else {
res = Response.created(uriInfo.getAbsolutePath()).build();
}
UserDAO.instance.getModel().put(user.getId().toString(), user);
return res;
}
}

Da questo punto di vista, il WebService esposto, se progettato bene, svolge egregiamente la funzione di FrontController dato che intercetta le varie URL che è possibile chiamare e le inoltra all’interno verso altre classi specializzate.
Il nostro codice è completo.

Anagrafica utenti: Il binding con JAXB
Torniamo adesso alle annotation JAXB per il binding in formato XML/JSON.
Notate come i metodi del WebService principale restituiscano direttamente oggetti User o Liste di User. Grazie alle annotation JAXB presenti nel POJO questi oggetti vengono automaticamente convertiti in uno stream XML o JSON a seconda della specifica richiesta che andiamo a fare. Questo è possibile dato che JAX-WS e JAXB lavorano in coppia rendendo trasparente l’operazione di trasformazione.

Le annotation utilizzate sono abbastanza semplici e basilari:
  • @XmlRootElement, definisce l’elemento root del mio tracciato XML per un determinato POJO.
  • @XmlType(propOrder = { "id", "surname", "name", "address", "otherAddress" }), permette di definire l’ordine di visualizzazione dei campi del POJO quando effettuiamo il binding. In mancanza di questa annotation il default è l’ordinamento alfabetico.
  • @XmlElementWrapper(name = "otherAddress"), definisce un tag “wrapper” per rendere più leggibile gli oggetti Address presenti all’interno del vettore otherAddress.
  • @XmlElement(name = "address"), definisce il tag che identifica i singoli elementi all’interno del tag wrapper precedentemente definito.
In alternativa, se voleste usare solo JAXB in assenza di JAX-WS, diciamo ad esempio all’interno di un WebService scritto con Axis o CXF, dovreste usare il seguente codice per poter ottenere il bind della classe User in XML:
JAXBContext context = JAXBContext.newInstance(User.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
m.marshal(new User(“Luca”,”Cianci”), System.out);

Eccezione CXF: la variabile address manda il binding in errore perchè viene vista come parola riservata, in quel caso ho dovuto cambiare il nome da “address” in “principalAddress”.
Anagrafica utenti: un semplice client
I metodi annotati in GET possono essere invocati direttamente attraverso il browser, per chiudere il cerchio ci serve avere un client più generico in grado di contattare il nostro WebService “users”.
Nella sua versione primordiale possiamo usare degli oggetti inclusi nell’API di Jersey e quindi usare un codice simile al seguente:

public class TestClient {
public static void main(String[] args) {
ClientConfig config       = new DefaultClientConfig();
Client client                   = Client.create(config);
WebResource service = client.resource(getBaseURI());
WebResource restWS = service.path(“rest”);
// Inserisco un nuovo utente
User user = new User();
user.setId(3);
user.setName("Luca");
user.setSurname("Cianci");
user.setAddress(new Address("Via XYZ 999", 96100, "Siracusa"));
ClientResponse response = restWS.path(USERS).path(user.getId().toString()).accept(MediaType.APPLICATION_XML).put(ClientResponse.class, user);
// Recupero la lista di tutti gli utenti in XML e JSON
System.out.println(restWS.path(USERS).accept(MediaType.TEXT_XML).get(String.class));
System.out.println(restWS.path(“users”).accept(MediaType.APPLICATION_JSON).get(String.class));
// Recupero i dati dell’utente con ID 1 in XML
System.out.println(restWS.path("users/1").accept(MediaType.APPLICATION_XML).get(String.class));
// Cancello l’utente con ID 3 inserito in precedenza
restWS.path(USERS+"/3").delete();
}

Ovviamente in una ipotetica Web Application vi occuperete di generare dei form che aggiungono o modificano i dati in POST e avremo dei tasti che consentiranno di cancellare i record non più utili magari invocando una classe che gestisce un HTTPClient di Apache o similare.
Questo semplice esempio di client serve solo come traccia.
Per semplicità, chi fosse interessato può scaricare l’intero progetto WAR con i sorgenti di questo tutorial, con anche qualcosa in più rispetto a quanto descritto, da questo link: scarica il sorgente del tutorial rest.
Direi che questo è tutto.

Alla prossima.
Riferimenti:

4 commenti:

  1. Ciao, credo che quando parli di JAX-WS tu intenda JAX-RS?

    RispondiElimina
  2. Ciao, ottimo esempio. Io, però avrei bisogno di fare un client che si connette ad un server via https con mutua autenticazione.
    Si può fare con jersey? Come?
    Grazie

    RispondiElimina
  3. Vorrei un consiglio devo entrare in un posto di lavoro in ambito finance e mi hanno chiesto di fare questo:
    Creare una web-app in spring deployata in Jboss 4.2 che esponga dei
    servizi rest.
    I servizi rest devono:
    - lanciare un job che legge un json (in allegato alla mail) e lo converta
    in xml (sintassi vedi tu) ed execel (preferibilmente xls ordinato per
    data di pubblicazione)
    - servizio che restituisca in pagina xml (json convertito)
    - servizio che splitta il json per data di pubblicazione e salvi i dati
    in diversi xml nominati con la data di

    Io non so come iniziare. Sapete darmi un suggerimento?Grazie.

    RispondiElimina