image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

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

Services web SOAP

Introduction à SOAP

SOAP avec jax-ws

Pour webservicifier une classe, il est nécessaire :

  1. 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)
  2. 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)
  3. 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 :

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 :

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

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

Les annotations

À 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 :

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 :

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 :

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) :

Un provider implantant MessageBodyWriter<T> doit implanter les méthodes suivantes (et peut être annoté avec @Produces pour spécifier les types produits) :

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 :

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 :

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 :

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 :

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 :