I. Introduction

Il existe de nombreux frameworks implémentant la spécification n°311 (Java API for RESTful Web Services), le choix étant assez varié, je me suis décidé pour celui qui offrait à ce jour, la documentation la plus accessible ainsi que de nombreux exemples. Ce framework vient d'être publié en version 1.0.0.GA. Dans cet article, je vais vous expliquer comment configurer une application web exposant un service RESTful et ensuite, nous verrons comment effectuer les opérations de création, lecture, mise à jour ou suppression d'une ressource (CRUD). Cela fait longtemps que j'entends parler des vertus de ce type d'architecture, mais je n'ai jamais eu l'occasion de le mettre en place au sein d'une entreprise. La rédaction de cet article a été pour moi l'occasion de réaliser un POC (Proof Of Concept) de cette technologie.

II. Pré requis techniques

Un client subversion pour récupérer les sources et Maven ( http://maven.apache.org ) configuré pour les compiler. Afin de comprendre la configuration des plug-ins Maven, il est également nécessaire de connaître le cycle de vie de de maven

III. Autres frameworks REST

Les librairies REST les plus connues sont Jersey (SUN) qui est l'implémentation de référence, il en existe également d'autres :

  • Restlet de Noelios Technology (qui a été récemment présenté au Paris JUG)
  • CXF, de la fondation Apache
  • REST est également utilisé par Ruby on Rails et Spring 3 l'utilisera intensivement.

IV. Quelques définitions

Voici quelques mots de vocabulaire que nous utiliserons ensuite :

  • REST (REpresentational State Transfer) : Imaginé par Roy Fielding en 2000 (un des auteurs du protocole HTTP). C'est un style architectural et un ensemble de bonnes pratiques qui définissent comment utiliser les éléments standards du web que sont HTTP et URI.
  • Ressource : Toute référence à un objet pouvant être la cible d'un lien hypertexte est une ressource, dans les exemples de cet article nous ne manipulerons qu'une seule ressource qui sera « contact ». Une ressource peut-elle même référencer une autre ressource. Exemple : http://localhost:8080/monApplication/contact/1/ pointera sur la ressource contact ayant l'identifiant n°1.
  • Représentation : Une ressource est échangée avec l'appelant par le biais d'une représentation. Une première requête demandera par exemple une représentation XML du contact, puis la deuxième requête effectuée, via un client Ajax, la demandera au format JSON. Une représentation est ensemble composé d'une donnée et de métadonnées décrivant cette dernière, elle peut correspondre à la valeur instantanée de la ressource, ou bien à une valeur désirée (demande de mise à jour).
  • RestFul : Se dit d'un service exposé via HTTP respectant les contraintes REST
  • Provider : Une requête effectuée vers le serveur, utilisant une représentation du type XML ou JSON, devra être comprise par le serveur afin de la traiter pour obtenir au final l'objet Java désiré. RestEasy fourni des providers XML, JSON, YAML, Fastinfoset, Atom...

V. Contraintes REST

Une application REST doit respecter les contraintes suivantes :

  • L'application doit être sans état
  • Les erreurs doivent être standards
  • L'information doit être transmise dans une forme normalisée
  • Les ressources doivent être identifiées de manière unique
  • Les messages sont auto-descriptifs
  • Les ressources sont échangées via des représentations

Le fait de respecter quelques contraintes a les avantages suivants :

  • diminue la consommation mémoire de l'application
  • permet de répartir l'application sur plusieurs serveurs sans se soucier de persister la session client
  • mise au point plus simple, la gestion des sessions peut souvent être un écueil
  • URI sont facilement mises en cache (chaque ressource est associée à une URI unique)
  • bonne interopérabilité, il suffit de savoir lancer une requête HTTP

VI. Gestion des erreurs

Comme nous l'avons vu précédemment, les messages d'erreurs doivent être standards, les services ou ressources être exposés en HTTP, quoi de plus standard dans ce cas que d'utiliser des messages d'erreurs HTTP :

  • 1XX : Information
  • 2XX : Opération effectuée avec succès
  • 3XX : Redirection
  • 4XX : Erreur client
  • 5XX : Erreur serveur

Pour une création, l'application doit me renvoyer le code 201 (CREATED) et pour une suppression, DELETE = 204 (no content).

VII. Build avec Maven

Nous allons utiliser maven pour construire notre application. Les jars dont nous avons besoin ne sont pas dans le repository central, nous allons donc devoir ajouter le repository suivant dans le pom.xml de notre projet (dans le monde professionnel, vous devrez le faire dans votre fichier settings, ce qui est une bonne pratique Maven) :

Ajout du repository JBOSS dans le pom
Sélectionnez

...
<repository>
	<id>jboss</id>
	<url>http://repository.jboss.com/maven2/</url>
	<releases>
		<enabled>true</enabled>
	</releases>
	<snapshots>
		<enabled>false</enabled>
	</snapshots>
</repository>
...
 	
Librairie RESTeasy
Sélectionnez

...
	<dependency>
		<groupId>org.jboss.resteasy</groupId>
		<artifactId>resteasy-jaxrs</artifactId>
		<version>1.0.2.GA</version>
	</dependency>
...
	
	

Nous voulons manipuler du XML, du JSON, nous aurons donc besoin du provider JAXB :

 
Sélectionnez

...
<dependency>
	<groupId>org.jboss.resteasy</groupId>
	<artifactId>resteasy-jaxb-provider</artifactId>
	<version>1.0.2.GA</version>
</dependency>
...

VIII. Configuration de l'application

 
Sélectionnez

	<web-app>
		<display-name>Sample webapp avec RestEasy</display-name>
		
		<context-param>
		<!-- Classe standard JAXRS, liste les ressources et les providers -->
			<param-name>javax.ws.rs.Application</param-name>
			<param-value>com.cestpasdur.samples.restannuaire.resources.SampleApplication</param-value>
		</context-param>
	
		<context-param>
			<param-name>resteasy.servlet.mapping.prefix</param-name>
			<param-value>/rest</param-value>
		</context-param>
	
		<listener>
			<listener-class>org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
		</listener>
		
		<!--Servlet RESTeasy -->
		<servlet>
			<servlet-name>Resteasy</servlet-name>
			<servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
		</servlet>
		
		<!-- Les urls débutant par /rest/ seront traitées par la servlet RESTeasy-->
		<servlet-mapping>
			<servlet-name>Resteasy</servlet-name>
			<url-pattern>/rest/*</url-pattern>
		</servlet-mapping>
	</web-app>
			

IX. Commençons par l'habituel HelloWorld

Je vous propose de créer un premier service qui lorsque nous lui indiquons notre prénom, nous répondra bonjour XXXX.

 
Sélectionnez

@Path("hello")
public class HelloResource {

	@GET
	@Path("/{qui}")
	public Response echoService(@PathParam("qui") String message) {
		return Response.status(200).entity("hello "+message).build();
	}
}

http://localhost:8888/nomApplication/@Path/@PathParam

Dans cet exemple pour obtenir un hello world, il suffit d'appeler l'URL suivante :

http://localhost:8888/restsample/rest/hello/damien

Ce qui vous affichera le texte « hello damien »

@Path est le chemin d'accès à notre ressource, il est possible de le définir sur la classe elle-même ainsi que sur la méthode, dans ce cas, les path devront être concaténés.

{qui} est une variable dont nous obtiendrons ensuite la valeur via @PathParam("qui")

Afin d'indiquer au client que tout s'est bien déroulé, nous allons lui renvoyer une réponse HTTP 200, ce qui signifie que tout s'est bien passé.
Nous allons maintenant renvoyer le texte hello concaténé avec la valeur de {qui}
Nous avons vu dans ce chapitre comment exposer notre première ressource, je vous propose de rentrer un peu plus dans le coeur du sujet et donc de réaliser un CRUD couvert par un harnais de Test.

X. Tests

Après ce premier exemple simple, nous allons maintenant compliquer un peu les choses. Afin de nous assurer que tout se déroule correctement, tout au long de la mise en place de notre service REST, nous allons mettre en place des tests d'intégration. « Intégration », car l'environnement dans lequel ils vont se dérouler n'est pas bouchonné.

Nous allons via ces Tests apprendre comment fonctionne l'API. Cette approche est appelée DDL (Test Driven Learning, apprentissage mené par les tests), cela permet de tester une nouvelle API : comment elle fonctionne et ce que l'on peut attendre d'elle. Ces tests nous serviront ensuite lors des montées de version de RESTeasy.

X-A. Mise en place avec Surfire & jetty

Surfire est un plugin Maven lançant les tests unitaires de l'application pendant la phase test.

La configuration suivante désactive la phase de test, démarre jetty, exécute les tests lors de la phase « intégration test », pour finalement stopper Jetty.

Cycle de vie maven
Cycle de vie Maven

X-B. Jetty

Jetty est un container de servlet.
Jetty sera lancé sur le port 8888 lorsque nous serons dans la phase pre-integration-test pour être ensuite stoppé dans la phase post-integration-test

Afin de lancer l'application, positionnez vous au niveau du WAR, puis lancez la commande suivante :

 
Sélectionnez

mvn jetty:run
					

Pour exécuter les tests d'intégration :

 
Sélectionnez

mvn install
					
 
Sélectionnez

<plugin>
	<groupId>org.mortbay.jetty</groupId>
	<artifactId>maven-jetty-plugin</artifactId>
	<version>6.1.10</version>
		<configuration>
			<contextPath>/${artifactId}</contextPath>
			<scanIntervalSeconds>2</scanIntervalSeconds>
			<stopKey>foo</stopKey>
			<stopPort>9999</stopPort>
			<connectors>
				<connector
					implementation="org.mortbay.jetty.nio.SelectChannelConnector">
					<port>8888</port>
					<maxIdleTime>60000</maxIdleTime>
				</connector>
			</connectors>
		</configuration>
		<executions>
			<execution>
				<id>start-jetty</id>
				<phase>pre-integration-test</phase>
				<goals>
				<goal>run</goal>
				</goals>
				<configuration>
				<scanIntervalSeconds>0</scanIntervalSeconds>
				<daemon>true</daemon>
			</configuration>
			</execution>
			<execution>
				<id>stop-jetty</id>
				<phase>post-integration-test</phase>
				<goals>
					<goal>stop</goal>
				</goals>
			</execution>
		</executions>
</plugin>

X-C. Surfire

La configuration désactive tous les tests d'intégration dans la phase de test du cycle de vie de Maven, pour les relancer ensuite dans la phase « integration-test »

 
Sélectionnez

...
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<configuration>
		<skip>true</skip>
	</configuration>
	<executions>
		<execution>
			<id>surefire-it</id>
			<phase>integration-test</phase>
			<goals>
				<goal>test</goal>
			</goals>
		<configuration>
			<skip>false</skip>
		</configuration>
		</execution>
	</executions>
</plugin>
...

XI. Couche de persistance

Pour des raisons de simplicité, la couche de persistance sera simulée par une « ConcurrentHashMap »

XII. Ecriture

Nous effectuons une requête d'écriture (POST) qui va créer une ressource. Nous postons une représentation XML qui devra être acceptée par le service et nous renvoyer le code HTTP 201 une fois la création effectuée.

@Consume spécifie le type de données en entrée qui seront acceptées.

Image non disponible
Méthode du service
Sélectionnez

@POST
@Consumes("application/xml")
public Response AjouteContact(final Contact contact) throws URISyntaxException {
	contactDB.put(idCounter.getAndIncrement(), contact);
	//on retourne une réponse indiquant que l'entité à été crée
	return Response.status(HttpResponseCodes.SC_CREATED).build();
}
Test unitaire
Sélectionnez

@Test
public void addContact() throws IOException {
	PostMethod method = new PostMethod(URL_BASE + "/contact/");
	method.setRequestEntity(new StringRequestEntity(
		"<?xml version=\"1.0\"?>" + "<contact>"
		+ "<firstName>Joselyne</firstName>"
		+ "<lastName>MICHU</lastName>"
		+ "<mail>jmichu@gmail.com</mail>" + "</contact>",
		"application/xml", null));
	int status = client.executeMethod(method);
	Assert.assertEquals(HttpStatus.SC_CREATED, status);
	method.releaseConnection();
}

XIII. Lecture

Nous effectuons une requête de lecture (GET), demandant une représentation XML de la ressource. Nous devons donc vérifier que ce qui est retourné est conforme à nos attentes. Nous allons également vérifier que la ressource retournée est bien valorisée correctement. @Produces indique les types de représentation que peut retourner la méthode du service.

Image non disponible
Méthode du service
Sélectionnez

@GET
@Path("/{id}")
@Produces({"application/xml", "application/json"})
public Contact recupereContac(@PathParam("id") int id) {
	Contact retour;
	retour = contactDB.get(id);
	if (retour == null) {
		throw new WebApplicationException(Response.Status.NOT_FOUND);
	}
	return retour;
}
Test unitaire
Sélectionnez

@Test
public void recupereContactXml() throws IOException, JAXBException {
	GetMethod method = new GetMethod(URL_BASE + "/contact/0");
	method.setRequestHeader("Accept", MediaType.APPLICATION_XML);
	int status = client.executeMethod(method);
	Assert.assertEquals(MediaType.APPLICATION_XML, method.getResponseHeader("content-type").getValue());
	Assert.assertEquals(HttpStatus.SC_OK, status);
	Contact contact = fromString(Contact.class, method.getResponseBodyAsString());
	Assert.assertEquals("Joselyne", contact.getFirstName());
	Assert.assertEquals("MICHU", contact.getLastName());
	Assert.assertEquals("jmichu@gmail.com", contact.getMail());
}

XIV. Mise à jour

Nous effectuons une demande de mise à jour pour la ressource n°0, au format XML.
Nous devons recevoir le code HTTP 200 si tout s'est bien passé, le tout accompagné de la ressource au format XML que nous allons ensuite parser afin de vérifier que son contenu a bien été mis à jour. Si nous tentons de mettre à jour une ressource inexistante, nous recevrons le code HTTP 404 (bien connu).

Image non disponible
Méthode du service
Sélectionnez

@PUT
@Path("/{id}")
@Consumes({"application/xml", "text/xml", "application/json"})
@Produces({"application/xml"})
public Response updateContact(@PathParam("id") final int id, final Contact contact) {
	Contact contactToUpdate = contactDB.get(id);
	if (contactToUpdate==null)
	{
		//Si le contact n'est pas trouve, on retourne le code "non trouve" ce qui est différent de "pas de contenu"
		throw new WebApplicationException(Response.Status.NOT_FOUND);
	}
	
	contactToUpdate.setFirstName(contact.getFirstName());
	contactToUpdate.setLastName(contact.getLastName());
	contactToUpdate.setMail(contact.getMail());
	return Response.ok(contactToUpdate).build();
}
<code langage="java"><![CDATA[
Test unitaire
Sélectionnez

@Test
public void updateContact() throws IOException, JAXBException {
	PutMethod method = new PutMethod(URL_BASE + "/contact/0");
	method.setRequestEntity(new StringRequestEntity(
	"<?xml version=\"1.0\"?>" + "<contact>"
	+ "<firstName>Robert</firstName>"
	+ "<lastName>DUFOUR</lastName></contact>",
	"application/xml", null));
	int status = client.executeMethod(method);
	Assert.assertEquals(HttpStatus.SC_OK, status);
	//Nous verifions ensuite que la donnee recue a bien ete mise a jour
	Contact contactUpdated = fromString(Contact.class, method.getResponseBodyAsString());
	Assert.assertEquals("Robert", contactUpdated.getFirstName());
	Assert.assertEquals("DUFOUR", contactUpdated.getLastName());
}

XV. Suppression

Premier cas, nous tentons de supprimer une ressource existante. Si tout se déroule bien, nous recevrons le code HTTP 204 (no content) indiquant qu'il n'y a plus de contenu. Par contre, si la ressource à supprimer n'existe pas au moment de la suppression, ce sera le code d'erreur HTTP 404

Méthode du service
Sélectionnez

@DELETE
@Path("/{id}")
public Response deleteContact(@PathParam("id") int id) {
	Contact contactRemoved= contactDB.remove(id);
	if (contactRemoved==null)
	{
		//Si le contact n'est pas trouve, on retourne le code "non trouve" ce qui est different de "pas de contenu"
		throw new WebApplicationException(Response.Status.NOT_FOUND);
	}
return Response.status(HttpResponseCodes.SC_NO_CONTENT).build();
}
 
Sélectionnez

@Test
// Supprime une ressource existante
public void removeContact() throws IOException {
	DeleteMethod method = new DeleteMethod(URL_BASE + "/contact/0");
	int status = client.executeMethod(method);
	Assert.assertEquals(HttpStatus.SC_NO_CONTENT, status);
}

@Test
// Supprimes une ressources inexistante
public void removeContactInexistant() throws IOException {
	DeleteMethod method = new DeleteMethod(URL_BASE + "/contact/43");
	int status = client.executeMethod(method);
	Assert.assertEquals(HttpStatus.SC_NOT_FOUND, status);
}

XVI. Conclusion

Nous avons écrit un CRUD avec RestEasy dans cet article, les CRUD sont la « base » de beaucoup de projets, mais ne reflètent pas forcément la véritable complexité d'un projet.

Certaines choses n'ont pas été étudiées, comme la gestion des listes par exemple (pagination, offset), la mise en cache d'une ressource, l'intégration avec Spring...

Ce serait, d'ailleurs, peut-être un argument déterminant dans le choix d'un framework REST, faites vos tests.

XVII. Références

XVIII. Sources

Les sources de ce tutoriel sont disponibles ici

XIX. Remerciements

Tous mes remerciements aux relecteurs de cet article :

N'hésitez pas à réagir sur le forum 11 commentaires Donner une note à l'article (4.5)