Nous examinons l'utilisation des API Java EE afin de créer des services web. Nous nous intéresserons tout particulièrement à l'API jax-ws pour proposer des services web SOAP et jax-rs pour des services web REST.
Intérêt des services web
-
Le protocole HTTP est devenu au fil du temps un coûteau suisse applicatif s'attaquant à de nombreuses problématiques en dehors de la fourniture de pages HTML liées par des pages hypertextes. Parmi ces applications, on peut citer :
- le transfert de données audiovisuelles avec des sites de partage de vidéos
-
l'hébergement d'applications clientes associant le langage de description de pages HTML5 avec le JavaScript
- les données sont échangées par des appels de type AJAX selon un mode requête/réponse sans état
- le protocole websocket peut être utilisé pour des échanges bidirectionnels de longue durée sur une connexion TCP
- l'hébergement de données spécifiques en utilisant le protocol WebDAV : fichiers, contacts (CardDAV), agenda (CalDAV)...
- ...
- Parmi ces applications, le protocole HTTP peut être utilisé pour proposer des APIs appelables à distance par des applications
-
Le principal intérêt réside dans l'universalité du protocole HTTP avec des bibliothèques clientes existant pour la plupart des langages de programmation
- Des environnements hétérogènes peuvent dialoguer facilement (on peut par exemple réaliser un service web en utilisant la plateforme Java et y accéder depuis un client utilisant .Net et vice-versa)
- Les services web peuvent aussi s'interfacer facilement avec des serveurs webs qui peuvent jouer le rôle de proxy inverse (et éventuellement apporter une couche de sécurisation et de répartition de charge)
Services web SOAP
Introduction à SOAP
- SOAP = Simple Object Access Protocol (anciennement XML-RPC disposant d'une API JAX-RPC) défini par une recommandation du W3C
- Ce protocole décrit le format des données échangées pour les requête d'appels de méthodes distantes ainsi que pour les réponses
- SOAP est indépendant du protocole applicatif utilisé : on peut ainsi l'utiliser avec le protocole SMTP (avec échange d'emails), Java Message Service (JMS), dans des datagrammes UDP...
- En pratique SOAP est le plus couramment utilisé avec le protocole HTTP
- Les messages SOAP peuvent être routés pas plusieurs machines (avec des noeuds intermédiaires)
- SOAP utilise le langage WSDL (Web Service Description Langage) afin de documenter le fonctionnement d'un webservice (nomenclature des méthodes, des types des paramètres, des types de retour...)
- SOAP peut également utiliser le protocole UDDI (Universal Description Discovery and Integration) afin de publier les services dans des annuaires et de pouvoir les rechercher
-
SOAP utilise le format XML Information Set sérialisé comme du XML ; un document SOAP comprend quatre sections :
- une enveloppe indiquant la version du protocole
- un en-tête
- le corps avec le contenu de l'appel de méthode ou sa réponse
- les erreurs éventuelles rencontrées
- De part le format employé, SOAP est extrêmement verbeux (bien qu'il existe des surcouches d'optimisation) et peut donc s'avérer plus lent que d'autres technologies d'appels distants utilisant des échanges plus légers
- Il est difficile de réimplanter de zéro une bibliothèque cliente ou serveur SOAP contrairement au modèle REST beaucoup plus simple à manipuler
- Quelques services web SOAP accessibles publiquement : http://xmethods.net/ve2/index.po
SOAP avec jax-ws
- jax-ws propose depuis la JSR 224 une API pour créer un service web SOAP en Java
- Son utilisation est relativement aisée grầce à l'emploi d'annotations
Pour webservicifier une classe, il est nécessaire :
- d'ajouter l'annotation @WebService en-tête de cette classe (on pourra notamment spécifier le nom du service web avec la propriété name)
- de spécifier chacune des méthodes à publier dans la spécification WDSL en l'annotant avec @WebMethod (avec une operationName qui peut être un nom différent du nom de la méthode Java et qui sera utilisée le message XML échangé avec SOAP)
- d'indiquer facultativement pour chaque paramètre une annotation @WebParam afin d'indiquer des informations concernant le paramètre de méthode : name pour son nom SOAP
Différents styles de formats pour les messages échangés entre le client et serveur peuvent être adoptés :
- RPC
- document
- Chacun de ces styles peut utiliser un sous-style literal ou encoded. Cela détermine le format des données XML qui peut être plus ou moins strict avec l'intégration d'un schéma par exemple avec document (ce qui permet l'usage d'un validateur pour les données XML échangées). Le sous-style encoded est généralement plus verbeux en ajoutant des informations sur le type des paramètres pour chaque appel de méthode. Pour en savoir plus sur les différents styles SOAP, on pourra consulter cet article intéressant. Le style document, literal avec enveloppage (wrapped) est généralement un bon compromis.
Il est possible de créer automatiquement le fichier WSDL correspondant à une classe Java en utilisant un convertisseur Java>WSDL (par exemple en utilisant le script wsproduce.sh de WildFly qui utilise Apache AXF.
Côté client, une classe proxy peut également être générée afin de contacter le service web pour exécuter la méthode distante en utilisant wsimport.
Il est possible aussi de générer côté client un proxy supportant les appels asynchrones. Le service web reste synchrone, l'asynchronisme étant simulé par le client.
Exemple : récupération du n-ième nombre premier
Nous souhaitons réaliser un service web capable de nous fournir un nombre premier du rang que l'on indiquera. Ainsi le nombre premier de rang 0 est 2, de rang 1 est 3, de rang 2 est 5...
Nous écrivons la classe permettant d'implanter ce service web avec les annotations nécessaires :
package fr.upem.primeservice; import java.util.ArrayList; import java.util.List; import javax.jws.WebMethod; import javax.jws.WebService; import javax.xml.ws.Endpoint; /** A class supplying prime numbers of a given rank */ @WebService public class PrimeComputer { // We store the first prime numbers to cache the results and accelerate divider tests private final List<Long> firstPrimes = new ArrayList<Long>(); public PrimeComputer() { firstPrimes.add(2L); // 2 is the first prime number } private boolean testPrime(long p) { for (int i = 0; i < firstPrimes.size(); i++) { long v = firstPrimes.get(i); if (v * v > p) return true; if (p % v == 0) return false; } return true; } @WebMethod public synchronized long getPrime(int rank) { while (rank >= firstPrimes.size()) { long candidate = firstPrimes.get(firstPrimes.size() - 1) + 1; while (! testPrime(candidate)) candidate++; firstPrimes.add(candidate); } return firstPrimes.get(rank); } public static void main(String[] args) { // Start the embedded web service to offer the service Endpoint.publish("http://localhost:10033/PrimeComputer", new PrimeComputer()); } }
Nous n'utilisons pas ici un serveur applicatif pour fournir notre service web : un serveur web embarqué est utilisé en utilisant la méthode statique Endpoint.publish. Ceci est assez pratique pour tester rapidement le service sans un déploiement lourd mais ne doit bien sûr pas être employé en production.
Il est possible de récupérer la description en langage WSDL du service web en interrogeant le serveur à l'adresse suivante :
`` http://localhost:10033/PrimeComputer?wsdl
On remarquera que le fichier retourné comprend une section schéma fournie dans un fichier externe qui décrit les types de données utilisées.
Il faut maintenant pouvoir interroger le service web depuis un client externe. Pour un client en langage Java, il est utile de générer du côté du client les classes Java nécessaires à l'interrogation du service. Il s'agit d'une interface PrimeComputer qui reflétera les WebMethod du service web et qui sera implantée par un proxy interrogeant le serveur web. D'autres classes utilitaires peuvent être générées pour traiter les différents types de données.
On définit un fichier XML pour le binding des types de données. Ici nous nous contentons d'un fichier de binding minimaliste en réclamant juste la génération d'un proxy client supportant des appels asynchrones :
<bindings wsdlLocation="http://localhost:10033/PrimeComputer?wsdl" xmlns="http://java.sun.com/xml/ns/jaxws"> <enableAsyncMapping>true</enableAsyncMapping> </bindings>
On utilise l'utilitaire wsimport fourni avec le JDK pour générer les classes Java côté client :
`` wsimport http://localhost:10033/PrimeComputer?wsdl -b src/webBinding.xml -s src/
Nous implantons ensuite une méthode main testant un appel asynchrone puis un appel asynchrone. Pour un appel asynchrone, un objet Future<GetPrimeResult> est retourné. Le résultat est contenu à travers deux niveaux d'enveloppement :
- GetPrimeResult contient le long résultat de la méthode
- Future<T> ne contient pas immédiatement une référence vers le résultat. Sa méthode get() est bloquante jusqu'à la réception du résultat ; la méthode isDone() retourne immédiatement et permet de savoir si le résultat est déjà obtenu. Notons que l'AsyncHandler que l'on peut passer en paramètre sous la forme d'un lambda est exécuté dans une thread annexe dès que le résultat est connu (méthode de callback).
public static void main(String[] args) throws Exception { int rank = Integer.parseInt(args[0]); System.out.println("Querying the server for the " + rank + "-th prime number"); PrimeComputerService s = new PrimeComputerService(new URL(URL)); PrimeComputer pc = s.getPrimeComputerPort(); // Make an async call long start = System.nanoTime(); @SuppressWarnings("unchecked") Future<GetPrimeResponse> prime = (Future<GetPrimeResponse>) pc.getPrimeAsync(rank, res -> { try { System.out.println("Result received in " + (System.nanoTime() - start) / 10e9 + " s"); System.out.println("Result received on thread " + Thread.currentThread() + ": " + res.get().getReturn()); } catch (Exception e) { e.printStackTrace(); } }); while (! prime.isDone()) { System.out.println("Waiting for the result since " + (System.nanoTime() - start) / 10e9 + " s"); Thread.sleep(100); } System.out.println("Finished for the async call"); System.out.println("Starting the synchronous call (should be quicker since the server has cached the result)"); System.out.println("Result of the synchronous call: " + pc.getPrime(rank)); }
Services web REST
Introduction à REST
- REST = Representational State Transfer
- REST est un type d'architecture de service web ; il ne s'agit pas d'un protocole contrairement à SOAP, il n'y a donc pas de format de données imposées
- REST est généralement associé à l'utilisation du protocole HTTP pour contacter le service
-
Une API de type REST décrit :
- un certain nombre de ressources disponibles à des adresses déterminées sur le serveur
-
associées à une ou plusieurs actions données (méthodes HTTP) :
- GET pour récupérer une ressource (action nilpotente, i.e. sans effet de bord) ; la requête n'a pas de corps
- POST pour modifier une ressource
- PUT pour ajouter une ressource
- DELETE pour supprimer une ressource
- ...
-
Un service REST ne conserve pas d'état
- Chaque requête doit se suffire à elle-même pour son exécution (elle peut contenir des informations contextuelles comme un numéro de session attribué par le serveur web)
-
Chaque ressource représente une unité élémentaire d'information
- On évite de mettre en place une ressource dieu servant de fourre-tout pour de nombreuses données
- Cela implique que le client ait à réaliser plusieurs requêtes pour obtenir ce qu'il cherche
-
Exemple : on veut programmer un rendez-vous dans un agenda à Jakarta au moment du coucher du soleil le 10 novembre 2015
- On demande à un service web les coordonnées géographiques de Jakarta : GET http://cities.example.com/cityData/Jakarta
- On récupère l'éphéméride pour le point géographique de Jakarta : GET http://ephemeris.example.com/ephemeris/-6.22,106.82/20151110/
- On ajoute le rendez-vous dans un agenda : PUT http://agenda.example.com/myAgenda/20151110T193724/ (le corps contenant des informations contenant le rendez-vous)
-
Un service REST doit coopérer avec les caches web :
- Les en-têtes de réponse doivent contenir des informations pour la mise ou non en cache (date d'expiration du contenu)
- Le client peut ainsi éviter de redemander des ressources qu'il aurait déjà en cache (économie de temps et transferts de données)
-
L'interface doit être simple et uniforme
- Les ressources sont identifiées par des URIs ; il est possible de les obtenir sous une ou plusieurs représentations bien définies en tant que réponse (XML, JSON, HTML, texte pur...)
- Les représentations des ressources doivent être auto-suffisantes pour décrire complétement la ressource et permettre de demander sa modification ou sa suppression si cela est prévu
- Les messages échangés (requêtes et réponses) doivent être explicites en hébergeant eux-même toutes les métadonnées nécessaires à leur interprétation correcte (par exemple le type MIME de données envoyées, ou l'encodage de texte transmis)
-
Un service REST doit respecter une approche HATEOAS (Hypermedia As The Engine Of the Application State). Cela signifie que les éventuelles ressources liées à une ressources peuvent être mentionnés comme des liens hypertextes dans une réponse. Cela permet d'utiliser le service web avec un mécanisme de découverte sans disposer d'une documentation qui documenterait exhaustivement toutes les ressources existantes
- Cette approche s'oppose fondamentalement au protocole SOAP où tous les RPC du service web sont décrits dans un fichier WSDL comprenant également une description des types manipulés
- Un service REST repose sur une architecture client/serveur ; il est possible que les communications se fassent par l'intermédiaire de serveurs proxys avec d'éventuels caches transparents utilisés ou mécanismes de répartition de charge (à l'image de procédés utilisés par des sites web classiques)
-
Un service REST peut envoyer du code à exécuter côté client
- Par exemple un service d'éphéméride pourrait renvoyer du code JavaScript permettant de le calculer côté client plutôt que de nous envoyer directement la réponse qu'il calculerait lui-même (démarche code on-demand)
En résumé, les architectures REST s'inspirent fortement de principes introduits avec le protocole HTTP utilisé sur le web : on essaie d'exploiter les capacités de ce protocole pour concevoir des services web qui soient d'utilisation simple et auto-documentée (dont on peut comprendre l'utilisation par découverte dans documentation externe).
REST avec jax-rs
- jax-rs est l'API de Java EE utilisée pour la mise en oeuvre de services web suivant l'architecture REST ; elle est définie avec l JSR 339 ; la version 2.0 date de 2013
- L'architecture REST étant relativement simple, il est même possible de se passer de l'API pour utiliser directement une servlet
- L'implantation de référence de jax-rs est réalisée avec le projet Jersey d'Oracle ; il existe d'autres implantations telles que Apache CXF ou JBoss RestEasy
Les annotations
- Un service web REST se définit avec des annotations sur la classe et ses méthodes
- La classe du service web REST est instanciée pour chaque invocation ; elle n'est pas censée conserver un état (on peut néanmoins conserver un état en injectant des beans d'une portée spécifique).
- @Path("/chemin") indique le chemin d'accès au service web ; cette annotation peut décorer la classe et/ou les méthodes. Il est possible d'indiquer des paramètres nommés entre accolades dans le chemin (avec optionnellement une expression régulière).
- Chaque méthode du service web rendue accessible doit être publique et annotée avec la méthode HTTP avec laquelle elle est appelable. Il peut s'agir de @GET, @POST, @PUT @DELETE, @HEAD, @OPTIONS.
-
Il faut indiquer pour chaque paramètre de la méthode comme celui-ci est récupéré :
- @PathParam("name") indique que le paramètre est récupéré depuis le chemin de l'URI en capturant "name" (indiqué comme paramètre dans @Path)
- @QueryParam("name") spécifie que le paramètre est indiqué dans la section query de l'URI, e.g. http://example.com/webservice?name=value&name2=value2
- @HeaderParam("name") indique la récupération du paramètre depuis un en-tête HTTP
- @CookieParam("name") indique que le paramètre à récupérer est présent dans un cookie
- @MatrixParam("name") récupère un paramètre de type matrix : les paramètres de type matrix sont introduits et séparés par des caractères ;, e.g. ``http://example.com/webservice;name=value;name2=value2;name3=value3
- @FormParam("name") est utilisé pour récupérer un paramètre indiqué dans un champ de formulaire (envoyé avec la méthode GET ou POST)
- Pour chaque paramètre, il est possible d'indiquer une valeur par défaut si le paramètre ne peut être récupéré depuis les informations fournies par la requête : on utilise l'annotation @DefaultParam("defaultValue")
- Tous les paramètres doivent être constructibles depuis une chaîne de caractères. Le type d'un paramètre doit donc être soit String, soit un type primitif, soit n'importe quel type objet du moment que celui-ci dispose d'un constructeur avec un seul argument String ou alors d'une méthode statique valueOf(String). Le type peut également être une List<T> ou un Set<T> du moment que le type T soit instanciable depuis une chaîne de caractères.
- Pour injecter des objets de contexte pour des besoins spécifiques, il est possible d'annoter un paramètre de méthode avec l'annotation @Context. On peut ainsi injecter un objet de type javax.ws.rs.core.HttpHeaders, javax.ws.rs.core.UriInfo, javax.ws.rs.core.Request, javax.servlet.HttpServletRequest, javax.servlet.HttpServletResponse, javax.servlet.ServletConfig, javax.servlet.ServletContext ou javax.ws.rs.core.SecurityContext
À titre d'exemple, voici une classe représentant un service web REST gérant des comptes utilisateurs.
@Path("/user") public class UserWebService { // We inject an EJB to manage the users @EJB UserManager userManager; @Path("/") @GET public List<User> listUsers() { ... } @Path("/{name}") @PUT public boolean addUser( @PathParam("name") String name, @FormParam("email") String email, @FormParam("password") String password) { ... } @Path("/{name}") @POST public boolean modifyUser( @PathParam("name") String name, @HeaderParam("USER_PASSWORD") String password, @FormParam("newEmail") @DefaultParam("") String newEmail, @FormParam("newPassword") @DefaultParam("") String newPassword) { ... } @Path("/{name}") @GET public User consultUser( @PathParam("name") String name) { ... } @Path("/{name}") @DELETE public boolean deleteUser( @PathParam("name") String name) { ... } }
Sérialisation/désérialisation du corps
Une requête vers un service REST peut contenir un corps avec des données ; il en est de même pour la réponse.
Un corps de requête doit être converti en instance de classe Java pour être communiquée à la méthode du service web utilisée. Cette opération implique la désérialisation d'une séquence d'octets en un objet Java. Le corps de la réponse est exprimé sous la forme de l'objet retourné par la méthode, il doit être converti en octets pour être envoyé vers le client.
Types des corps
Les corps de la requête et de la réponse pour une méthode sont indiqués par les annotations suivantes :
- @Consumes("...") pour le format du corps de la requête
- @Produces("...") pour le format du corps de la réponse
Le format du média est indiqué sous la forme de type MIME : il comprend deux parties séparées par un / indiquant une famille de type et un sous-type. Les familles de type sont application, audio, example, image, message, model, multipart, text et video. Le registre complet des types maintenu par l'IANA est accessible ici.
Voici quelques exemples de type media :
- application/octet-stream pour des données binaires quelconques (type par défaut)
- text/plain; charset=UTF-8 pour du texte utilisant l'encodage UTF-8
- image/jpeg pour une image au format JPEG
- text/json ou application/json pour un format JSON
- text/xml ou application/xml pour un document XML
Il est possible de désigner un type avec un joker dans l'annotation @Consumes ou @Produces : par exemple @Consumes("text/*") signifie que la méthode accepte un corps de n'importe quel type texte ; on pourrait également avoir @Consumes("*/*") pour tout type. On peut également indiquer plusieurs types : @Produces({"application/json", "application/xml")} indique par exemple que la méthode peut produire soit du JSON, soit du XML selon la préférence de l'émetteur de la requête.
L'émetteur de la requête indique le type du corps qu'il envoie (en-tête HTTP Content-Type) ainsi que le type de la réponse qu'il préférerait recevoir (en-tête HTTP Accept). En fonction de ces informations, sa requête est distribuée à la méthode la plus appropriée : on peut ainsi avoir plusieurs méthodes réalisant le même travail mais ne divergeant que par le type de corps accepté et retourné.
Il est également possible d'annoter globalement une classe avec @Consumes et @Produces : l'annotation prend effet pour toutes ses méthodes sauf si une annotation est localement présente sur la méthode pour changer ce paramétrage. Si aucune méthode ne satisfait les critères de la requête, une erreur HTTP peut être renvoyée :
- Une erreur 415 Unsupported media type si le service web ne peut pas interpréter le format de données envoyé par le client
- Une erreur 406 Not Acceptable si le service web est incapable de produire un corps au format voulu par le client
Providers de sérialisation
Un provider a pour mission soit de convertir le corps d'une requête HTTP en un objet Java, soit de convertir un objet Java en corps de réponse HTTP. Une classe provider est annoté avec @Provider : elle doit implanter au moins une des deux interfaces MessageBodyReader<T> ou MessageBodyWriter<T> selon le sens de la conversion. Tous les providers sont automatiquement instantiés et sont testés pour savoir s'ils sont capables de réaliser une conversion : si c'est le cas, ils sont utilisés.
Un provider implantant MessageBodyReader<T> doit implanter les méthodes suivantes (et peut être annoté avec @Consumes pour indiquer les types qu'il peut accepter) :
- boolean isReadable(java.lang.Class<?> type, java.lang.reflect.Type genericType, java.lang.annotation.Annotation[] annotations, MediaType mediaType) afin d'indiquer s'il est capable de convertir une requête HTTP avec corps d'un certain type média en objet Java
- T readFrom(java.lang.Class<T> type, java.lang.reflect.Type genericType, java.lang.annotation.Annotation[] annotations, MediaType mediaType, MultivaluedMap<java.lang.String,java.lang.String> httpHeaders, java.io.InputStream entityStream) pour réaliser la conversion en lisant les octets du corps depuis l'InputStream fourni
Un provider implantant MessageBodyWriter<T> doit implanter les méthodes suivantes (et peut être annoté avec @Produces pour spécifier les types produits) :
- boolean isWriteable(java.lang.Class<?> type, java.lang.reflect.Type genericType, java.lang.annotation.Annotation[] annotations, MediaType mediaType) pour indiquer si le type avec la classe donnée est supporté
- long getSize(T t, java.lang.Class<?> type, java.lang.reflect.Type genericType, java.lang.annotation.Annotation[] annotations, MediaType mediaType) pour indiquer la taille prévisionnelle en octets du corps à écrire (-1 si la taille ne peut pas être prédite)
- void writeTo(T t, java.lang.Class<?> type, java.lang.reflect.Type genericType, java.lang.annotation.Annotation[] annotations, MediaType mediaType, MultivaluedMap<java.lang.String,java.lang.Object> httpHeaders, java.io.OutputStream entityStream) pour réaliser la conversion de l'objet Java en écrivant dans un OutputStream
Pour les usages normaux, il n'y a pas besoin de créer ses propres providers : on peut se contenter de ceux déjà fournis par défaut.
Providers par défaut
Il existe des providers fournis par défaut capables de (dé)sérialiser des corps de message.
Les types Java suivants sont supportés pour tous les types média MIME :
- InputStream : on retourne l'InputStream de la requête ou on fournit un InputStream où l'on lira les données binaires de la réponse
- byte[] : la conversion est directe puisqu'il n'y a aucune interprétation
- String : les octets du corps sont convertis en chaîne de caractères (ou le contraire) en utilisant le jeu de caractère UTF-8 ou celui indiqué explicitement dans le type média
- File : le contenu du fichier est transféré dans le corps de la réponse ou le corps de la requête est transféré dans un fichier temporaire
Si le type média MIME est text/plain, on peut convertir un objet Java en String en utilisant sa méthode toString() ou le construire en utilisant un constructeur prenant un String en paramètre ou une méthode statique T valueOf(String s).
Pour le type média MIME application/x-www-form-urlencoded qui consiste à exploiter les données d'un formulaire, un objet Java javax.ws.rs.core.MultivaluedMap pourra être utilisé : il s'agit d'une sorte de Map où chaque clé peut être associée à plusieurs valeurs.
Pour les types média XML (application/*+xml, text/*+xml), un objet Java org.w3c.dom.Document avec une représentation du document en utilisant un arbre DOM peut être utilisé.
Pour les types média XML et JSON (application/*+xml, text/*+xml, application/*+json, application/*+fastinfoset, application/atom+*), des classes annotées en utilisant JAXB peuvent être employées. Cela nous permet de définir une représentation univoque XML ou JSON pour des classes Java personnalisées. Ainsi lorsque nous écrivons nos propres classes Java pour un modèle qui sont manipulées par un service web, nous préférerons utiliser JaxB plutôt que d'avoir à recréer pour chacune de ces classes un Provider spécifique.
Utilisation de JAXB
JAXB (Java Architecture for XML Binding) permet de convertir des objets Java en une représentation XML et vice-versa. L'API JAXB est également fournie avec Java SE.
L'utilisation typique de JAXB réside dans la création de beans Java POJO (avec constructeur sans argument et getters/setters) puis leur annotation pour définir comment les instances de celles-ci doivent être représentées en XML. Cela permet ainsi de communiquer des objets transformés en XML entre des programmes Java ou même des programmes utilisant des technologies différentes (Java, .Net, C++...). Il est possible en effet de générer un schéma XML pour chacune des classes grâce à l'utilitaire schemagen, ce schéma pouvant être transmis au programme correspondant afin de l'aider à reconvertir le document XML en objet. Il existe également un utilitaire xjc réalisant l'opération inverse de conversion de schémas en classes Java annotées.
Les différentes annotations utilisables sont définies dans le paquetage javax.xml.bind.annotation.
Voici les principales annotations appliquables sur les classes :
- @XmlRootElement(name="name", namespace="namespace") permet d'indiquer que l'objet est représenté sous la forme d'un élement XML. Les attributes name et namespace sont optionnels et par défaut sont dérivés du nom de la classe et son paquetage.
- @XmlType permet d'indiquer des informations de mapping XML de le classe avec une factoryClass et factoryMethod pour construire une instance de la classe (la méthode ne doit pas avoir d'argument), un name pour indiqué le nom du schéma XML ou un propOrder qui est un tableau de String indiquant l'ordre des différents sous-élements de la classe
- @XmlAccessorType(...) indique la politique de sérialisation par défaut des champs et propriétés. La valeur XmlAccessType.FIELD indique la sérialisation de tous les champs par défaut, XmlAccessType.NONE permet de ne rien sérialiser par défaut, XmlAccessType.PROPERTY sérialise uniquement les propriétés (matérialisés par un couple de getter/setter) tandis que XmlAccessType.PUBLIC_MEMBER sérialise tous les membres publics (champs et couples getter/setter).
- @XmlAccessorOrder(XmlAccessOrder.ALPHABETICAL) indique que les champs sont listés dans l'ordre lexicographique
On peut également définir des annotations sur les champs ou getters afin d'indiquer la manière dont ces sous-éléments de la classe doivent être représentés :
- @XmlElement indique qu'un élément XML doit être utilisé pour représenter le champ. On peut indiquer son nom (attribut name), s'il peut être nul (nillable), s'il est obligatoire (required), s'il a une valeur par défaut (defaultValue).
- @XmlAttribute indique que le champ doit être représenté non pas sous la forme d'un sous-élément de l'élement correspondant à l'objet mais sous la forme d'un attribut. Un attribut ne peut être qu'un type simple (String, primitif, Date...) ou une liste de type simple étant donné qu'un type complexe ne peut être représenté par une chaîne de caractères.
- @XmlTransient interdit la sérialisation du champ ou getter ainsi annoté
- @XmlID permet d'annoter un champ pour indiquer qu'il s'agit d'un identificateur unique de l'objet (le type du champ est obligatoirement un String)
- @XmlIDREF permet d'indiquer que le champ doit être conservé sous la forme d'une référence (et pas d'une copie de l'objet)
- @XmlElementRef indique que le nom de l'entité utilisée dépend du type réel à l'exécution du chhamp
- @XmlElementWrapper(name="name") est utiisée pour les champs de type collection pour indiquer que toutes les valeurs doivent être encapsulées à l'intérieur d'un élément dont on indique le nom
Voici un exemple d'annotations utilisées pour une classe User :
@XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) // fields are serialized public class User { @XmlID @XmlAttribute private String name; // automatically serialized private String email; @XmlTransient // not serialized for security purposes private String password; @XmlElementWrapper(name="friends") @XMLIDREF // we save only the names of the friends (and not a copy of the user) private List<User> friends; @XmlAttribute // saved as a XML attribute private Date modificationDate; }
Il est possible de cumuler l'usage d'annotations JAXB avec des annotations JPA pour la persistance des objets en base.
On peut ensuite tester le fonctionnement de la sérialisation XML en dehors de l'utilisation d'un service web :
User user = ...; JAXBContext jc = JAXBContext.newInstance(User.class); Marshaller m = jc.createMarshaller(); m.marshal(user, System.out);
Cela pourrait produire un résultat ressemblant à ceci :
<User id="foo" modificationDate="20151007T23:45:10Z"> <email>foo@example.com</email> <friends> <User id="toto" /> <User id="titi" /> </friends> </User>
On pourrait aussi récupérer un utilisateur enregistré dans un fichier :
JAXBContext jc = JAXBContext.newInstance(User.class); Marshaller u = jc.createMarshaller(); User user = (User)u.unmarshal(new FileInputStream("user.xml"));
Il est possible également d'utiliser un format de sortie JSON :
JAXBContext jc = JSONJAXBContext.newInstance(User.class); Marshaller m = jc.createMarshaller(); JSONMarshaller marshaller = JSONJAXBContext.getJSONMarshaller(m); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); marshaller.marshallToJSON(user, System.out);
Le résultat ressemblerait à ceci :
{"@id": "foo", "email": "foo@example.coom", "friends": ["toto", "titi"], "@modificationDate": "20151007T23:45:10Z"}
Création d'un client REST
L'API client REST est basée sur un pattern Builder. On obtient d'abord un client REST que l'on paramètre en chaînant des appels de méthodes pour indiquer l'adresse du service web, l'adresse de la ressource, les paramètres de la requête, le type média souhaité.
A partir d'un Client, on chaîne des méthodes pour obtenir :
- un WebTarget après l'utilisation de la méthode target(String uri) pour cibler une adresse de service web
- on paramètre le WebTarget avec path(String p) pour ajouter un segment de chemin, queryParam(String name, Object... values) pour ajouter des paramètres en section query, on peut également résoudre une adresse template en injectant des valeurs avec resolveTemplate(String name, Object value)
- l'étape suivante consiste à appeler la méthode request() de WebTarget qui peut prendre en paramètre un type média pour indiquer quel est le format de données souhaité : on obtient un Invocation.Builder
- on peut rajouter sur l'Invocation.Builder un cookie ou un header avec les méthodes éponymes ; on peut aussi indiquer un objet CacheControl avec cacheControl(CacheControl cacheControl)
- on appelle ensuite la méthode correspondant à la méthode HTTP sur l'Invocation.Builder : get(), post(), put()... on obtient un objet Invocation
- l'appel de la méthode invoke() sur Invocation permet d'obtenir une réponse avec de nombreux getters pour lire les différents attributs de la réponse (en-têtes, date de dernière modification, taille, code statut...)
- sur Response, la méthode readEntity(Class<T> entityType) permet de convertir la réponse en un objet Java en utiisant les providers configurés
Voici un exemple pour récupérer un utilisateur depuis le service web précédemment écrit (on utilise la méthode raccourcie get(Class<T> cl) pour obtenir directement une entité depuis l'Invocation sans passer par Response) :
Client client = ClientBuilder.newClient(new ClientConfig()); User user = client.target("http://user.example.com/") .path("user/foo") // .queryParam("key", "value") // to add a query parameter // .cookie("key", "param") // to add a cookie // .header("key", "value") // to add a HTTP header .request(MediaType.APPLICATION_JSON) .get(User.class);
Des exceptions peuvent être levées en cas de problème, parmi elles :
- ResponseProcessingException en cas de problème pour convertir le corps de la réponse en objet Java
- ProcessingException s'il y a une erreur d'E/S (connexion inaccessible, coupée...)
- WebApplicationException si le code de statut retourné révèle une erreur (code non-compris entre 200 et 299)