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

Une bonne architecture pour une application complexe impose de séparer les différentes composantes. Ainsi celles-ci peuvent évoluer indépendamment sans introduire de dépendances inutiles. D'autre part, elles peuvent être hébergées sur des machines différentes, voire réparties sur plusieurs machines. Néanmoins ces composantes doivent continuer à communiquer : pour cela nous pouvons exposer une API REST.

Ce type d'API est basé sur le protocole HTTP avec l'usage de ses différentes méthodes (GET, POST, PUT, DELETE...) associée à un chemin désignant la ressource. Des paramètres supplémentaires peuvent être passés dans le corps de la requête par exemple en utilisant le format JSON. Une composante A contactant une autre composante B en utilisant une API REST est complètement agnostique de l'architecture mise en oeuvre pour le fonctionnement de B. Le serveur web de B répondant aux requêtes de A peut n'être qu'une façade pour une architecture très complexe impliquant de nombreuses sous-composantes (bases de données réparties, appels à d'autres services web...).

Une approche de plus en plus populaire est de faire appel à des composantes hébergées par des tiers (Software As a Service : SaaS) ; on peut également gérer soi-même une composante logicielle mais laisser le soin à un prestataire externe de gérer l'infrastructure (réseau, matériel) avec une approche Infrastructure As a Service (IaaS) voire également le système d'exploitation et d'éventuels frameworks avec l'approche Platform As a Service (PaaS). Si ce sont juste les données qui sont gérées de façon externe, on parle de Data as a Service (DaaS). Ces approches d'externalisation utilisant l'informatique en nuage permettent dans certaines situations de simplifier le développement et de réduire les coûts d'acquisition et de maintenance (TCO). Mais ce n'est pas systématique (le calcul des coûts est souvent complexe) et la dépendance sur une solution externe peut induire des inconvénients liés à la perennité, à la fiabilité de la solution, à la connectivité réseau ainsi qu'à la confidentialité des données. Il faut donc toujours prévoir des solutions de repli en cas de problème.

Quelques exemples de services nébuleux sur Internet :

HttpUrlConnection

La classe HttpURLConnection est fournie dans l'API standard Java et permet de réaliser un client HTTP pouvant interroger un serveur web (et donc contacter un service web). L'usage de cette classe nous obligerait à manipuler manuellement les données à envoyer dans le corps de la requête (conversion en JSON, XML...) ainsi que pour le corps de la réponse. Ce travail peut être un peu rébarbatif. La plupart des frameworks web nous proposent une API spécifique pour contacter un service web.

Web scraping

Le web scraping désigne un procédé consistant à interroger automatiquement un site web dont la réponse est initialement destinée à un humain. La réponse aux requêtes est généralement sous la forme de pages HTML. Il faut donc ensuite analyser la page pour en extraire les informations importantes. L'application Weboob (codée en Python) est un exemple d'implantation de cette pratique.

Il s'agit d'une solution à employer que s'il n'existe pas d'autres alternatives avec une API plus formalisée. Outre la difficulté plus ou moins importante à analyser les données (dans certaines situations de simples expressions régulières peuvent faire l'affaire mais dans d'autres cas, c'est plus complexe), il n'existe aucune pérennité quant à la procédure d'analyse, le site web pouvant faire évoluer à tout moment la présentation de ses pages. D'autre part cette pratique n'est pas toujours accueillie avec bienveillance par les webmasters des sites (surtout si les requêtes sont trop fréquentes).

Services web SOAP

SOAP est un protocole permettant de gérer des services web (mais pas uniquement car ce protocole peut s'accomoder d'autres protocoles pour le transport des données tel que SMTP). Il est néanmoins compliqué à mettre en oeuvre même s'il présente certains avantages comme l'intégration de schémas pour valider le format des requêtes et réponses. Java Enterprise Edition propose une API pour contacter un service SOAP en tant que client ainsi que pour générer des classes souches adaptées pour contacter un service SOAP. Pour cela le service web émet une définition des appels qu'il propose avec le format WSDL (Web Service Definition Language). Les différents services peuvent être référencés dans des annuaires spécifiques en utilisant UDDI (Universal Description Discovery and Integration).

Le protocole SOAP tombe de nos jours en désuétude avec la popularité croissante de l'approche REST. Il s'agit d'utiliser de simples appels à des méthodes HTTP avec méthode et ressource et expression de données en XML ou JSON.

Services web REST

Les services REST (Representational state transfer) est un type d'architecture pour les services web plus en vogue actuellement. Pour être qualifiée de REST, une architecture doit respecter certaines contraintes : elle doit fonctionner selon un mode client-serveur sans état (protocole HTTP), elle doit proposer des services de mise en cache, une séparation de ressources individuelles et une interface uniforme. Enfin le serveur peut également envoyer des scripts à exécuter côté client (approche Code-On-Demand) ; cette approche est cependant plutôt rare.

REST exploite la nature hypermédia des ressources envoyées : ainsi une ressource peut contenir des liens vers d'autres ressources à récupérer et ainsi de suite. Ainsi par exemple un appel d'API pouvant permettre d'obtenir la liste d'amis d'une personne sur un réseau social pourra retourner ces amis avec leur nom ainsi qu'une adresse vers une ressource permettant d'obtenir de plus amples informations sur la personne (caractéristiques de contact, photo d'avatar...). Cette approche suit la philosophie hypertexte du web mais présente l'inconvénient de devoir nécessiter de nombreuses requêtes si l'on cherchait à obtenir des informations sur toutes ces personnes ; cela peut poser des problèmes de volume d'échange de données et de latence. Néanmoins, il est à noter que la réutilisation possible de connexion et le pipelining en HTTP peuvent palier partiellement à ces problématiques.

Un service web REST peut accepter des requêtes avec des données dans leur corps et peut retourner une réponse avec un corps. Les formats de données utilisés sont généralement :

On pourra trouver un répertoire avec beaucoup de sites proposant des API REST sur le site programmableweb.com.

Correspondance avec le modèle

Une approche envisageable pour la conception d'un service web REST et de se rapprocher du modèle de données manipulées. Par exemple pour un réseau social gérant des utilisateurs avec des messages échangés entre-eux, il peut être logique d'utiliser deux appels REST suivants :

GET	/user/:id/profile
GET	/user/:id/friends
GET	/user/:id/messages
GET	/message/:id
...

Chaque appel peut comporter des paramètres supplémentaires passés en section query en mode url encoded. Par exemple, il serait possible de paramétrer la récupération des messages envoyés d'un utilisateur en indiquant un intervalle de date à appliquer.

Les requêtes GET ne doivent être destinées qu'à récupérer de l'information sans modification. Pour toute création ou mise à jour de données, on utilise la méthode POST, voire PUT ou DELETE.

Par exemple, on pourra mettre en place les appels suivants :

POST	/user/create
POST	/user/createFriendship
POST	/message/create
DELETE  /message/:id
POST	/message/:id
...

Authentification

L'authentification sur une API REST répond généralement à différentes problématiques :

L'authentification se fait généralement par un jeton (token) secret généré aléatoirement (généralement appelé clé d'API), ce jeton devant être gardé secret. Le jeton est souvent communiqué dans l'en-tête de la requête HTTP. Il peut être révocable.

Une API A peut quelquefois avoir à contacter une autre API B pour le compte d'un de ses utilisateurs. A doit donc acquérir des droits que possède l'utilisateur auprès de B. Le protocole OAuth répond notamment à cette problématique en permettant une délégation d'autorisation : l'utilisateur autorise A à utiliser ses données sur l'API B.

Client web REST avec Play

Nous examinons maintenant l'utilisation du client REST disponible avec Play. Play reposant sur un serveur web avec un fonctionnement non-bloquant, le client REST proposé propose des appels asynchrones. Nous nous intéressons d'abord à l'asynchronisme apporté par Java 1.8.

CompletableFuture

La classe Future<V> a été introduite dans Java 1.5. Son objectif est de pouvoir d'exécuter des tâches en mode asynchrone. On soumet la tâche à réaliser sous la forme d'une instance de Runnable ou Callable<V> (on peut maintenant utiliser des expressions lambda sans argument pour Runnable etCallable<V>) à un ExecutorService (que l'on peut instantier à l'aide des méthodes statiques de Executors).

Par exemple, imaginons que nous souhaitions trouver le maximum d'un tableau d'entiers. Cela nécessite $n$ comparaisons pour un tableau de $n$ entiers.

Nous pouvons écrire le code suivant soumis à un ExecutorService :

int[] tab = ...;
ExecutorService es = ExecutorService.newCachedThreadPool();
Future<Integer> result = es.submit( () -> {
	int max = Integer.MIN_VALUE;
	for (int i = 0; i < tab.length; i++)
		if (tab[i] > max) max = tab[i];
	return max;
});

Le Future<Integer> possède une méthode get() dont l'appel bloque jusqu'à ce que le résultat soit calculé. Si par exemple nous avons besoin de ce résultat pour un calcul ultérieur, nous pouvons l'appeler et ensuite exécuter notre calcul. Il y a aussi une version get(long timeout, TimeUnit unit) qui lève un TimeoutException si la limite de temps est dépassée. On peut aussi appeler une méthode boolean isDone() qui retourne immédiatement si le résultat a été calculé. Le calcul peut également être annulé avec la méthode cancel(boolean interrupt) : il est conseillé de passer le booléen true est d'appeler régulièrement la méthode statique Thread.interrupted() dans notre code pour vérifier si une interruption a été demandée.

Ici le calcul est réalisé d'un seul tenant en monopolisant une thread. On pourrait également décomposer le travail en deux sous-tâches qui peuvent être exécutées parallèlement. Le travail sera plus rapide si nous disposons d'un processeur avec au moins deux coeurs. On va utiliser pour cet objectif des CompletionStage :

public static int computeMax(int[] tab, int start, int stop)
{
	int max = Integer.MIN_VALUE;
	for (int i = start; i < stop; i++)
		if (tab[i] > max) max = tab[i];
	return max;
}

public static CompletableFuture<Integer> computeMax2(int[] tab)
{
	CompletableFuture<Integer> stage1 = CompletableFuture.supplyAsync(() -> { return computeMax(tab, 0, tab.length / 2); });
	CompletableFuture<Integer> stage2 = CompletableFuture.supplyAsync(() -> { return computeMax(tab,tab.length/2+1, tab.length); });
	return stage1.thenCombine(stage2, (Integer max1, Integer max2) -> { return Math.max(max1, max2); });
}

public static void main(String[] args)
{
	long start = System.nanoTime();
	int[] tab = ...
	for (int i = 0; i < tab.length; i++)
		tab[i] = i;
	computeMax2(tab).thenAccept(v -> { System.out.println("Computed max: " + v + " in " + (System.nanoTime() - start)/10e9 + " s");});
}

CompletionStage<V> dispose d'une classe dérivée CompletableFuture<V> avec une méthode statique supplyAsync pour obtenir des CompletableFuture<V> à partir d'une méthode sans argument ou avec argument. On chaîne ensuite les CompletionStage entre-eux par des appels de méthodes :

L'API est très riche et comporte d'autres méthodes pour chaîner des stages entre-eux et avec des méthodes ; nous nous sommes plutôt ici concentrés sur les méthodes prenant un paramètre (Function) ou deux paramètres (BiFunction) et retournant une valeur. Il existe égalemnent des versions ne retournant pas de valeur (void) avec Runnable (0 paramètre), Consumer (1 paramètre) et BiConsumer (2 paramètres).

Action retournant un CompletionStage

Play fonctionne selon un mode asynchrone : les actions doivent être exécutées rapidement et ne pas bloquer de thread. Pour cela, on retourne un CompletionStage<Result> calculé de façon asynchrone et non un Result calculé de façon bloquante`. On pourra utiliser l'API CompletionStage pour le chaînage des différentes méthodes et on prendre soin que chacune des méthodes soit exécutée rapidement (quite à découper en plusieurs méthodes).

On pourra lire à ce sujet cette page de la documentation.

Utilisation de la classe WSClient

WSClient est une classe de l'API Play implantant un client pour des services web reposant sur le protocole HTTP. Il est bien sûr également possible de l'utiliser pour réaliser du web scraping.

On utilise une instance de WSClient obtenue par injection dans un champ :

@Inject
WSClient client;

On chaîne ensuite les appels de méthodes à partir du WSClient : l'objectif étant au final d'obtenir un CompletionStage<WSResponse> en retour. On peut ensuite avec l'API asynchrone Java chaîner à ce CompletionStage d'autres méthodes à exécuter pour extraire certains résultats de la réponse, les combiner avec d'autres données obtenues via une autre API...

Voici les principales méthodes à appeler pour arriver à un CompletionStage<WSResponse> :

client
	.url(url) // on indique d'abord l'URL du service à interroger
	.setHeader("authToken", "authValue") // on peut ajouter un en-tête HTTP spécifique
	.setQueryParameters("key1", "value1") // on peut ajouter des paramètres de requête transmis dans l'URL
	.setRequestTimeout(2000) // on peut ajouter un temps limite en millisecondes (si le timeout est atteint on obtient une exception)
	.setContentType("application/json") // pour indiquer le type de données à poster
	.post(jsonNode) // pour poster les données (cela peut être par exemple un JsonNode, du texte, un tableau d'octets, un InputStream...)
	// ou alors on peut utiliser la méthode get() sans argument
	// après l'appel à get() ou post(...), on obtient un CompletionStage<WSResponse>

Il faut ensuite analyser la réponse du serveur en chaînant l'appel thenApply...