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

Play est un framework facilitant le développement d'applications web en Java (mais aussi en langage Scala). Il favorise l'emploi d'un paradigme MVC (Modèle-Vue-Contrôleur).

Le protocole HTTP

HyperText Transport Protocol est un protocole applicatif fonctionnant au dessus du protocole de transport TCP (lui-même au dessus du protocole IP). L'usage du protocole TCP garantit la retransmission des données perdues. Par contre nativement TCP ne garantit pas l'intégrité des données transmises ni leur confidentialité (elle peuvent être interceptées et réécrites par un routeur). Il est nécessaire pour ce faire d'utiliser le protocole Transport Layer Security (TLS) (anciennement SSL) s'intercalant entre HTTP et TCP (HTTPS). Il est recommandé d'utiliser TLS ; la principale difficulté lié à son usage concerne la nécessité d'utiliser un certificat signé par une autorité de confiance ; il existe cependant des initiatives pour faciliter ce procesus (Let's Encrypt par exemple).

Le protocole HTTP évolue au cours de ses versions. La version majeure la plus récente est la version 2.0 qui a introduit certaines innovations notamment pour le multiplexage des flux ou la compression de données. En revanche le protocole devient plus complexe ce qui rend son implantation plus délicate.

On privilégiera donc l'usage d'un framework qui nous décharge de toutes les problématiques de bas-niveau du protocole HTTP pour se concentrer plutôt sur la génération du contenu des réponses aux requêtes.

Le protocole HTTP fonctionne selon un principe requête-réponse : le client émet vers le serveur une requête composée d'une méthode (parmi GET, HEAD, POST, PUT...), une ressource (qui correspond au chemin présent sur une URL, par exemple : /my/resource pour http://example.com/myresource), des champs d'en-tête (dont la signification est normalisée par le protocole) ainsi qu'éventuellement un corps avec des données à envoyer (par exemple un fichier).

Le serveur répond à cette requête par une réponse comprenant des champs d'en-tête (toujours un dictionnaire de clé-valeur) ainsi qu'un corps de réponse (généralement la ressource demandée).

Le protocole HTTP est fondamentalement un protocole sans état. Cela signifique que chaque requête réalisée est indépendante de toute autre ; les données d'une requête doivent donc intégrer toutes les informations contextuelles nécessaires (notamment pour un processus nécessitant plusieurs requêtes).

Un serveur HTTP peut retourner n'importe quel type de données, les données les plus courantes étant des pages HTML ainsi que des ressources associées (feuille de style CSS, image, vidéo...). Un client HTTP peut être un navigateur web utilisé par un utilisateur humain ou alors une application réalisant des appels distants vers une API. Bien sûr dans chacun des cas, on utilisera des données présentées differemment.

Présentation de Play

Composants principaux

Play est un framework web reposant sur différents composants assurant chacun une tâche spécifique :

Installation

La distribution du framework peut être téléchargée sur le site de Play ; on privilégiera la version offline regroupant l'ensemble des composants.

Play repose sur le système de construction de projets SBT (très utilisé notamment pour les projets en langage Scala).

Une fois l'archive téléchargée, on la décompresse dans le répertoire de son choix. Il est conseillé ensuite d'ajouter dans la variable d'environnement PATH le chemin vers le répertoire d'exécutables de Play. Cela peut être fait par exemple en éditant le fichier .bash_rc de son répertoire $HOME. On peut alors lancer directement l'exécutable activator.

La création d'un nouveau projet se fait avec la commande activator new : la commande est interactive et demande des détails sur le projet. La commande activator ui permet de lancer l'interface graphique d'administation de Play sous la forme d'un serveur web tournant sur le port 8888 par défaut. Un mode console est également disponibe avec la command activator.

Un projet Play comporte les sous-répertoires et fichiers suivants :

Le routeur

Routage basique

Lorsque le serveur Play reçoit une requête HTTP, il doit déterminer l'action à réaliser (afficher une liste de fichiers, un formulaire, envoyer une ressource spécifique...). Il analyse la section path de l'URI pour distribuer la requête vers l'action appropriée.

La page consacrée au routage de Play nous explique l'usage du routeur.

Le routeur par défaut consulte les routes du fichier conf/routes. Chaque route est spécifiée avec le nom de la méthode HTTP, le chemin demandée puis un descriptif de l'action à réaliser (en indiquant où la méthode à exécuter se situe).

Par exemple, si l'on souhaite réaliser un bloc-notes en ligne, on peut indiquer une route pour consulter la liste de toutes les notes :

GET /notepad/					controllers.Notepad.listNotes()

Lorsque l'utilisateur du site demandera l'URI http://example.com/notapad/, nous appelerons la méthode Notepad.listNotes() qui renverra une réponse avec la liste des notes présentes.

Les routes peuvent être plus complexes en intégrant des paramètres passées à la méthode. Par exemple, pour afficher une note d'identificateur id (un long identifiant la note), on pourra ajouter la route suivante :

GET /notepad/:id				controllers.Notepad.showNote(id: Long)

Ici le routeur injectera le paramètre id lorsqu'il appelera la méthode. :id va attraper l'identificateur de la note (en fait toute expression ne comprennant pas un caractère /). On peut indiquer une expression régulière personnalisée pour attraper un paramètre :

GET /notepad/$id<[0-9]+>	controllers.Notepad.showNote(id: Long)

Une valeur par défaut peut être fournie à une méthode. Par exemple pour ajouter une route récupérant la 1ère note ajoutée (d'identificateur 0) :

GET /notepad/first			controllers.Notepad.showNote(id = 0)

Les routes sont toujours évaluées dans l'ordre de leur déclaration : la première route acceptable pour une requête est utilisée.

Utilisation de paramètres query

Une URI de requête comprend une section query préfixée par un caractère ? pour indiquer un dictionnaire de paramètres (liste de clé-valeur). Une section query est de la forme key1=value1&key2=value2&... (certains caractères sont considérés spéciaux et doivent être spécialisés utilisant l'encodage url-encoded. Le routeur a la possibilité de capturer automatiquement ces paramètres et de les injecter comme arguments aux méthodes appelées.

Ainsi par exemple si l'on veut afficher une liste de notes depuis l'identificateur start jusqu'à l'identificateur stop, on peut utiliser la route suivante :

GET /notepad/					controllers.Notepad.listNoteRange(start: Long ?= 0, stop: Long ?= -1)

Ainsi par exemple si on demande http://example.com/notepad/?start=10&stop=20, nous afficherons la liste des notes depuis la note 10 jusqu'à la note 20. Nous avons aussi indiqué des paramètres par défaut qui sont utilisés si nous n'avons pas indiqué dans la section query le paramètre. Ainsi par exemple, http://example.com/notepad/?start=10` utilisera start=10 et stop=-1 (-1 pouvant être une valeur spéciale indiquant la dernière note).

DSL

Il est possible de définir programmatiquement des routes en utilisant un Domain Specific Langugage (DSL). Une page de la documentation dermr Play y est consacrée.

Le contrôleur

Ressources statiques

Un contrôleur spécifique existe pour traiter les ressources statiques. En pratique on les disposera dans un répertoire du projet (par exemple public) et on les enverra lorsque l'URI aura un certain format. Voici un exemple de route à spécifier (demander le chemin /assets/script.js conduira à renvoyer le fichier /public/script.js du projet) :

GET  /assets/*file        controllers.Assets.at(path="/public", file)

Écriture d'un contrôleur

Un contrôleur est une classe qui hérite de Controller et dont les paramètres d'initialisation et le cycle de vie sont régis par les annotations spécifiées. Nous n'avons jamais à instantier un contrôleur : Play s'en charge selon les annotations indiquées.

Deux types de contrôleurs peuvent être employés :

Pour en savoir plus sur l'injection de dépendances

Il est possible d'écrire une ou plusieurs méthodes dans un contrôleur ; chacune des méthodes est une action spécifique qui sera réalisée. Une action peut prendre des arguments et retourne toujours un objet de type Result.

Pour obtenir un objet de type Result, on appelle une méthode définie dans Controller selon le type de résultat que l'on souhaite retourner. Si tout se passe bien, on appelle la méthode ok avec en paramètre le contenu du résultat (qui peut être une chaîne de caractères, un tableau d'octets, un fichier, un InputStream...). Cela correspond à un code de status HTTP 200. Si cela se passe moins bien, on peut utiliser d'autres méthodes pour des status différents (liste non exhaustive) :

Code de statut Méthode correspondante
200 ok…
303 redirect…
307 temporaryRedirect…
308 permanentRedirect…
400 badRequest…
401 unauthorized…
403 forbidden…
404 notFound…

Format de données

Les données peuvent être échangées entre le client et le serveur en utilisant différents formats. Il peut s'agir de formats binaires, généralement pour des ressources statiques (image PNG, image JPEG, vidéo Matroska...) ou alors de format texte. Pour un format texte, il est nécessaire de préciser le jeu de caractères (charset) à utiliser. Dans la majorité des cas, on privilégiera l'utilisation du charset UTF-8 de la famille Unicode. Il permet de représenter tous les caractères définis par le standard Unicode (virtuellement tout alphabet de langues mortes et vivantes) sur un nombre variable d'octets et reste rétro-compatible avec le jeu ASCII (caractères latins non-accentués) pour lequel les caractères sont codés sur un octet.

Pour du texte, de nombreux formats peuvent être employés. Parmi les plus courants :

Play supporte n'importe quel format et offre des facilités dans son API pour manipuler du XML, du JSON et du YAML. On privilégiera donc plutôt ces formats.

Nous pouvons être confronté aussi bien à la manipulation de ces formats en entrée (corps de la requête) qu'en sortie (corps de la réponse). En entrée, il est possible d'appeler dans une action du contrôleur la méthode request().body() pour obtenir un objet RequestBody. On peut ensuite appeler les méthodes asBytes(), asFormUrlEncoded(), asJSON(), asMultipartFormData(), asText(), asXML()... selon le type de format que l'on attend.

On peut également créer un objet correspondant au format souhaité et le renvoyer comme résultat. Voici un exemple tiré de la documentation Play :

public Result sayHello() {
    ObjectNode result = Json.newObject();
    result.put("exampleField1", "foobar");
    result.put("exampleField2", "Hello world!");
    return ok(result);
}

Il est même possible de magiquement convertir des objets Java en JSON ou XML et vice-versa. Ces fonctionnalités sont basées sur la bibliothèque de sérialisation Jackson embarquée dans Play. Par exemple pour créer un objet JSON à partir d'un POJO (Plain Old Java Object), on peut utiliser la méthode statique JsonNode JSon.toJSon(Object object). Le contraire est également possible avec T Json.fromJson(JsonNode node, Class<T> clazz). Bien sûr certaines subtilités de fonctionnement sont à connaître... on pourra se reporter à la documentation de Jackson à cet égard.

Stockage de données sur le client

Le client peut stocker des données de différentes façons. La façon la plus classique de demander au client de stocker des données consiste à lui transmettre un cookie. Il s'agit d'un dictionnaire de clé-valeur qu'on lui demande de stocker jusqu'à une certaine date d'expiration et qu'il renverra systématiquement au serveur pour chaque requête réalisée. Etant donnée que ces données sont envoyées à chaque requête, un cookie est utilisé généralement pour mettre en place un système de session. Il ne faut pas y stocker des données volumineuses.

Pour stocker un cookie on utilise la méthode session(String key, String value). Notons que l'entrée est stocké sous la forme d'un cookie signé (mais non chiffré) que l'utilisateur ne peut pas en théorie modifier sans connaître la clé d'authentification secrète du serveur. Il est également possible la méthode flash ne réalisant le stockage sur un cookie que pour la requête suivante (stockage de court terme) : c'est notamment utile pour gérer des formulaires en plusieurs parties nécessitant plusieurs requêtes.

Pour demander le stockage de données plus volumineuses sans que celles-ci soient renvoyées au serveur, HTML5 propose une API spécifique. Son usage implique l'usage de JavaScript et ne concerne pas le présent document.

Stockage de données sur le serveur

Le stockage sur le serveur peut être réalisé en mémoire vive ou alors dans une base de données (généralement relationnelle). A cet effet, on peut utiliser Java Persistence API.

Les services

Les services sont des composants utilisés par les contrôleurs pour réaliser des tâches spécifiques. Un service peut être utilisé par un ou plusieurs contrôleurs et possède un cycle de vie personnalisé. Ils sont généralement utilisés pour accéder aux données de l'application. On peut ainsi se contenter d'écrire des contrôleurs qui ne se préoccupent que des problématiques de traitement de requêtes et retour de résultat en déléguant le travail sur les dnnées aux services.

Le cache

Le cache permet de sauvegarder temporairement des données en mémoire afin d'éviter à les recalculer par la suite. Cette page nous explique comment nous servir du cache.

  1. On peut tout d'abord ajouter dans la configuration le support de différents caches : play.cache.bindCaches = ["db-cache", "user-cache", "session-cache"]
  2. On peut ensuite injecter comme champ d'un contrôleur un cache à utiliser : @Inject @NamedCache("session-cache") CacheApi cache;
  3. La méthode cache.set(String key, Object value, int timeout) permet de rajouter une entrée dans le cache (avec un timeout en secondes)
  4. La méthode cache.get(String key) permet de récupérer une valeur du cache (retourne null si cette valeur n'existe pas)
  5. La méthode cache.remove(String key) permet de supprimer une entrée du cache
  6. Une action peut être annotée pour que son résultat soit mis automatiquement en cache :
@Cached(key = "homePage")
public Result index() {
    return ok("Hello world");
}

La vue

Des templates peuvent être utilisés pour retourner une réponse.

Format de templates

L'essentiel à savoir sur l'utilisation des templates est indiqué sur cette page de la documentation.

Il est possible d'utiliser dans un template du code HTML classique ou n'importe quel autre format textuel. La seule précaution à prendre est de déspécialiser le caractère @ qui est utilisé pour les instructions du template. Pour la déspécialisation, il faut doubler @ (par exemple on écrira l'adresse email foo@@example.com).

Les templates doivent être ajoutée dans le répertoire views et suivent la convention de nommage views/templateName.scala.html. Un tel template va conduire l'activator à créer une classe Scala views.html.Application.templateName : il suffira ensuite d'appeler sa méthode render avec des paramètres adaptés pour calculer un rendu pour le template (par exemple depuis un contrôleur). Le type de retour dépend du suffixe du template (Html par exemple).

Les instructions de template s'inspirent de la syntaxe du langage Scala (différente de celle de Java).

Philosophiquement, un template fonctionne comme une méthode, il faut donc tout d'abord indiquer ses paramètres. On peut aussi indiquer des commentaires entourés par @* et *@. On peut ensuite inclure des expressions Scala préfixée par une arobase. Les expressions sont automatiquement formatées selon le mode de sortie utilisé (par exemple en HTML les caractères < ou > sont déspécialisés en &lt; et &gt;.

Par exemple, si l'on veut afficher une liste de notes (avec un auteur, une date et un corps de message), on pourrait utiliser le template suivant :

@import _root_.model._
@(notes: List[Note])

@display(note: Note) = {
	Here is a note from @note.author (posted on @note.timestamp): <br />
	<pre>
		@note.content
	</pre>
}

<ul>
@for(note <- notes) {
	<li>
		@display(note)
	</li>
}
</ul>

Internationalisation (i18n)

Pour internationaliser une application, il faut tout d'abord spécifier dans le fichier de configuration conf/application.conf les langues supportées :
play.i18n.langs = [ "en", "en-US", "fr" ]
On créé ensuite des fichiers conf/messages.LANG (messages.en, messages.fr...) adaptés aux langues supportées (sachant qu'un fichier conf/messages doit indiquer les messages par défaut).

Le fichier est de type INI avec une entrée par ligne avec cle=valeur :

message1=une valeur pour le message 1
message2=une valeur pour le message 2 avec une 1ère variable à substituer {0} et une seconde variable à substituer {1}

On peut ensuite récupérer le message2 dans un contrôleur avec Messages.get("message2", "subst1", "subst2") ; les paramètres supplémentaires sont les valeurs à substituer.

Il est bien sûr possible d'utiliser également les messages dans un template Play avec la notation @Messages.get("message2", "subst1", "subst2").

Par défaut, Play essaie de trouver la langue la plus adaptée au contexte courant (cookie envoyée, préférence donnée par le navigateur dans les en-têtes de sa requête...). Ce comportement peut être modifiable.

Pour en savoir plus sur l'internationalisation, on pourra consulter cette page de la documentation.

Configuration

Gestion de la configuration

Il est possible de centraliser toutes les informations de configuration dans le fichier application.conf qui utilise le format HOCON. On peut ensuite dans l'application récupérer la valeur associée à une clé avec une méthode telle que Configuration.root().getString("key") ou alors Configuration.root().getDouble("key") (des méthodes existent pour différents types et retournent toujours des types objet qui sont nuls si la clé n'est pas présente dans le fichier de configuration). Cette approche permet d'éviter l'usage de valeurs "en dur" dans les classes Java.

Exécuter du code au démarrage ou à la fermeture de l'application

La classe Globals est une classe spéciale placée à la racine du répertoire app qui permet d'intercepter des événements de démarrage de l'application, de fermeture de l'application ou alors de réception d'une requête.
Cette classe est désormais dépréciée. Pour exécuter du code au démarrage de l'application, on utilisera désormais une classe classique que l'on demandera à l'injecteur de dépendance d'instantier systématiquement au démarrage de l'application (et non pas uniquement paresseusement lors de l'utilisation). On crééra à cet effet une classe Module héritant d'AbstractModule (en utilisant la bibliothèque Guice d'injection de dépendances) :

public class Module extends AbstractModule 
{
    protected void configure() {
        bind(ApplicationStarter.class).asEagerSingleton();
        bind(ApplicationTerminator.class).asEagerSingleton();
    }
}

On peut ainsi profiter du démarrage de l'application pour planifier l'exécution de tâches périodiques (par exemple envoyer un rapport d'activité de l'application de façon hebbdomadaire le lundi matin à 8h) :

@Singleton
public class ApplicationStarter {

	public void schedule(final Runnable r)
	{
		Calendar c = Calendar.getInstance();
		c.set(Calendar.HOUR_OF_DAY, 8);
		c.set(Calendar.MINUTE, 0);
		c.set(Calendar.SECOND, 0);
		c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
		if (new Date().after(c.getTime()))
			c.add(Calendar.DAY_OF_MONTH, 7);
		long delay = new Date().getTime() - c.getTimeInMillis();
		Akka.system().scheduler().scheduleOnce(Duration.create(delay, TimeUnit.MILLISECONDS),() -> { r(); schedule(r); }, null);
	}
	
	public ApplicationStarter() 
	{
		// add a new scheduler to send a report each weeb on monday 8 a.m
		schedule(() -> { /* code to be executed */ });
	}
}

Nous pouvons également réaliser une action à la fermeture de l'application grâce à une classe Singleton (déclarée dans Module) :

@Singleton
public class ApplicationTerminator {
   
    @Inject
    public ApplicationTerminator(ApplicationLifecycle lifecycle) {
        lifecycle.addStopHook(() -> {
           // action to be done when the application is stopped...
        });
    }
}