3. REST et Jersey

3.1. Introduction

Techniquement, les services REST sont spécifiés par le JCP (Java Community Process) sous le nom JAX-RS. Cette spécification précise ce que peut ou doit faire une implémentation, comme pour toutes ces spécifications. L'implémentation de référence que l'on utilise est Jersey. Jersey est installé en standard dans un serveur JEE (tel que Glassfish ou JBoss), et peut s'installer dans un serveur Tomcat. Nous allons voir quelques points spécifiques à Jersey dans cette partie, notamment comment injecter une référence vers une ressource JNDI (en l'occurrence un EJB). En JEE5, l'annotation @EJB ne permet pas d'injecter un EJB dans un service REST Jersey. Il y a un peu de code technique à ajouter, ce que nous allons faire.

3.2. Ressources d'un service REST

Une ressource web dans le contexte des services REST est une simple classe Java (on parle de POJO pour Plain Java Object ). Cette classe doit être annotée par @Path, ou, à défaut, au moins une de ses méthodes doit l'être.

3.3. Cycle de vie d'un service REST

À la différence des servlets ou des EJB, une instance d'un service REST ne peut servir qu'une unique fois. Chaque objet sert une requête, puis est détruit. Dans un premier temps, le constructeur de cette classe est appelé, puis les différents champs annotés sont initialisés par injection, puis la méthode capable de répondre à la requête envoyée est invoquée.

3.4. Annotations des paramètres

La spécification REST définit six annotations pour associer les éléments de la requête à des champs ou des paramètres du service REST invoqué. Ces annotations peuvent être posées sur des champs, des paramètres de méthodes, ou des setters . Ces annotations sont les suivantes.
  • @PathParam est l'annotation que nous avons vue dans notre premier exemple. Elle permet d'associer un morceau de l'URL de requête à un champ ou un paramètre.
  • @QueryParam et @FormParam permettent d'associer un paramètre de la requête à un champ ou un paramètre d'une méthode de notre classe.
  • @CookieParam permet d'associer une valeur stockée dans un cookie ou l'instance de Cookie elle-même.
  • @HeaderParam permet d'associer une valeur d'un champ HTTP à un champ ou un paramètre d'une méthode de notre classe.
  • @MatrixParam permet d'associer un paramètre HTTP matriciel à un champ ou un paramètre de méthode de notre classe. Le problème est que les URL portant des paramètres matriciels ne sont pas clairement supportées par les navigateurs et les serveurs web. Cette annotation est donc à utiliser avec précautions.
D'une façon générale, ces annotations peuvent mener à la création de champs ou de paramètres qui peuvent être des types suivants :
  1. un type primitif Java ;
  2. une classe qui possède un constructeur prenant en paramètre une unique chaîne de caractères ;
  3. une classe qui possède une méthode statique de construction, qui peut s'appeler valueOf(String) ou fromString(String)
  4. une instance de List<T>, Set<T> ou SortedSet<T>, où T est un type qui satisfait l'une des conditions énoncées.
À ces annotations s'ajoute @DefaultValue, qui permet de fixer la valeur par défaut de l'élément annoté.

3.5. Associer une requête à une méthode

Cinq annotations sont définies, qui peuvent annoter certaines méthodes d'un service REST : @GET, @POST, @PUT, @DELETE, et @HEAD. Elles correspondent aux cinq méthodes HTTP qui portent le même nom. On ne peut poser chaque annotation qu'une seule fois sur une unique méthode dans une classe donnée, pour un chemin d'accès donné. Une telle méthode doit être public. Lorsqu'une telle requête arrive, la bonne méthode est invoquée, avec les paramètres annotés correctement positionnés. Enfin, une méthode annotée peut aussi porter un paramètre non annoté, que l'on appelle le paramètre d'entité. Ce paramètre portera l'intégralité de la requête. On peut définir par ailleurs une classe annotée @Provider dont la responsabilité sera de convertir cette requête dans le type Java désiré.

3.6. Annotation @Path

Cette annotation peut se poser sur la classe de notre service, ou sur une méthode, ou sur les deux. Elle désigne d'une part l'URI d'accès au service et à ses sous-services, et d'autre part la présence de paramètres dans cette URI. Voyons ceci sur un exemple.

Exemple 31. Utilisation de Path sur une classe et des méthodes

@Path("marin")
 public  class MarinService {

     @GET  @Path("id/{id}")
     @Produces("application/xml")
     public Object getMarin(@PathParam("id")  long id) {

         // contenu de la méthode
    }


     @GET  @Path("list")
     @Produces("application/xml")
     public Object getMarinsList() {

         // contenu de la méthode
    }
}

Cette classe définit deux URI :
  • marin/list, censée nous retourner la liste de nos marins ;
  • marin/id/15, censée nous retourner le marin dont l'ID est 15.
Notons que les chemins spécifiés dans les annotations @Path sont toujours relatifs. Cela rend le / en début de valeur inutile. L'annotation posée sur la classe définit un chemin relatif au chemin de l'application.

3.7. Annotations @Produces et @Consumes

Ces deux annotations permettent de déclarer les types MIME supportés pour la requête ( @Consumes) et pour la réponse ( @Produces). Ces déclarations sont facultatives. On peut poser ces annotations sur une classe ou sur des méthodes. Dans le cas d'une double déclaration, c'est celle de la méthode qui prévaut. On peut également poser ces annotations sur des classes annotées @Provider, comme nous le verrons dans la suite.

3.8. Annotation @Provider

Cette annotation se pose sur une classe, et permet de définir la notion de provider dans JAX-RS. Ces providers sont de deux types : les entity providers et les context providers . Un entity provider permet de convertir une requête en un objet Java (au sens large), et réciproquement. Il y en a de deux types :
  • les implémentations de MessageBodyReader, qui permettent de créer des objets Java à partir du contenu d'une requête ;
  • les implémentations de MessageBodyWriter, qui permettent de créer des réponses HTTP à partir d'objets Java.
La spécification définit plusieurs providers standard, dont celui qui a été utilisé dans le premier exemple de ce chapitre : javax.xml.bind.JAXBElement. C'est ce provider qui est responsable de la génération du XML lorsqu'un objet annoté par JAXB est retourné dans une méthode REST.

3.9. Interfaces MessageBodyReader et MessageBodyWriter

Au moins l'une de ces deux interfaces doit être implémentée par les classes qui se déclarent entity provider . MessageBodyReader définit une première méthode, isReadable(). Cette méthode reçoit en paramètre toutes les informations sur le contenu de la requête, et le type d'objet Java que le moteur de service REST attend. Si cette implémentation est capable de traiter cette requête, alors true doit être retourné. MessageBodyWriter possède une méthode analogue : isWriteable(). Ainsi, le moteur de service REST peut déterminer, parmi tous les entity providers qu'il connait, lequel est capable de traiter la conversion qu'il doit effectuer. Ensuite, chacune de ces implémentations définit deux autres méthodes : readFrom() et writeTo(), appelées par le moteur de service REST. Ces méthodes prennent en paramètres tous les éléments nécessaires à leur travail. Notons que MessageBodyWriter définit une méthode en plus : getSize(). Cette méthode est appelée par le moteur de service REST avant writeTo(), de façon à insérer dans le code HTTP la bonne valeur du champ length.

3.10. Injection d'EJB en JEE 5

Malheureusement, certaines injections de dépendances ne fonctionnent pas dans les services REST en JEE5. Notamment l'annotation @EJB, qui n'est pas reconnue, et laisse à null les champs qu'elle annote. Il est possible de créer un provider propriétaire dans Jersey pour pallier cet inconvénient. Ce provider est annoté de façon standard par @Provider, mais implémente une interface qui fait partie de Jersey, et non pas de JAX-RS : InjectableProvider<T, U> Cette interface définit deux méthodes, dont la seconde est appelée pour injecter une référence de l'EJB dans le champ annoté : getInjectable(ComponentContext context, T t, V v). Le deuxième paramètre de cette méthode est une référence sur l'annotation rencontrée par Jersey, où v représente la classe du champ annoté. On prend en général Type comme type pour V, dans la mesure où cette classe est la super classe de tous les types en Java. Cette méthode est donc appelée par Jersey avec l'annotation et le champ annoté. Elle doit retourner une instance de Injectable, qui n'est qu'une enveloppe sur l'objet injecté. Voyons un exemple d'une telle classe.

Exemple 32. Service REST avec injection d'un EJB

@Path("marin/{id}")
 public  class MarinXMLService {

     // injection d'un EJB : ne fonctionne pas sans un Provider ad hoc
     @EJB(mappedName="MarinService")
     private MarinService marinService ;

     @GET
	 @Produces("text/xml")
     public Object getMarin(@PathParam("id")  long id) {

        Marin marin = marinService.findMarinById(id) ;
         return marin ;
    }
}

Voici à présent le code du provider qui réalise l'injection. Cette classe est rangée dans le même projet que les services REST.

Exemple 33. Provider pour l'injection de l'EJB

// Ce provider supporte l'annotation @EJB uniquement
 @Provider
 public  class EJBProvider  implements InjectableProvider<EJB, Type> {

     // méthode technique qui indique à Jersey comment créer les
     // instances de cet objet
     public ComponentScope getScope() {
         return ComponentScope.Singleton;
    }

     // méthode appelée pour déterminer la valeur à injecter
     public Injectable getInjectable(ComponentContext context, EJB ejb, Type t) {
         // un EJB ne peut pas être un type primitif
         // si t n'est pas une classe, alors l'annotation 
         // n'est pas posée correctement
         if (!(t  instanceof Class)) 
             return null;

         try {
            Class clazz = (Class)t ;
             // nous sommes dans un contexte JEE, donc pas besoin
             // de fichier jndi.properties
            Context initialContext =  new InitialContext();

             // la valeur par défaut du nom de notre EJB est le nom de sa classe
            String componentName = clazz.getName() ;
             // si l'annotation mappedName est présente, alors elle porte
             // le nom de l'EJB
             if (ejb.mappedName() != null) {
                componentName = ejb.mappedName() ;
            }

             // requête sur l'annuaire avec le nom de l'EJB
             final Object ejbInstance = initialContext.lookup(componentName);

             // on retourne enfin une instance d'Injectable, conformément à 
             // l'interface InjectableProvider
             return  new Injectable() {
                 public Object getValue() {
                     return ejbInstance ;
                }
            };
        }  catch (Exception e) {
             return null;
        }
    }
}

Cet exemple n'est bien sûr par complet, il faudrait gérer les autres attributs de l'annotation @EJB, mais l'organisation du code reste la même. On peut imaginer d'autres types de providers , construits sur le même principe, pour supporter les annotations @Resource et @PersistenceContext par exemple.
JAXB et services REST
Retour au blog Java le soir
Cours & Tutoriaux