Retour d’expérience sur Play Framework

Après un peu plus d’un an d’utilisation quotidienne de Play Framework dans sa version 2, il est temps pour moi de me livrer à l’exercice du retour d’expérience. Je précise que j’utilise Play en Java et pas en Scala.

Un concept très séduisant

Play FrameworkPlay est un des frameworks web les plus connus dans le monde Java. Lancé en 2009 dans sa première version, une deuxième version avec des changements majeurs a été publiée en 2012, le principal changement étant sa réécriture en Scala. L’idée qui est à l’origine de Play est de créer un outil simple et léger pour faire du développement web en Java. A une époque où tous les langages fréquemment utilisés dans le monde du web proposaient des outils modernes pour créer des sites web (Django pour Python, Rails pour Ruby, Symfony pour PHP, Grails pour Groovy, et bien d’autres), Java en était encore aux servlets et autres conteneurs d’application Java Enterprise Edition et toute la lourdeur qui va avec, aussi bien en terme de développement que de déploiement. Play a vocation à apporter à l’écosystème Java les avantages de ces outils qui existent dans d’autres langages.

Les principes fondateurs de Play sont les suivants :

  • « Developer Friendly » (simple, léger, support du rechargement à chaud, intégration avec les principaux IDE)
  • Robuste (code testable, sécurité du typage)
  • Capacité de monter en charge (serveur sans état donc permet facilement de faire de la redondance, basé sur des entrées sorties non bloquantes)
  • Modernité (support natif de REST, JSON, less, websocket et bien d’autres)

Pour résumer, Play est un peu l’outil dont tout développeur Java rêve de disposer pour faire du développement web. Sur le papier c’est un outil très séduisant, mais qu’en est-il en réalité ?

En pratique, Play tient la plupart de ses promesses

Simplicité

Pour commencer à utiliser Play, il faut le télécharger. Cette étape est assez simple et permet d’avoir en quelques minutes une environnement prêt pour travailler. Play a simplement besoin d’une JVM pour fonctionner. Créer un projet se fait en quelques secondes grâce à la console de développement. Créer un contrôleur, lui ajouter une route et créer le template HTML associé sont un jeu d’enfant. La productivité est assez impressionnante.

Play fournit une API de programmation HTTP assez complète et pratique à utiliser. Elle supporte nativement par exemple la sérialisation au format JSON et permet de façon assez naturelle de manipuler les cookies et autres statuts ou en-têtes de réponse. Cette API n’est pas une abstraction de HTTP, au contraire elle expose les différents composants du protocole. Parfois c’est un peu frustrant parce qu’on a un peu plus de choses à gérer soi-même, mais cela permet de pouvoir utiliser toute la puissance du protocole HTTP et c’est appréciable de ne pas être limité comme avec certains outils sur ce plan-là. Cela nécessite de bien maîtriser les subtilités de HTTP mais de toute façon il est difficilement envisageable de faire de la programmation web correctement (que ce soit un site web ou des web services) sans bien connaître le protocole sur lequel le web repose.

Sécurité et robustesse

Comme j’ai pu déjà l’exprimer, le typage statique est pour moi presque une évidence. En plus de s’appuyer sur du code à typage statique, Play propose la sécurité du typage à d’autres endroits et notamment sur les templates ou sur le routage des différents contrôleurs. Cela permet par exemple de ne pas tolérer à la compilation l’utilisation d’une propriété d’un objet qui n’existe pas dans un template, ou faire une route qui mène à un contrôleur inexistant. Un certain nombre de problèmes sont mis en lumière directement à l’étape de la compilation et pour moi c’est un gros point positif.

Play fournit également des outils permettant de faire des tests sur  la couche web à plusieurs niveaux. On peut par exemple tester un contrôleur en démarrant un serveur web (donc via HTTP) ou, sans le démarrer, en appelant simplement le contrôleur avec le contexte qui va bien, le tout de manière simple et rapide à l’exécution. Ces outils sont particulièrement appréciables pour les web services que l’on peut intégralement coder en TDD sans faire la moindre requête HTTP.

Redéploiement à chaud

Play ne nécessite pas d’extension à son IDE préféré pour supporter le rechargement à chaud. Il suffit de lancer Play via sa console en mode développement et il prendra automatiquement en compte à chaud les changements effectués sur le code, mais aussi les templates ou le routage. Cela donne un peu l’impression de travailler avec un langage de script, d’autant plus qu’il affiche les erreurs de compilation lorsqu’il y en a directement dans la page web. C’est clairement appréciable et ça contribue largement à la productivité qu’offre Play, c’est d’ailleurs un point qu’ils mettent particulièrement en avant.

Déploiement en production

Les développeurs de Play ont décidé de prendre à bras le corps la problématique de déploiement en production de l’application web. Autant avec certaines technologies, cette étape peut être un véritable calvaire, autant avec Play c’est une simple formalité. Le défi a été relevé avec succès. Il suffit d’utiliser la commande dist qui génère un fichier zip contenant tout ce qu’il faut pour lancer l’application (sauf la JVM bien entendu), avec deux scripts de lancement, un pour les systèmes UNIX et un pour Windows.

Rapide et réactif

Clairement, l’application sur laquelle je travaille n’est pas suffisamment utilisée pour pouvoir tirer la moindre conclusion quant à la capacité de montée en charge de Play, même si j’ai assez peu d’inquiétudes à ce sujet. En revanche, dans la situation de faible charge qui est la nôtre, les temps de réponse que nous rencontrons sont tout à fait satisfaisants. Notre application basée sur Play est déployée en redondance sur plusieurs serveurs et nous n’avons rencontré aucun problème lié à cela, et c’est grâce au fait que tout est fait dans Play pour que le serveur n’ait pas d’état.

Moderne

Play fait en sorte de supporter les outils modernes de développement web tels que le compilateur CSS less mais aussi les websockets. La version 2.3 récemment sortie apporte de nombreux changements relatifs à la gestion des ressources de l’application web, avec l’apparition de sbt-web, une extension à SBT destinée à gérer les ressources web. En plus de la compilation des fichiers Javascripts et CSS, sbt-web supporte également les WebJars et le versionnage des ressources (pour faciliter la mise en cache par les navigateurs). Les outils relatifs à la gestion des ressources sont décrits ici.

Malgré quelques défauts assez pénalisants

Même si en pratique, Play est un outil très agréable à utiliser quelques petits problèmes récurrents viennent parfois gâcher l’expérience et faire perdre du temps et de l’énergie.

SBT,  la plaie de Play

Pour moi, utilisateur de Play en Java, un de ses plus gros défauts est le fait qu’il ne soit pas écrit en Java mais en Scala. Étant plus un détracteur qu’un adepte de ce langage, je pourrais me contenter de dire cela juste pour polémiquer. Mais je vais tenter d’expliquer en quoi le fait que Play repose sur un écosystème Scala pose des problèmes au quotidien au développeur Java.

Incontestablement, je dirais que le plus gros point faible de Play est son système de build, SBT en l’occurrence, c’est en tout cas ce qui me pose le plus de problèmes au quotidien. SBT propose une API Scala sous forme de DSL qui permet de décrire déclarativement les caractéristiques de son projet. Je suis désolé pour ceux qui ont conçu ce système, mais un fichier de description de build en SBT c’est absolument immonde. Lorsqu’on crée un projet, Play génère un template de projet avec un fichier de build fonctionnel. Tant qu’on n’a pas à le toucher, tout va bien, même si c’est très difficile à déchiffrer. D’une certaine manière, lorsqu’on comprend ce que cela veut dire, je pense je n’ai pas eu cette chance-là qu’on peut voir cela comme une manière élégante (ou artistique) de décrire son build. Mais en tant que débutant, on dirait un mélange d’ASCII art et de Brainfuck. Un build SBT c’est une succession de symboles et d’opérateurs d’une variété incroyable (<+=, ++=, %% et j’en passe) avec parfois quelques mots (noms de variables, types ou fonctions) qui font croire qu’on peut réussir à comprendre. Les ennuis arrivent lorsqu’on doit le modifier, parce qu’il est difficile de modifier quelque chose qu’on ne comprend pas. Pour ajouter une dépendance, un plugin, ou une étape au build, il faut être bien accroché. Nous avons perdu des heures et peut-être même des jours à errer de désespoir sur internet en cherchant un exemple de ce que nous cherchions à faire. En pratique, même après plus d’un an d’utilisation, nous sommes incapables de modifier notre fichier de build de manière autonome, sauf pour des opérations simples et fréquentes comme rajouter une dépendance, et encore ! Le comble c’est quand même que SBT signifie Simple Build Tool. Ce fil de discussion m’amène à penser que je ne partage pas leur définition du mot simple.

De la même manière, sbt-web, nouveauté de Play 2.3, apporte de nombreuses fonctionnalités qui permettent de gérer les ressources statiques. Mais sans doute à cause de sa jeunesse, sbt-web est un peu limité en terme de fonctionnalité et pas encore tout à fait au point. Et Scala constitue une barrière du langage qui rend toute contribution ou création d’extension très difficile à quiconque n’est pas initié à cette technologie.

Le fait que Play soit fortement lié à SBT pose d’autres problèmes concernant l’intégration du projet avec d’autres systèmes de build et en l’occurrence Maven. SBT n’est pas interopérable avec Maven mais on ne peut pas vraiment lui en tenir rigueur dans la mesure où ce n’est pas vraiment ce qu’on attend d’un système de build. Il existe bien un plugin qui nous rend déjà de grands services, mais il ne fait que déléguer à SBT l’exécution des différentes tâches Maven. Il reste de nombreux problèmes et notamment pour ce qui concerne le partage de modules Maven produits par le build, à cause du fait que l’application Play n’est pas un vrai module au sein du projet Maven.

L’intégration dans les IDE est également loin d’être parfaite. On a l’impression de revenir à l’époque où Maven commençait à être intégré dans les IDE. La cohabitation n’est pas toujours simple et fraternelle. Lorsqu’on est habitué au support de Maven par IntelliJ IDEA, on a juste l’impression de revenir à l’âge de pierre. C’est difficile de revenir en arrière à ce sujet-là.

Compilation et génération de code perfectibles

L’aspect type-safe d’éléments comme les templates apporte beaucoup de sécurité que je suis le premier à apprécier. Mais le revers de la médaille c’est qu’ils sont compilés, cette étape générant du code Scala. Or, ce dernier étant un langage réputé pour la lenteur de son compilateur, Play en pâtit quelque peu. Quelques fichiers Scala mettent parfois autant de temps à compiler que tout le code Java, soit plusieurs centaines de fichiers. Ce n’est pas très pénalisant même si c’est agréable quand la compilation est très rapide, notamment en mode développement avec le rechargement à chaud.

En revanche, j’ai eu par le passé de nombreux problèmes liés à la génération de code. Il m’est arrivé à plusieurs reprises de me retrouver dans un état duquel je n’arrivais pas à sortir. Cela se produisait en faisant une modification sur un template qui casse la compatibilité avec son ancienne version. Le code qui l’utilisait ne compile plus, et parfois le nouveau template ne semble pas être rechargé par l’IDE. Impossible alors de s’en sortir avec un IDE configuré pour réorganiser automatiquement les imports (les templates sont accessibles en statique) lors de la sauvegarde. La seule issue étant de commenter du code pour que tout rentre dans l’ordre. Cela fait un moment que je n’ai pas rencontré ce problème, je ne sais pas si c’est une mise à jour de Play qui l’a corrigé ou si c’est que j’ai changé d’IDE il y a quelques mois. En tout cas ce problème qui était assez systématique était très rageant.

Une API parfois trop simpliste

Play est un framework qui permet de développer aussi bien en Scala qu’en Java. Play est écrit en Scala, le support de Scala est donc natif. L’utilisation de Play en Java passe par une API qui expose les fonctionnalités disponibles en Scala dans du code exploitable en Java. L’API Java est assez jolie, elle masque plutôt bien Scala et son byte-code généré qui n’est pas franchement joli, en revanche, dans les stack traces ou lorsqu’on parcourt le code avec un debugger, on arrive assez vite dans du mapping Java / Scala ou même dans du Scala pur, et cela ouvre la porte d’un monde très différent, et c’est malheureusement souvent là que je m’arrête avec le debugger.

En utilisant Play en Java on se rend également compte que Java n’est pas le langage natif de Play. Même si de gros efforts sont faits pour maintenir les API en parallèle (l’API Java s’appuyant sur l’API Scala), il existe des fonctionnalités qui ne sont pas accessibles en Java et c’est un peu frustrant. Par exemple, j’utilise Play dans le contexte d’une application multi-tenant et j’aurais souhaité appliquer un traitement à toutes les requêtes en les interceptant pour les modifier avant le routage, mais ce n’est pas possible en Java.

Finalement, on se demande si le support de Java dans Play n’est pas en sursis. Play 1 était en Java, Play 2 est en Scala avec un portage Java. Play 3 supportera-t-il Java ?

Du tout statique

Play pousse fortement à coder en tout statique, comme le montre la documentation sur les contrôleurs. Une classe qui n’a que des méthodes statiques qui hérite d’une autre classe ?! Un bien bel exemple d’héritage abusif. Bien entendu, le tout statique a des avantages, mais c’est aussi un énorme obstacle à la testabilité. Heureusement qu’une petite porte ouverte permet de travailler avec des instances de contrôleurs, et donc l’utilisation d’un mécanisme d’injection de dépendances. C’est à mon sens presque indispensable pour la testabilité de l’ensemble de l’application.

Un autre point qui me gêne dans le tout statique est le fait que la récupération de la requête courante (et de bien d’autres choses) se fasse de manière statique via la classe Controller. Ce n’est pas pour rien que les contrôleurs héritent de cette classe, cela permet un accès direct à ses membres… Le problème c’est que tout se code s’exécute dans un environnement hautement parallélisé, et c’est complètement contradictoire avec la notion de statique. Je suppose que le contexte de la requête est attaché au thread courant via un mécanisme tel que ThreadLocal. Tout ce qui est lié au contexte des requêtes n’est donc ni plus ni moins proposé sous forme de variables globales, ce qui incite à s’en servir partout et ainsi à mélanger les niveaux d’abstraction du code, en insérant par exemple une dépendance à la notion de requête HTTP dans du code métier. Dommage pour la testabilité… Pour contourner ce problème, Play fournit des outils permettant de tester la couche HTTP et dont j’ai vanté les mérites un peu plus haut dans cet article. Ils permettent de créer un contexte HTTP avant d’appeler le code du contrôleur.

Des migrations douloureuses

Nous avons commencé à utiliser Play dans sa version 2.0 et notre application a suivi l’évolution des versions en passant par la 2.1, puis 2.2 et très récemment 2.3. Aucune de ces migrations n’a été une formalité, à cause notamment des changements liés à l’environnement de Play (souvent autour de SBT) ou bien d’une rupture de compatibilité dans les API. La migration de 2.2 à 2.3 a été particulièrement compliquée à mettre en œuvre, notamment à cause du passage à Activator, et sbt-web. Le guide de migration est assez révélateur de la quantité des changements.

Ceci étant dit, je crois qu’on peut dire que ces importants changements entre les versions majeures des Play sont un mal pour un bien parce qu’ils permettent d’apporter des améliorations intéressantes concernant parfois les fondations de Play. Bien souvent il est bénéfique de tirer un gros trait sur ce qui existe pour repartir de zéro en tirant les enseignements du passé plutôt que d’essayer de maintenir la compatibilité avec un socle qui n’est pas adapté. Les développeurs de Play osent casser la compatibilité pour le bien de l’outil mais cela engendre des étapes assez désagréables à passer pour les utilisateurs de Play.

Un code asynchrone

En tant que plate-forme qui se revendique être réactive, Play est basé sur du code non bloquant et donc asynchrone. Les contrôleurs sont capables de fonctionner aussi bien en mode synchrone qu’en mode asynchrone, et les API à disposition du développeur comme celle qui permet d’appeler des Web Services sont également non bloquantes, basées sur des promesses.

Même si l’aspect asynchrone et non bloquant permet sans doute une meilleure capacité à monter en charge aussi bien horizontalement que verticalement, je trouve que cela augmente la complexité  et nuit à la testabilité. Parfois, un petit changement peut engendrer un gros refactoring, c’est par exemple le cas si une fonction doit tout d’un coup appeler un web service. Le résultat de l’appel au web service sera obtenu de manière asynchrone, ce qui fait que le traitement de la réponse se fera dans une callback. Du coup la fonction ne peut plus renvoyer un résultat directement mais doit renvoyer une promesse de résultat. J’ai déjà vécu la situation où un changement si mineur conduit à reprendre le code qui l’appelle pour qu’il devienne asynchrone, et pour peu que ce code soit un peu partagé, il faut retoucher la moitié de l’application et réécrire tous les tests (et tester du code asynchrone c’est rarement une partie de plaisir)… Heureusement que l’arrivée des lambdas de Java 8 permet de limiter la verbosité de la programmation asynchrone.

L’expérience que j’ai en programmation asynchrone avec Play fait que j’aurais tendance à penser qu’un modèle synchrone est bien meilleur du point de vue du génie logiciel, quitte à devoir acheter quelques serveurs supplémentaires pour compenser la différence en consommation de ressources, c’est à mon avis bien plus rentable.

Conclusion

Pour conclure, je dois dire que Play est un bon outil qui permet de faire du web de manière simple et productive. D’une manière générale, mon avis est largement positif à son sujet, mais parfois certains problèmes et particulièrement ceux qui concernent SBT (et qui malheureusement sont récurrents) sont vraiment épuisants et viennent gâcher la fête.

C’est en effet assez frustrant de voir qu’il ne manque pas grand chose pour que Play soit le framework web idéal. Malheureusement je n’ai personnellement pas beaucoup d’espoir quant au fait que ces points négatifs puissent s’effacer au fil du temps dans la mesure où ils concernent pour la plupart des choix qui sont parmi les fondements de Play comme l’utilisation de SBT.

Peut-être que ces points qui sont négatifs pour moi sont considérés comme des atouts dans le monde du Scala. L’utilisation de Play en Scala offre peut-être un ressenti très différent. Quoi qu’il en soit je n’ai pas suffisamment d’affection envers ce langage pour avoir le courage de tenter l’expérience.

Pour terminer, je dirais que la nouvelle génération de serveurs web semble se dessiner peu à peu. La tendance semble être que le serveur web ne doit plus être un framework qui, comme Play, est maître de l’application mais une simple bibliothèque qui fournit tous les outils permettant de faire un site web via un serveur web qu’on démarre soi-même, ce qui est le cas de Spring Boot ou de Fluent HTTP. J’ai d’ailleurs récemment utilisé Fluent HTTP sur un petit projet et c’est très plaisant et productif.

L’image d’en-tête provient de Flickr.

7 thoughts on “Retour d’expérience sur Play Framework”

  1. La version 2.4.0-M1 est sortie dans le but d’obtenir du feedback sur l’intégration native de l’injection de dépendance dans Play. Voir l’annonce. Ils se sont enfin rendus compte que le tout statique n’est pas une solution très sérieuse.

    1. Non, je n’ai pas essayé, mais il faudra que je le fasse.

      Ce dernier mois nous avons encore perdu beaucoup de temps en se battant contre SBT… Décidément, rien n’est jamais acquis avec cet outil.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *