Téléchargement et installation des outils
Pour suivre ce tutorial, vous aurez besoin des outils et api suivantes :Une fois téléchargées et installée, créez un nouveau Web Application Project :
Ajoutez les librairies Jersey suivante dans le répertoire war/WEB-INF :
- asm.jar
- commons-validator.jar
- jackson-core-asl.jar
- jackson-jaxrs.jar
- jackson-mapper-asl.jar
- jackson-xc.jar
- jersey-client.jar
- jersey-core.jar
- jersey-json.jar
- jettison.jar
- jsr311-api.jar
Mise en place du modèle de données
Dans le répertoire src/META-INF du projet, nous allons remplacer le fichier de configuration JDO créé par défaut par un fichier de configuration JPA persistence.xml :<?xml version="1.0" encoding="UTF-8" ?> <persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"> <persistence-unit name="transactions-optional"> <provider> org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider </provider> <properties> <property name="datanucleus.NontransactionalRead" value="true"/> <property name="datanucleus.NontransactionalWrite" value="true"/> <property name="datanucleus.ConnectionURL" value="appengine"/> </properties> </persistence-unit> </persistence>
Nous nous contenterons de la configuration JPA minimale.
Notre entité persisté sera une classe User :
package net.androgames.blog.sample.rest.server; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import org.datanucleus.jpa.annotations.Extension; @Entity public class User implements Serializable { /** * 1 : Version initiale */ private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Extension(vendorName="datanucleus", key="gae.encoded-pk", value="true") private String id; private String nom; private String prenom; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public String getPrenom() { return prenom; } public void setPrenom(String prenom) { this.prenom = prenom; } }
Mise en place de Jersey et CRUD simple
Pour exposer les fonctionnalités de création, suppression, modification et récupération en REST, nous allons utiliser l'api Jersey. Comme la plupart des framework utilisés dans une application web, sa configuration s'effectue en déclarant une Servlet spécifique dans le fichier web.xml :<?xml version="1.0" encoding="utf-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>Jersey</servlet-name> <servlet-class> com.sun.jersey.spi.container.servlet.ServletContainer </servlet-class> <!-- Packages a analyser --> <init-param> <param-name>com.sun.jersey.config.property.packages</param-name> <param-value>net.androgames.blog.sample.rest.server</param-value> </init-param> <!-- Mapping JSON POJO --> <init-param> <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>
La sérialisation / désérialisation des POJO par Jersey sera effectuée par la classe com.sun.jersey.api.json.POJOMappingFeature. Jersey va ainsi supporter la sérialisation / désérialisation au format JSON. L'implémentation de notre CRUD est la suivante :
package net.androgames.blog.sample.rest.server; import java.util.List; import java.util.logging.Logger; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import com.sun.jersey.api.NotFoundException; @Path("/user") @Produces("application/json") @Consumes("application/json") public class UserService { private static final Logger log = Logger.getLogger(UserService.class.getName()); private static final EntityManagerFactory ENTITY_MANAGER = Persistence.createEntityManagerFactory("transactions-optional"); public static EntityManager getEntityManager() { return ENTITY_MANAGER.createEntityManager(); } /** * Mise a jour d'un utilisateur par son id * @param id * @param user * @return */ @POST @Path("{id}") public User update( @PathParam("id") String id, User user) { log.info("Mise a jour du user d'id : " + id); if (user == null) { throw new IllegalArgumentException(); } EntityManager em = getEntityManager(); User persistedUser = em.getReference(User.class, id); if (persistedUser == null) { throw new NotFoundException(); } persistedUser.setNom(user.getNom()); persistedUser.setPrenom(user.getPrenom()); em.getTransaction().begin(); em.merge(persistedUser); em.getTransaction().commit(); return persistedUser; } /** * Recupere un utilisateur par son id * @param deviceId * @return */ @GET @Path("{id}") public User get(@PathParam("id") String id) { log.info("Recuperation du user d'id : " + id); EntityManager em = getEntityManager(); User persistedUser = em.getReference(User.class, id); if (persistedUser == null) { throw new NotFoundException(); } return persistedUser; } /** * Recuperation de la liste des utilisateurs * @param deviceId * @return */ @GET @SuppressWarnings("unchecked") public List<User> list() { log.info("Recuperation des utilisateurs"); EntityManager em = getEntityManager(); List<User> users = em.createQuery("SELECT u FROM User u").getResultList(); return users; } /** * Supprime un utilisateur par son id * @param deviceId * @return */ @DELETE @Path("{id}") public String delete(@PathParam("id") String id) { log.info("Suppression du user d'id : " + id); EntityManager em = getEntityManager(); User persistedUser = em.getReference(User.class, id); if (persistedUser == null) { throw new NotFoundException(); } em.getTransaction().begin(); em.remove(persistedUser); em.getTransaction().commit(); return id; } /** * Ajoute un utilisateur * @param deviceId * @return */ @PUT public User add(User user) { log.info("Ajout d'un utilisateur"); EntityManager em = getEntityManager(); em.getTransaction().begin(); em.persist(user); em.getTransaction().commit(); return user; } }
Jersey se configure au moyen d'annotations :
- @Path
- Cette annotation permet de spécifier le chemin d'accès à la ressource relativement au chemin paramétré dans le fichier web.xml pour la Servlet Jersey. Utilisée sur la déclaration d'une classe, elle s'applique à toutes ses méthodes. Utilisée sur une méthode, elle s'applique relativement au Path définit pour la classe.
- @PathParam
- Elle permet de récupérer une partie du Path en paramètre d'une méthode. Dans notre exemple, nous déclarons le Path "/user/{id}" pour la méthode get. Ainsi définit, notre Path nous permet de récupérer la partie mappée par "{id}" grâce à l'annotation PathParam.
- @PUT, @POST, @GET, @DELETE
- Ces annotations permettent de spécifier la méthode HTTP à mapper sur une méthode de la classe UserService. Seules les requêtes du type spécifié seront transmises à la méthode. Cela permet d'utiliser le même chemin "/user" pour la création d'un utilisateur (PUT), la récupération de tous les utilisateurs (GET) et la suppression d'un utilisateur (DELETE). La récupération d'un utilisateur (GET) et la imse à jour d'un utilisateur (POST) partagent le chemin "/user/{id}".
- @Produce
- Cette annotation permet de préciser le (ou les) type MIME que Jersey peut utiliser pour sérialiser les retours des méthodes annotées PUT, GET, POST, DELETE, ... Dans notre exemple, nous avons spécifier une sérialisation en JSON pour l'ensemble des méthodes de la classe. Il est également possible de spécifier le type MIME par version, ou d'en spécifier plusieurs séparés par des virgules. Dans ce cas, le type MIME utilisé pour la sérialisation correspond au premier type MIME rencontré dans le header HTTP Accept.
- @Consume
- Tout comme l'annotation Produce, cette annotation permet de définir un type MIME et, plus précisément, le type MIME qui sera utiliser par Jersey pour désérialiser les valeurs à passer en paramètre aux méthodes exposées. Dans notre exemple, nous attendons donc pour les méthodes add et update une requête HTTP nous envoyant un bean User sérialisé en JSON. Il est également possible de spécifier le type MIME par méthode ou d'en préciser plus d'un.
Test des services avec l'API de test Jersey
Jersey offre la possibilité de recetter rapidement un webservice REST en utilisant la classe Client de son API. L'exemple ci dessous montre comment tester rapidement l'ajout, la mise à jour, la récupération et la suppression d'un utilisateur.
package net.androgames.blog.sample.rest.server; import javax.ws.rs.core.MediaType; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.WebResource; public class TestUserService { private static final String URL = "http://localhost:8888/user"; /** * * @param args */ public static void main(String[] args) { // creation d'un client Jersey Client c = Client.create(); WebResource r; User user; // test d'insertion d'un utilisateur r = c.resource(URL); user = new User(); user.setNom("Vianey"); user.setPrenom(""); user = r.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE) .put(User.class, user); System.out.println("User enregistré avec l'id : " + user.getId()); // test de mise a jour de l'utilisateur r = c.resource(URL + "/" + user.getId()); user = new User(); user.setNom("Vianey"); user.setPrenom("Antoine"); user = r.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE) .post(User.class, user); System.out.println("User mise a jour : " + user.getPrenom() + " " + user.getNom()); // test de recuperation de l'utilisateur r = c.resource(URL + "/" + user.getId()); user = r.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE) .get(User.class); System.out.println("User récupéré : " + user.getPrenom() + " " + user.getNom()); // test de suppression des utilisateurs r = c.resource(URL + "/" + user.getId()); r.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE) .delete(); } }
Pour le test de récupération de la liste des utilisateurs, je n'ai malheureusement pas trouver de méthode élégante pour désérialiser directement le type List
Pour que la classe de test fonctionne effectivement :
RépondreSupprimerAu lieu de r.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE)
.toto();
Faire :
ObjectMapper mapper = new ObjectMapper();
s = mapper.writeValueAsString(user); resJson = r.type(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE)
.toto(String.class, s);
user = mapper.readValue(resJson, User.class);