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 maven.
III. Autres frameworks REST▲
Les bibliothèques 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 finalement l'objet Java désiré. RestEasy fournit 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 autodescriptifs ;
- 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) :
...
<repository>
<id>
jboss</id>
<url>
http://repository.jboss.com/maven2/</url>
<releases>
<enabled>
true</enabled>
</releases>
<snapshots>
<enabled>
false</enabled>
</snapshots>
</repository>
...
...
<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 :
...
<dependency>
<groupId>
org.jboss.resteasy</groupId>
<artifactId>
resteasy-jaxb-provider</artifactId>
<version>
1.0.2.GA</version>
</dependency>
...
VIII. Configuration de l'application▲
<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 url 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.
@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 cœur 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.
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 postintegration-test
Afin de lancer l'application, positionnez-vous au niveau du WAR, puis lancez la commande suivante :
mvn jetty:run
Pour exécuter les tests d'intégration :
mvn install
<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 »
...
<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. Écriture▲
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.
@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é a été créée
return
Response.status
(
HttpResponseCodes.SC_CREATED).build
(
);
}
@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.
@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
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).
@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 trouvé, 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
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 vérifions ensuite que la donnée reçue a bien été mise à 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
@DELETE
@Path
(
"/{id}"
)
public
Response deleteContact
(
@PathParam
(
"id"
) int
id) {
Contact contactRemoved=
contactDB.remove
(
id);
if
(
contactRemoved==
null
)
{
//Si le contact n'est pas trouvé, on retourne le code "non trouve" ce qui est différent de "pas de contenu"
throw
new
WebApplicationException
(
Response.Status.NOT_FOUND);
}
return
Response.status
(
HttpResponseCodes.SC_NO_CONTENT).build
(
);
}
@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
// Supprime 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 :
- dourouc05
- Baptiste Wicht
- Lionel
- Alexandre
- François
N'hésitez pas à réagir sur le forum 11 commentaires