Play 2.0 est une refonte majeure du framework faisant la part belle à Scala. La version Beta sortie en novembre dernier était assez loin d’être finalisée et nous avait laissé quelque peu sur notre faim.
La sortie des premières RC et, depuis peu, de la version finale nous permet enfin de plonger réellement dans l’outil et d’analyser plus en détail cette nouvelle mouture.
Rien n’a changé ?
Lorsqu’on commence à tester rapidement Play 2.0, on se dit que finalement rien n’a vraiment changé. On télécharge le framework, on le dézippe, on l’ajoute à son PATH, on lance un « play new » puis un « play run » et on peut accéder à la page d’accueil de notre nouvelle appli dans notre navigateur en quelques secondes.
La structure du projet est sensiblement la même que pour la version précédente. On retrouve rapidement ces packages habituels : controllers, models, views. Les points forts de Play semblent avoir été conservé :
– La documentation est intégrée dans notre appli en mode dev et acccessible avec une « route » spécifique
– le code est rechargé « à chaud » sans qu’on ait besoin de s’en occuper
– on peut écrire notre code en Java ou en Scala comme on pouvait le faire avec le module play-scala de la version précédente
C’est en creusant un peu plus qu’on se rend compte qu’il y a quand même beaucoup de changements.
Tout a changé
Accès aux données
Tout d’abord, l’accès aux données a été revu. Pour Java, c’est la librairie Ebean qui a été retenue en standard. Plus adaptée au mode de fonctionnement stateless de Play qu’Hibernate, elle est bien intégrée au framework. On retrouve globalement les mêmes automatismes que dans Play 1.
Pour Scala par contre, on change de philosophie avec le librairie Anorm. C’est plutôt un retour aux sources qui est opéré via l’utilisation directe de requêtes SQL mappées « à la main » vers des objets. Des outils sont disponibles pour faciliter la tâche, mais on est quand même loin du côté « magique » de la version Java.
Avec ces 2 possibilités offertes, le développeur est libre de travailler avec la solution la plus adaptée à son contexte (rapidité de développement, performance de l’application…).
Voici un exemple de code d’un Model réalisé avec Ebean :
@Constraints.Required
public String name;
@Entity
public class Company extends Model @Id
public Long id;
public Company(String name) {
this.name = name;
public static Finder<Long, Company> find = new Finder<Long, Company>(Long.class, Company.class);
public static Company findById(Long id) return find.byId(id);
public static int count() return find.findRowCount();
public static List all() return find.all();
public static void create(Company company) company.save();
public static void delete(Long id) find.ref(id).delete();
}
Et voici le code d'un Model Anorm tiré d'une application exemple fournie avec Play :
case class User(email: String, name: String, password: String)
object User // -- Parsers
/**
* Parse a User from a ResultSet
*/
val simple = {
get[String]("user.email") ~
get[String]("user.name") ~
get[String]("user.password") map {
case email~name~password => User(email, name, password)
}
// -- Queries
/**
* Retrieve a User from email.
*/
def findByEmail(email: String): OptionUser] = DB.withConnection { implicit connection =>
SQL("select * from user where email = {email").on(
'email = DB.withConnection { implicit connection =>
SQL("select * from user").as(User.simple *)
}
/**
* Create a User.
*/
def create(user: User): User = DB.withConnection { implicit connection =>
SQL(
"""
insert into user values (
{email, name, password
)
"""
).on(
'email -> user.email,
'name -> user.name,
'password -> user.password
).executeUpdate()
user
}
}
}
@Constraints.Required
public String name;
@Entity
public class Company extends Model @Id
public Long id;
public Company(String name) {
this.name = name;
public static Finder<Long, Company> find = new Finder<Long, Company>(Long.class, Company.class);
public static Company findById(Long id) return find.byId(id);
public static int count() return find.findRowCount();
public static List all() return find.all();
public static void create(Company company) company.save();
public static void delete(Long id) find.ref(id).delete();
}
Et voici le code d'un Model Anorm tiré d'une application exemple fournie avec Play :
case class User(email: String, name: String, password: String)
object User // -- Parsers
/**
* Parse a User from a ResultSet
*/
val simple = {
get[String]("user.email") ~
get[String]("user.name") ~
get[String]("user.password") map {
case email~name~password => User(email, name, password)
}
// -- Queries
/**
* Retrieve a User from email.
*/
def findByEmail(email: String): OptionUser] = DB.withConnection { implicit connection =>
SQL("select * from user where email = {email").on(
'email = DB.withConnection { implicit connection =>
SQL("select * from user").as(User.simple *)
}
/**
* Create a User.
*/
def create(user: User): User = DB.withConnection { implicit connection =>
SQL(
"""
insert into user values (
{email, name, password
)
"""
).on(
'email -> user.email,
'name -> user.name,
'password -> user.password
).executeUpdate()
user
}
}
}
Routing
Le routing n’a pas beaucoup changé en apparence, on retrouve toujours le même fichier de configuration des routes. Par contre, cette fois, la configuration est vérifiée à la compilation. Impossible donc de déclarer une route sans avoir créé le controller associé. Play nous apporte néanmoins une petite astuce pour créer très rapidement un controller « TODO » qui affichera une page spéciale dans le navigateur.
Controllers
La logique des controllers reste la même. On crée une classe Controller dans laquelle on déclare des méthodes statiques pour chacune de nos Actions. Il est également possible de créer une classe pour chaque Action. Le code en devient plus verbeux mais cela peut rendre les tests plus faciles.
Les Actions, au lieu d’appeler une méthode render(), doivent maintenant retourner un objet Result. Des helpers sont là pour construire des Result avec différents codes HTTP de retour (ok(), forbidden(), notFound(),…) et différents types de données (texte, json, html, xml,…). On donne ainsi la possibilité au développeur de manipuler finement la couche HTTP.
Les paramètres sont passés automatiquement à l’Action grâce au mapping effectué dans le routing.
Voici un exemple de code d’un Controller :
public class Application extends Controller public static Result testText() {
return ok("Hello world !");
public static Result testTemplate() return ok(index.render("Your new application is ready."));
public static Result testNotFound() return notFound("Could not find this page !");
public static Result testRedirect() return redirect(controllers.routes.Application.testText());
}
Formulaires
La gestion des formulaires fait son apparition à travers un système qui permet de structurer son code tout en restant simple. Il est conçu pour s’interfacer facilement avec le Model. Les cas d’utilisations classiques seront donc très faciles à implémenter. Les cas plus complexes nécessiteront un travail spécifique du développeur.
Templates
C’est pour moi un des changements les plus importants car il apporte une philosophie très différente du moteur Groovy utilisé dans la version précédente.
Le moteur de template repose entièrement sur Scala, ce qui a permis de le rendre « typé ». En clair, chaque template attend des paramètres d’entrée typés (et peut appeler d’autres templates avec également leur définition de paramètres). C’est à la compilation que tout cela va être vérifié, rendant les templates très robustes.
Pour accentuer encore plus cette robustesse, l’appel au routing depuis les templates pour générer des urls (pour les liens, les formulaires,…) est également vérifié à la compilation. Avec ça, Il va devenir très difficile d’avoir un lien mort dans son application.
Il est à noter qu’il existe des solutions pour utiliser le moteur de template Groovy de Play 1 vers Play 2 (voir Groovy templates for Play! 2 et Faster Groovy Template Engine). Il sera donc envisageable de réutiliser certains éléments lors d’une phase de migration.
Voici par exemple le code d’un template « index » prenant en paramètre un User et une liste de Link et appelant un template « main » :
@(user: User, links : List[link])
@main("User links")
Welcome @user.name !
@link.title
}
@(title: String)(content: Html)
@content
Assets
Play 2.0 suit la tendance actuelle en matière de CSS et javascript en permettant de gérer nativement la compilation du code Less ou CoffeeScript. La gestion d’autres types de fichiers sources peut être ajoutée via des modules de manière assez simple.
Le framework en profite pour gérer également la minification des assets.
Asynchrone
Play 1 apportait déjà quelques briques pour le développement de traitements asynchrones. Une étape supplémentaire est franchie avec l’intégration d’Akka et son modèle d’Actor. Les traitements pourront désormais être distribués facilement par Akka sur plusieurs instances de l’application.
Les manques
Au delà de toutes ces nouveautés, comme très fréquemment lors d’une refonte, certains détails manquent à l’appel.
Le principal « manque » de cette nouvelle version est l’absence de compatibilité avec les applications développées en utilisant la version précédente. La migration est loin d’être évidente et nombre de projets resteront vraisemblablement en Play 1 pendant un moment.
Il faut noter également l’absence de module CRUD (utilisé pour créer rapidement des interfaces d’administration basiques). Cela est dû à la direction prise avec Anorm de redonner plus la main au développeur sur l’accès aux données en enlevant un peu de « magie ».
Même si le code de Play 2.0 repose essentiellement sur Scala, l’utilisation du framework en lui-même ne posera pas de problème à un développeur Java sans réelle connaissance de Scala. En revanche, lorsqu’on veut aller fouiller un peu dans le code source du framework (chose qui arrive forcément à un moment ou à un autre), cela pourra se révéler assez handicapant. D’un autre côté, la simplicité de l’API de Play et la possibilité de pouvoir « mixer » Java et Scala peuvent en faire le framework idéal pour apprendre ce nouveau langage.
Dernier petit détail, le développement en utilisant Scala est gourmand en temps de compilation. Même si celle-ci se fait à la volée en cas de modification, on perd quand même un peu de réactivité lors des rafraîchissements de page.
Conclusion
Les grands principes qui ont fait la réputation de Play 1 sont toujours présents dans cette nouvelle version. L’API reste très simple à utiliser et les nouveautés sont réellement intéressantes. On regrettera l’abscence du module CRUD qui était très pratique dans les premières phases d’un projet à des fins de prototypage ou de test.