dimanche 13 février 2011

RESTful mon arch

Aujourd'hui, j'aimerais emmener le débat sur les services Web REST avec l'implémentation JAX-RS (JSR-311) du framework CXF d'Apache. Pour commencer, je vais rentrer dans le vif du sujet avec un exemple non exhaustif mais significatif, en utilisant l'IDE JDeveloper d'Oracle et le serveur Web Tomcat.


Initialisation de l'environnement de développement
Création d'un projet Web sous JDeveloper
Implémentation de services REST
Courte introduction à REST
Classe de services effectuant une sortie plain/text
- Formulaire pour effectuer un POST
- Formulaire pour effectuer un PUT
- Formulaire pour effectuer un DELETE
Classe de services effectuant une sortie application/xml
Fichier de configuration des services : beans.xml
Fichier de configuration web.xml
Exemple de classe de tests unitaires JUnit allégée
Fichier index.html pour l'application Web "Gestion de ressources"

Déploiement sous Tomcat avec JDeveloper
Utilisation de l'application "Gestion de ressources"
Page d'accueil
Accès à la description WADL
Ajouter une ressource
Lire une ressource
Mettre à jour une ressource
Supprimer une ressource

Conclusion



Initialisation de l'environnement de développement

J'ai choisi de travailler sous Windows XP avec les produits suivants :
  • JDK 1.6.0_23
  • Tomcat 6.0.32
  • JDevelopper 11.1.1.4.0; la version complète d'1,3 Go et non pas la version allégée de 75 Mo.
  • CXF 2.3.2
Après avoir téléchargé et installé les produits (dans le répertoire d:\bin par exemple), il faut configurer quelques variables d'environnement système associés. Dans le panneau de configuration, côté "Système", dans l'onglet "Avancé", cliquer sur "Variables d'environnement" et ajouter dans la partie "Variables système"  :
  • CATALINA_HOME=d:\bin\apache-tomcat-6.0.32
  • JAVA_HOME=d:\bin\jdk1.6.0_23
  • ;%JAVA_HOME%\bin à la fin de PATH


Création d'un projet Web sous JDeveloper

Pour ce faire, je vous propose de suivre les étapes suivantes :
  • dans le menu "File", choisir "New...", 
  • sélectionner "Generic Application" dans la catégorie "General",

  • entrer le nom de l'application et le nom du projet (au sens JDeveloper),


  • ajouter une ressource web.xml au projet,



Après cette étape, le système de fichier associé au projet dans le workspace de travail est le suivant :

Il peut être utile ici de créer les répertoires "lib" dans "WEB-INF", "src" dans "myrestprj" et "test" dans "src".
  • faire un click droit sur le projet et choisir "Deploy/New Deployment Profile...",

  • donner le nom "mywebapp" et valider,  
  • ajouter le filtre "**/*Test.class" en exclusion dans les filtres de "WEB-INF/classes", 

Ce filtre permet d'exclure les classes de tests unitaires dont les noms finissent par "Test", de type JUnit par exemple, du WAR à générer.
  • aller ensuite sur "Platform", définir "Tomcat 6.x" comme plateforme par défaut et valider,

Cela permet également d'éviter que JDeveloper ajoute un fichier weblogic.xml  par défaut dans le WAR généré.
  • dans le menu "View", choisir "Application Server Navigator",
Cela permet d'ouvrir la vue des serveurs d'applications.
  • faire un click droit sur "Application Servers" et choisir "New Application Server...",

  • créer un serveur d'application de type Tomcat Standalone,


  • intégrer le framework JUnit à JDeveloper (non intégré par défaut), en le téléchargeant via JDeveloper,



  • copier dans "WEB-INF\lib" les librairies provenant de CXF suivantes :
commons-logging-1.1.1.jar
cxf-2.3.2.jar
geronimo-activation_1.1_spec-1.1.jar
geronimo-annotation_1.0_spec-1.1.1.jar
jsr311-api-1.1.1.jar
neethi-2.0.4.jar
spring-aop-3.0.5.RELEASE.jar
spring-asm-3.0.5.RELEASE.jar
spring-beans-3.0.5.RELEASE.jar
spring-context-3.0.5.RELEASE.jar
spring-core-3.0.5.RELEASE.jar
spring-expression-3.0.5.RELEASE.jar
spring-web-3.0.5.RELEASE.jar
wsdl4j-1.6.2.jar
XmlSchema-1.4.7.jar
      • faire un click droit sur le projet et choisir "Project Properties...",
      • aller dans "Libraries and Classpath",
      •  cliquer sur "User" et ajouter un nouvel ensemble de librairies utilisateur (portant le nom "mylib" par exemple),



      Implémentation de services REST


      Courte introduction à REST

      CXF permet de réaliser des services REST en utilisant des annotations JAVA, en mappant ces derniers sur les méthodes ou verbes HTTP et en jouant sur les URIs. Les verbes HTTP concernés sont POST, GET, PUT et DELETE. On retrouve ainsi les quatres opérations de base pour la persistance des données, à savoir les fameux CRUD (Create, Read, Update et Delete). Cependant, certains navigateurs ne supportent pas toutes les méthodes HTTP, notamment PUT et DELETE. Du coup CXF vient avec une technique particulière pour adresser ces deux méthodes.

      Je vous propose à ce stade d'implémenter une petite application Web de gestion de ressources diverses et variées.

      Classe de services effectuant une sortie plain/text

      Commençons par créer une classe JAVA "TextOutputServices" dans le package "mycommunityman.myrest", par exemple, dans laquelle figure un attribut statique contenant un petit ensemble de ressources de départ. Les services de cette classe renvoient un résultat sous forme de String. Ils sont de type plain/text. Les quatre verbes HTTP sont implémentés dans cette classe.

      package mycommunityman.myrest;

      import java.util.HashMap;

      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 javax.ws.rs.core.MediaType;


      @Path("/id/{id}")
      @Produces(MediaType.TEXT_PLAIN)
      public class TextOutputServices {
         
          private static HashMap ressources = new HashMap();
          static {
              ressources.put("1", "moi");
              ressources.put("2", "toi");
              ressources.put("3", "lui ou elle");
          }
         
          @GET
          public String get(@PathParam("id") String id) {
              return "C'est " + ressources.get(id) + " !";
          }
         
          @POST
          @Path("/value/{value}")
          public String post(@PathParam("id") String id, @PathParam("value") String name) {
              String result;
              if (ressources.get(id) == null) {
                ressources.put(id, name);
                result = "Bonjour " + ressources.get(id) + " !";
              } else {
                result = "Impossible d'ajouter la ressource. L'identifiant de ressource existe déjà.";
              }
              return result;
          }

          @DELETE
          public String delete(@PathParam("id") String id) {
                  return "Au revoir " + ressources.remove(id) + " !";
          }

          @PUT
          @Path("/value/{value}")
          public String put(@PathParam("id") String id, @PathParam("value") String name) {
              String result;
              if (ressources.get(id) != null) {
                ressources.put(id, name);
                result = "Rebonjour " + ressources.get(id) + " !";
              } else {
                result = "Impossible de mettre à jour la ressource. La ressource n'existe pas.";
              }
              return result;
          }
       
      }



      - Formulaire HTML pour effectuer un POST

      Pour effectuer un POST de ressource (l'équivalent d'un ajout de ressource), mettons en place un formulaire HTML dédié : "topost.html".

      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
      <html>
        <head>
          <meta http-equiv="Content-Type" content="text/html; charset=windows-1252"></meta>
          <title>topost</title>
         
         <script type="text/javascript">   
          function validate() {
            document.forms[0].action = document.forms[0].action +
            "/id/" + document.forms[0].elements['id'].value +
            "/value/" + document.forms[0].elements['value'].value;
            document.forms[0].submit();
          }
        </script>
        
        </head>
        <body>
       
          <form action="./myrest/textoutputservices" method="POST">
            <input type="text" name="id"/>
            <input type="text" name="value"/>
            <input type="submit" onclick="validate()"/>
          </form>
         
        </body>
      </html>

      Le code JAVASCRIPT permet de changer l'action du formulaire lors de la soumission de ce dernier. L'URI est ainsi adapté par rapport aux données saisies.



      - Formulaire HTML pour effectuer un PUT

      Pour effectuer un PUT de ressource (l'équivalent d'une mise à jour de ressource), mettons en place, de la même manière que précédemment, un formulaire HTML dédié pour mieux comprendre ce qui se passe : "toput.html".


      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
      <html>
        <head>
          <meta http-equiv="Content-Type" content="text/html; charset=windows-1252"></meta>
          <title>toput</title>
         
         <script type="text/javascript">   
          function validate() {
            document.forms[0].action = document.forms[0].action +
            "/id/" + document.forms[0].elements['id'].value +
            "/value/" + document.forms[0].elements['value'].value +
            "?_method=put";
            document.forms[0].submit();
          }
        </script>
        
        </head>
        <body>
       
          <form action="./myrest/textoutputservices" method="POST">
            <input type="text" name="id"/>
            <input type="text" name="value"/>
            <input type="submit" onclick="validate()"/>
          </form>
         
        </body>
      </html>


      Pour simuler l'action PUT, CXF (ou c'est XF d'ailleurs ?) utilise toujours l'action POST, mais passe un paramètre "_method" à la requête dont la valeur doit être "put".



      - Formulaire HTML pour effectuer un DELETE

      Pour effectuer un DELETE de ressource (l'équivalent d'une suppression de ressource), mettons en place un formulaire HTML dédié également : "todelete.html".


      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
      <html>
        <head>
          <meta http-equiv="Content-Type" content="text/html; charset=windows-1252"></meta>
          <title>todelete</title>
         
         <script type="text/javascript">   
          function validate() {
            document.forms[0].action = document.forms[0].action +
            "/id/" + document.forms[0].elements['id'].value +
            "?_method=delete";
            document.forms[0].submit();
          }
        </script>
        
        </head>
        <body>
       
          <form action="./myrest/textoutputservices" method="POST">
            <input type="text" name="id"/>
            <input type="submit" onclick="validate()"/>
          </form>
         
        </body>
      </html>

      De la même manière que précédemment, pour simuler l'action DELETE, CXF utilise l'action POST, mais avec un paramètre de requête "_method" dont la valeur doit être "delete".


      Classe de services effectuant une sortie application/xml

      Commençons par créer une classe POJO "EmbeddedInformation" dans le package "mycommunityman.mypojo" par exemple. Elle va servir de classe résultat renvoyée par les services REST.

      package mycommunityman.mypojo;

      import javax.xml.bind.annotation.XmlRootElement;

      @XmlRootElement(name = "embeddedInformation")
      public class EmbeddedInformation {
         
          private String information;
          private EmbeddedInformation moreInformation;
      }

      L'annotation "@XmlRootElement" permet d'indiquer que la classe sera traduite en élément XML racine dans un document XML.

      Puis, implémentons des services REST en exploitant uniquement le verbe GET pour simplifier. Pour cela, créons la classe "XmlOutputServices" dans le package "mycommunityman.myrest".

      package mycommunityman.myrest;

      import java.util.List;

      import javax.ws.rs.GET;
      import javax.ws.rs.Path;
      import javax.ws.rs.PathParam;
      import javax.ws.rs.Produces;
      import javax.ws.rs.core.MediaType;
      import javax.ws.rs.core.PathSegment;

      import mycommunityman.mypojo.EmbeddedInformation;


      @Path("/name/{name}")
      @Produces(MediaType.APPLICATION_XML)
      public class XmlOutputServices {
         
         
          @GET
          public EmbeddedInformation get(@PathParam("name") String name) {
              return new EmbeddedInformation("Bonjour " + name + " !", null);
          }

          @GET
          @Path("/something/{something}")
          public EmbeddedInformation get(@PathParam("name") String name,
                                         @PathParam("something") String something) {
              EmbeddedInformation result = get(name);
              result.setMoreInformation(new EmbeddedInformation("Aimez-vous " + something + " ?", null));
              return result;
          }

          @GET
          @Path("/something/{something}/{params:.*}")
          public EmbeddedInformation get(@PathParam("name") String name,
                                         @PathParam("something") String something,
                                         @PathParam("params") List paramsAsList) {
              EmbeddedInformation result = get(name, something);
              EmbeddedInformation tmp = build(paramsAsList);
              result.getMoreInformation().setMoreInformation(new EmbeddedInformation("Mais encore ?", tmp));
              return result;
          }
       
          private EmbeddedInformation build(List paramsAsList) {
              EmbeddedInformation result = new EmbeddedInformation();
              EmbeddedInformation resultTmp = result;
              int i = 0;
              for(; i < paramsAsList.size()-1; i++) {
                  String param = paramsAsList.get(i).toString();
                  resultTmp.setInformation(param);
                  resultTmp.setMoreInformation(new EmbeddedInformation());
                  resultTmp = resultTmp.getMoreInformation();
              }
              resultTmp.setInformation(paramsAsList.get(i).toString());
              return result;
          }
       
      }

      Revenir au début de l'article


      Fichier de configuration des services : beans.xml

      Le framework CXF repose sur le framework Spring pour sa partie configuration des services dans une application Web. Le fichier beans.xml de Spring est défini de la manière suivante :

      <?xml version="1.0" encoding="UTF-8"?>
      <beans xmlns="http://www.springframework.org/schema/beans"
                     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                     xmlns:jaxrs="http://cxf.apache.org/jaxrs"
                     xsi:schemaLocation="
      http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://cxf.apache.org/jaxrs
      http://cxf.apache.org/schemas/jaxrs.xsd">
                 <import resource="classpath:META-INF/cxf/cxf.xml" />
                 <import resource="classpath:META-INF/cxf/cxf-extension-jaxrs-binding.xml" />
                 <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
       
      <jaxrs:server id="textoutputservices" address="/textoutputservices">
           <jaxrs:serviceBeans>
                 <ref bean="textOutputServices" />
           </jaxrs:serviceBeans>
      </jaxrs:server>
      <bean id="textOutputServices" class="mycommunityman.myrest.TextOutputServices" />
       
      <jaxrs:server id="xmloutputservices" address="/xmloutputservices">
           <jaxrs:serviceBeans>
                <ref bean="xmlOutputServices" />
           </jaxrs:serviceBeans>
      </jaxrs:server>
      <bean id="xmlOutputServices" class="mycommunityman.myrest.XmlOutputServices" />

      </beans>



      Fichier de configuration web.xml

      Le fichier de configuration de l'application Web "Gestion de ressources" est le suivant :

      <?xml version = '1.0' encoding = 'windows-1252'?>
      <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
               version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee">
          <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>WEB-INF/beans.xml</param-value>
          </context-param>
          <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
          </listener>   
          <servlet>
              <servlet-name>cxf</servlet-name>
              <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
              <load-on-startup>1</load-on-startup>
          </servlet>
          <servlet-mapping>
              <servlet-name>cxf</servlet-name>
              <url-pattern>/myrest/*</url-pattern>
          </servlet-mapping>
      </web-app>



      Exemple de classe de tests unitaires JUnit allégée

      Pour créer une classe de tests "TextOutputServicesTest", faisons un click droit sur le package "test", choisissons "New...", et sélectionnons "Unit Tests" dans la catégorie "General". La classe de tests unitaires JUnit de la classe "TextOutputServices" pourrait s'écrire de la manière suivante :


      package test.mycommunityman.myrest;

      import mycommunityman.myrest.TextOutputServices;

      import static org.junit.Assert.assertEquals;
      import org.junit.Test;

      public class TextOutputServicesTest{
         
          public TextOutputServicesTest () {
          }

          /**
           * @see TextOutputServices#get(String)
           */
          @Test
          public void testGet() {
              TextOutputServices textOutputServices = new TextOutputServices();
              assertEquals("C'est moi !", textOutputServices.get("1"));
              assertEquals("C'est toi !", textOutputServices.get("2"));
              assertEquals("C'est lui ou elle !", textOutputServices.get("3"));
          }
         
      }

      L'exécution des tests donne le résultat suivant :




      Fichier index.html pour l'application Web "Gestion de ressources"

      Pour donner accès, de manière explicite, aux différents services de notre petite application, je vous propose de mettre en place le fichier "index.html".

      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
      <html>
        <head>
          <meta http-equiv="Content-Type" content="text/html; charset=windows-1252"></meta>
          <title>index</title>
        </head>
        <body>
        Bienvenue sur le site de gestion de ressources !
        <br>
        <br>
        <a href="./myrest">accéder aux services REST</a>
        <br>
        <a href="./topost.html">ajouter une ressouce</a>
        <br>
        <a href="./todelete.html">supprimer une ressouce</a>
        <br>
        <a href="./toput.html">mettre à jour une ressouce</a>
         </body>
      </html>

      Après cette étape, la structure du projet sous JDeveloper est la suivante :




      Déploiement sous Tomcat avec JDeveloper

      • En préambule, nous pouvons nous amuser à configurer Tomcat afin d'avoir accès à la console d'administration. Pour cela, il faut éditer le fichier %CATALINA_HOME%\conf\tomcat-users.xml et ajouter un utilisateur de la manière suivante :

      <role rolename="manager-gui"/>
      <user username="tomcat" password="tomcat" roles="manager-gui"/>

      La console d'administration Tomcat est accessible à l'URL http://localhost:8080/. En cliquant sur "Tomcat Manager" dans le menu en haut à gauche, une popup d'authentification apparaît. Il faut alors saisir le login et le mot de passe vus ci-dessus.


      On accède alors à la partie administration.


      • Démarrons le serveur Tomcat, en double cliquant par exemple sur le script %CATALINA_HOME%\bin\startup.bat,
      • Faisons un click droit sur le projet, choisissons "Deploy" et puis "mywebapp...",


      Le fichier "mywebapp.war" est copié dans le répertoire %CATALINA_HOME%\webapps. Et l'application est déployée à chaud et est accessible à l'URL http://localhost:8080/mywebapp.



      Utilisation de l'application "Gestion de ressources"


      Page d'accueil

      La page d'accueil de notre petite application Web de gestion de ressources ressemble à :




      Accès à la description WADL

      Une description WADL (Web Application Description Language) est le pendant d'une description WSDL (Web Service Description Language) des services SOAP pour les services RESTful. Ce fichier descriptif des services REST repose sur une grammaire beaucoup plus simple que celle du WSDL. Pour notre application, les ressources WADL sont disponibles à l'URL http://localhost:8080/mywebapp/myrest.




      Ajouter une ressource

      Vérifions que la ressource à ajouter n'existe pas déjà.


      Ajoutons la nouvelle ressource.




      Vérifions qu'elle a bien été ajoutée.




      Lire une ressource

      Pour utiliser la méthode HTTP GET, il suffit simplement de saisir l'URI de la ressource recherchée directement dans le navigateur. Pour récupérer la ressource dont l'id est 1 via le service renvoyant une réponse plain/text, il suffit de saisir http://localhost:8080/mywebapp/myrest/textoutputservices/id/1.


      Pour utiliser un des services renvoyant une réponse application/xml, nous pouvons utiliser http://localhost:8080/mywebapp/myrest/xmloutputservices/name/mycommunityman/something/la pomme/la cerise/et/l'orange.




      Mettre à jour une ressource

      Avant de mettre à jour la ressource, récupérons sa valeur.



      Mettons à jours la ressource maintenant.



      Vérifions qu'elle a bien été mise à jour.




      Supprimer une ressource

      Commençons par récupérer la ressource à supprimer.


      Supprimons cette ressource.



      Vérifions qu'elle a bien été supprimée.




      Conclusion

      Malgré la longueur de cet article, nous sentons bien la facilité avec laquelle nous pouvons écrire des services RESTful, que l'on soit en point à point ou en mode orchestration. REST est bien adapté aux applications Web de type gestion de ressources, en offrant un style d'architecture existante (l'organisation de toutes les ressources sur internet) et adéquate (on peut presque tout faire rien qu'avec les méthodes HTTP GET et POST).
      Contrairement à l'orienté-objet, REST se base sur les URI plutôt que sur les méthodes. De plus, il n'y a plus la complexité des enveloppes SOAP (Simple Object Access Protocol) ou des protocoles RPC (Remote Procedure Call). Bien évidemment, les avantages et les inconvénients ne peuvent pas se résumer qu'à ces quelques lignes.Alors, à vos REST, prêt, partez !

      Aucun commentaire:

      Enregistrer un commentaire