Version PDF
Transcription
Version PDF
Services REST en JEE (JAX-RS) 1 Présentation Pour connecter un client à un serveur métier, nous avons déjà vu trois solutions : Spring, RMI, EJB. Dans les trois cas, le client et le serveur doivent être développés sur la même plateforme (Java). 1.1 Les services WEB Nous avons maintenant besoin de généraliser cette connexion et d’améliorer l’hétérogénéité de nos solutions. Pour ce faire, nous allons introduire la notion de Services WEB 1 (Web Services) qui consiste à utiliser des documents XML pour coder les requêtes et les réponses qui transitent entre le client et serveur via le protocole HTTP : +--------------------+ | | | Client | | | +--------------------+ ----> Requ^ etes ----> <---- Réponses <---- +--------------------+ | | | Serveur | | | +--------------------+ Ces WS peuvent publier des données et/ou permettre aux clients de modifier les données métiers. Ces WS échangent souvent des documents XML sur la base du protocole Soap 2 . Le protocole SOAP est géré dans JEE par l’API JAX-WS 3 . 1.2 Les services REST Depuis quelques années, les WS se démocratisent et se simplifient. Afin d’éviter le recours à XML, des auteurs ont suggéré d’utiliser toute la puissance du protocole HTTP pour coder les requêtes en utilisant à la fois : • la méthode HTTP ( GET , POST , PUT , DELETE , ...) pour coder la nature de la requête, • les paramètres HTTP placés dans l’entête ou même dans l’URI pour préciser les arguments de la requête. Par exemple GET http://monservice.fr/ws/produit/A255/prix/euros pour obtenir le prix en euros du produit identifié par le code A255 . Le résultat peut être codé en mode texte, en XML ou par le langage JSON 4 . Nous venons de définir les services REST 5 (representational state transfer). Ils sont gérés dans JEE par l’API JAX-RS 6 . 1.3 Utilisation des Web Services REST/SOAP Les API JAX-WS et JAX-RS s’utilisent à la fois dans • un environnement JRE classique (hors JEE) 1. 2. 3. 4. 5. 6. https://fr.wikipedia.org/wiki/Service web https://fr.wikipedia.org/wiki/SOAP https://docs.oracle.com/javaee/6/tutorial/doc/bnayl.html https://fr.wikipedia.org/wiki/JavaScript Object Notation https://fr.wikipedia.org/wiki/Representational State Transfer https://docs.oracle.com/javaee/6/tutorial/doc/giepu.html 1 Extérieur | Intérieur ----------------------+------------------------------| Client Ws -----> Application JRE/Rest/Soap | • un environnement JEE/EJB (sans couche Web) Extérieur | Applicatif | Métier --------------+--------------------------------------| | Utilisateur -----> App. client Ws -----> JEE/EJB/Rest/Soap | (Natif/JSF/Servlet) | • un environnement JEE/JSF ou JEE/Servlet (pas d’EJB) Extérieur | Web & Métier ------------------+---------------------| Client Web & Ws -----> JSF/JSP/Rest/Soap | • un environnement JEE/JSF/EJB ou JEE/Servlet/EJB Extérieur | Web | Métier ------------------+--------------------------------------| | Client Web & Ws -----> PHP/.NET/JSF/JSP -----> EJB/Rest/Soap | | 1.4 Quelques liens • Tutorial JAX-RS JEE˜6 7 • Une série d’exemples 8 2 Mise en place • Préparez dans Eclipse JEE un projet de type Application WEB basé sur le serveur TomEE Plus. • Nous allons utiliser la librairie Apache CXF 9 qui est intégrée à TomEE Plus. Elle est également utilisable de manière autonome. • Activez la fonction Maven de votre projet. 3 Un premier Service REST • Comme habituellement dans JEE 6 et plus, les informations de déploiement sont gérées par l’utilisation des annotations. 7. http://docs.oracle.com/javaee/6/tutorial/doc/giepu.html 8. http://www.mkyong.com/tutorials/jax-rs-tutorials/ 9. https://cxf.apache.org/ 2 • Préparez la classe ci-dessous : package myrest; import javax.ws.rs.GET; import javax.ws.rs.Path; @Path("/ws") public class MessageRestService { /* Premier exemple avec un GET */ @GET() @Path("hello") public String hello() { return "Hello"; } } • L’annotation @Path • L’annotation @GET 10 11 indique le chemin d’accès à un service indique la méthode HTTP qui doit être utilisée Travail à faire : • Observez le déploiement de ce service dans les traces du serveur TomEE. • Essayez d’utiliser ce service avec un navigateur. • Ajoutez les méthodes init et close classiques (annotées par @PostConstruct et @PreDestroy ) pour suivre l’instanciation de votre service. • Ajoutez à votre application une page JSP qui va nous aider à coder la partie cliente : <html> <body> <p><a target="_blank" href="ws/hello">Get Hello</a></p> </body> </html> • Ajoutez une méthode helloPost() annotée @POST dans votre page JSP pour tester ce WS. 12 qui traite le même chemin et prévoir un formulaire • Modifiez la méthode hello pour qu’elle accepte un argument (annoté par @QueryParam cette fonction avec votre page JSP. @Path("hello") @GET() public String hello(@QueryParam("message") String message) { if (message == null) { return "Hello"; } else { return "Hello " + message.toUpperCase(); } } 10. 11. 12. 13. https https https https ://docs.oracle.com/javaee/6/api/javax/ws/rs/Path.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/GET.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/POST.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/QueryParam.html 3 13 ) et testez • Ajoutez (grace à l’annotation @DefaultValue 14 ) une valeur par défaut et testez son fonctionnement. @Path("hello") @GET() public String hello( @QueryParam("message") @DefaultValue("bye") String message) { ... } • En étudiant la documentation de @QueryParam 15 , faites en sorte d’avoir une liste de messages en entrée de ce WS. Comment fonctionne dans ce cas la valeur par défaut ? • Modifiez la méthode helloPost() pour qu’elle accepte un paramètre annoté par @FormParam ajoutez à votre formulaire un champ de saisie : 16 et <form target="_blank" action="ws/hello" method="post"> <p> <input type="text" name="message" /> <input type="submit" value="Submit" /> </p> </form> 4 Traiter les URI avec paramètres • Nous allons maintenant exploiter des paramètres placés directement dans l’URI et annotés par @PathParam 17 . @GET() @Path("hello/{message}") public String hello(@PathParam("message") String message) { return "Hello " + message; } • Nous pouvons également préciser par une expression régulière la nature du paramètre. Essayez l’exemple suivant et vérifiez que la bonne version est appelée en fonction du paramètre. @GET() @Path("hello/{message:[0-9]+}") public String hello(@PathParam("message") Integer number) { return "Number " + (number); } • Finalement, nous pouvons récupérer la requête et traiter les paramètres de manière conventionnelle comme le ferait une servlet : 14. 15. 16. 17. https https https https ://docs.oracle.com/javaee/6/api/javax/ws/rs/DefaultValue.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/QueryParam.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/FormParam.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/PathParam.html 4 @GET() @Path("param") public String param(@Context UriInfo uri) { String result = ""; result += "path: " + uri.getPath(); for (Entry<String, List<String>> e : uri.getQueryParameters().entrySet()) { for (String value : e.getValue()) { result += " "; result += e.getKey() + "=" + value; } } return result; } Testez ce service avec la requête ws/param?a=1&b=2&c=3&a=4 . Étudiez ensuite l’annotation @Context et la classe UriInfo 19 . • Exploitez les PathSegment 5 20 18 (méthode uri.getPathSegments() ) sur l’exemple ws/param;a=1;b=2;c=3 Améliorer le résultat Travail à faire : • Vous pouvez piloter la production du résultat avec la classe Response 21 . A partir d’un ResponseBuilder 22 vous appelez différentes méthodes (qui renvoient toutes le même ResponseBuilder 23 ) pour construire votre réponse : @GET() @Path("reponse/{m}") public Response reponse(@PathParam("m") @DefaultValue("Salut") String m) { String res = "Result : " + m; return Response.ok().type(MediaType.TEXT_PLAIN).entity(res).build(); } A faire : Si le message est vide, tentez une redirection vers un service existant (méthode seeOther ). • Les services REST peuvent produire du XML (un arbre DOM dans l’exemple ci-dessous). L’annotation @Produces 24 indique le type MIME de la donnée à produire. 18. 19. 20. 21. 22. 23. 24. https https https https https https https ://docs.oracle.com/javaee/6/api/javax/ws/rs/core/Context.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/core/UriInfo.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/core/PathSegment.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/core/Response.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/core/ResponseBuilder.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/core/ResponseBuilder.html ://docs.oracle.com/javaee/6/api/javax/ws/rs/Produces.html 5 @GET() @Path("xhello/{message}") @Produces(MediaType.TEXT_XML) public Document xhello(@PathParam("message") @DefaultValue("Salut") String message) throws ParserConfigurationException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder = factory.newDocumentBuilder(); Document doc = docBuilder.newDocument(); Element root = doc.createElement("hello"); root.appendChild(doc.createTextNode(message)); doc.appendChild(root); return doc; } • Si vous enlevez l’annotation @Produces 25 , ce WS va produire du JSON. • Un WS peut produire plusieurs types de résultat avec : @Produces({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON }) il va produire du XML par défaut, mais, si le client précise son choix, il peut recevoir du JSON : wget -q --header "accept:application/json" \ -O - ’http://localhost:8080/votre-app/ws/xhello/Bye 6 Utiliser des JavaBeans Travail à faire : • Commencez par définir un javaBean pour représenter un message. Cette classe est annotée par des déclarations JAXB (Java API for XML Binding) qui une technologie de mise en correspondance d’un schéma XML et d’une classe Java (voir un exemple ici 26 ). 25. https ://docs.oracle.com/javaee/6/api/javax/ws/rs/Produces.html 26. http://www.mkyong.com/java/jaxb-hello-world-example/ 6 package myrest; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement public class Message { Integer number; String body; public Integer getNumber() { return number; } public void setNumber(Integer number) { this.number = number; } @XmlElement(required = true) public String getBody() { return body; } public void setBody(String body) { this.body = body; } public Message() { super(); } public Message(Integer number, String body) { super(); this.number = number; this.body = body; } @Override public String toString() { return "Message [number=" + number + ", body=" + body + "]"; } } • Nous pouvons maintenant mettre en place un service de production de messages (au format XML) : @GET() @Path("msg/{number}") @Produces(MediaType.TEXT_XML) public Message msg(@PathParam("number") Integer number) { return new Message(number, "texte " + number); } • Changer l’annotation @Produces pour produire du JSON. • Nous pouvons également renvoyer un javaBean en indiquant que c’est un sous-ressource qui doit être exploitée via des annotations. Pour ce faire, . ajoutez les annotations suivantes à votre JavaBean 7 ... @XmlAttribute(name = "id", required = true) @GET() @Path("number") public Integer getNumber() ... @XmlElement(required = true) @GET() @Path("body") public String getBody() ... ... . ajoutez ensuite la méthode ci-dessous (notez l’absence de GET ou POST ) : @Path("message/{number}") public Message message(@PathParam("number") Integer number) { return new Message(number, "texte " + number); } . Vous devez être capable de traiter la requête suivante : GET http://localhost:8080/votre-app/ws/message/100/body • Nous allons maintenant consommer (annotation @Consumes le service : 27 ) du XML et produire du XML. Préparez @POST() @Path("addMsg") @Produces({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON }) @Consumes({ MediaType.TEXT_XML, MediaType.APPLICATION_JSON }) public Message addMessage(Message m) { if (m == null) { m = new Message(0, "NULL"); } System.err.println(m); m.setNumber(m.getNumber() + 1000); m.setBody(m.getBody().toUpperCase()); return m; } • Nous allons tester ce service avec un client sous la forme d’un script bash : 27. https ://docs.oracle.com/javaee/6/api/javax/ws/rs/Consumes.html 8 #!/bin/bash tmp=/tmp/tmp.$$ trap "rm -f ${tmp}*" EXIT CONTENT="text/xml" ACCEPT="text/xml" MESSAGE="<message id=’10’><body>Bye</body></message>" wget --quiet \ --header="Content-Type: $CONTENT" \ --header="accept: $ACCEPT" \ --post-data="$MESSAGE" \ "http://localhost:8080/votre-application/ws/addMsg" \ -O "$tmp.result" cat $tmp.result exit • faites varier (dans le script) le type de codage d’entrée et de sortie. • Si le client est basé sur Java, nous pouvons utiliser les API Client : . Ajoutez les dépendances Maven pour jersey-client . Ajoutez à votre projet le test unitaire suivant : package myrest; import org.junit.Assert; import org.junit.Test; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; public class MyTest { @Test public void testMsg() { Client client = Client.create(); WebResource webResource = client.resource("http://localhost:8080/votre-app/ws/msg/200"); Message m = webResource.accept("text/xml").get(Message.class); Assert.assertNotNull(m); Assert.assertEquals(m.getNumber(), new Integer(200)); Assert.assertEquals(m.getBody(), "texte 200"); System.out.println(m); } } 9