Dans cet article, je vous propose de regarder 2 approches différentes pour résoudre un même problème : construire une application web sécurisée, moderne et Ajaxisé. Je vais vous présenter les différences de conceptions sur la partie Java essentiellement. Cet article ne sera pas une introduction à l’une ou l’autre des technologies. Je souhaite vous éclairer sur quelques concepts différents dans Play! par rapport à l’approche classique.
Pour comparer 2 choses, il faut un référentiel comparable. JBoss Seam propose sur cette page plusieurs versions d’une application de réservation de chambres d’hôtels. La version originale basée sur JSF 1.2 n’est plus toute jeune mais fonctionne très bien. J’en avais parlé sur le Touilleur Express il y a 3 ans et demie… Quand je me dis que cela fait plus de 3 ans que j’ai vu cela, et qu’à l’époque je vous avais convaincu de regarder JSF par rapport à Struts, je me dis qu’il faut que je réussisse dans cet article à vous montrer la nouvelle approche, celle de 2010.
Nous allons prendre Spring Faces et Spring Webflow, une approche que je trouve très intéressante et qui permet de réaliser des applications Webs avec les technologies de Spring.
I. Le match : télécharger, installer et lancer la démo
Voyons tout d’abord comment télécharger, compiler et lancer la démonstration avec chacun des environnements. Je souhaite évaluer la rapidité de prise en main. Je souhaite que le projet soit configuré dans IDEA IntelliJ ou Eclipse pour travailler, et je vais évaluer la facilité à monter chacun des environnements. Je regarderai aussi le temps de démarrage des serveurs Webs testés.
I.1) Préparation de l’espace de démo pour Spring
SpringSource propose « Spring Travel« , une version basée sur Spring Faces, Spring WebFlow 2.0.9 et une librairie JSF avancée pour le rendu. J’ai regardé précisément le code « booking-faces » dans le répertoire projects/spring-webflow-samples pour écrire cet article. Si vous souhaitez tester par vous même :
– téléchargez la version Spring WebFlow 2.0.9 sur le site de SpringSource
– décompressez l’archive
– ouvrez le répertoire projects/spring-webflow-samples/booking-faces
– tapez mvn eclipse:eclipse pour créer un nouveau projet
– ou sinon tapez mvn idea:idea pour créer un projet IDEA IntelliJ
– ou enfin, si vous avez IDEA IntelliJ, celui-ci importe sans problème le pom.xml et vous prépare un projet configuré… Pas besoins de plugins.
Faire tourner la démo en local
Pour faire fonctionner la version Spring en local, éditez le pom.xml et ajoutez Jetty dans la balise build/plugins comme ici:
... <build> <finalName>swf-booking-faces</finalName> <plugins> ... <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> </plugin> </plugins> </build> <properties> <spring.version>2.5.6.SEC01</spring.version> <jsf.version>1.2.0.09</jsf.version> </properties> </project>
Vous pourrez alors lancer la démo avec mvn jetty:run dans le répertoire de la démo de Spring.
I.2) Configuration de la version Play! Framework
Play! Framework se télécharge sur le site de Play!. Voici comment suivre mes explications pour la deuxième partie :
– décompressez le zip dans un répertoire C:\Soft\play par exemple
– ajoutez C:\Soft\play à votre PATH
Pour lancer l’application :
– allez dans le répertoire C:\Soft\play\samples-and-tests\booking
– tapez « play run » et connectez-vous sur http://localhost:9000/
Pour travailler avec Eclipse ou IDEA IntelliJ
– allez dans le répertoire C:\Soft\play\samples-and-tests\booking
– tapez play eclipsify pour créer un projet Eclipse
– OU tapez play idealize pour créer un module IDEA IntelliJ
c’est tout !
I.3) Résultat du match :
Facilité d’installation : match nul
Qualité de la doc sur le site : match nul
Création du projet dans IDEA IntelliJ : match nul
Serveur : la démo Spring n’embarque pas de serveur, il a fallut ajouter Jetty pour tester alors que Play! Framework dispose de son propre serveur
Temps de démarrage : avantage Play! qui démarre très rapidement en 2 secondes.
Vainqueur : Play! Framework d’une courte longueur
II. Le modèle
II.1) Spécification fonctionnelle
L’objectif de l’application est de réaliser un site de réservation d’hôtel. Pour cela, l’utilisateur peut chercher un hôtel, puis ensuite créer une réservation (Booking en Anglais).
L’utilisateur doit s’authentifier avant de pouvoir terminer sa réservation. Une entité User permet donc de gérer la sécurité facilement avec un moteur de persistence.
Nous utiliserons JPA et une base en mémoire pour les 2 versions. L’essentiel est que Play! Framework comme la version Spring, utilisent JPA. Donc les entités sont presque identiques.
II.2) Une entité simple : l’Hotel
Prenons tout d’abord l’entité Hotel qui représente un hôtel. Voici la version de la démo Spring Webflow/JSF :
package org.springframework.webflow.samples.booking; import java.io.Serializable; import java.math.BigDecimal; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; /** * Version Spring Web Flow. La sérialisation est nécessaire pour JSF. */ @Entity public class Hotel implements Serializable { private static final long serialVersionUID = 4011346719502656269L; private Long id; private String name; private String address; private String city; private String state; private String zip; private String country; private BigDecimal price; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getZip() { return zip; } public void setZip(String zip) { this.zip = zip; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } @Column(precision = 6, scale = 2) public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } public Booking createBooking(User user) { return new Booking(this, user); } @Override public String toString() { return "Hotel(" + name + "," + address + "," + city + "," + zip + ")"; } }
Voici la version Play! Framework :
package models; import play.db.jpa.*; import play.data.validation.*; import javax.persistence.*; import java.math.*; /** * Version Play! Framework */ @Entity public class Hotel extends play.db.jpa.Model { @Required @MaxSize(50) public String name; @MaxSize(100) public String address; @Required @MaxSize(40) public String city; @Required @MaxSize(6) @MinSize(2) public String state; @Required @MaxSize(6) @MinSize(5) public String zip; @Required @MaxSize(40) @MinSize(2) public String country; @Column(precision=6, scale=2) public BigDecimal price; public String toString() { return "Hotel(" + name + "," + address + "," + city + "," + zip + ")"; } }
Les différences entre les 2 versions :
– Play! Framework étend la classe « play.db.jpa.Model » qui fournit le support JPA.
– Les attributs sont publics dans la version Play!, ce qui logiquement évite des getters/setters comme dans la version Spring classique. Moi depuis que l’encapsulation n’est plus à la mode en soirée, j’ai arrêté.
– La syntaxe JPA est un peu plus poussée dans la version Play! Nous verrons si cela permet d’améliorer l’expérience utilisateur dans l’interface graphique ou non.
Le bean de Spring n’étend pas une classe de base contrairement à Play!, ce qui permet d’étendre éventuellement Hotel. Mais est-ce que c’est quelque chose que nous faisons pour des Entities ? Je me pose la question. Perso je décourage l’héritage avec JPA. FBI = Fausse bonne idée.
[Update] Avec Play! Framework vous pouvez étendre la classe Model afin de simplifier la gestion JPA de l’entité. Cela fait partie des bonnes pratiques comme expliqué par Nicolas Leroux dans les commentaires. Pour en savoir plus, regardez la doc de la partie JPA de Play!
Play! gère l’identité dans la class Model avec un attribut Id de type Long par défaut. Si vous souhaitez gérer votre propre id, vous devez pouvez par exemple étendre la class JPASupport de Play!, puis simplement définir un attribut @Id comme d’habitude avec JPA. C’est tout à fait possible.
On voit que dans la version Spring, il y a une méthode « createBooking » qui permet de créer au niveau de l’Hotel une réservation.
Il n’y a donc pas de vainqueur dans cette partie pour moi. Les 2 approches se valent je pense. Je préfère la simplicité de Play! Framework qui remet en question quelques habitudes du monde Java, comme l’encapsulation systématique.
Par contre je n’aime pas trop l’approche de placer les annotations JPA sur les getters/setters plutôt que sur les attributs de la classe dans la version Spring. On perd beaucoup en lisibilité.
II.3) Une entité plus compliquée : Booking
Une réservation est l’association d’un User, d’un Hotel, d’une date d’arrivée et de départ et de différents autres attributs. Je vais faire exprès de vous donner la version complète des 2 frameworks, afin de vous montrer les différences flagrantes entre les 2 approches.
La version de la démo Spring WebFlow telle qu’elle est livrée, sans trucages, sans effets spéciaux :
package org.springframework.webflow.samples.booking; import java.io.Serializable; import java.math.BigDecimal; import java.text.DateFormat; import java.util.Calendar; import java.util.Date; import javax.persistence.Basic; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToOne; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; import org.springframework.binding.message.MessageBuilder; import org.springframework.binding.message.MessageContext; import org.springframework.binding.validation.ValidationContext; /** * A Hotel Booking made by a User. */ @Entity public class Booking implements Serializable { private static final long serialVersionUID = 1171567558348174963L; private Long id; private User user; private Hotel hotel; private Date checkinDate; private Date checkoutDate; private String creditCard; private String creditCardName; private int creditCardExpiryMonth; private int creditCardExpiryYear; private boolean smoking; private int beds; private Amenity amenities; // Absent de la version Play! public Booking() { } public Booking(Hotel hotel, User user) { this.hotel = hotel; this.user = user; Calendar calendar = Calendar.getInstance(); setCheckinDate(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); setCheckoutDate(calendar.getTime()); } @Transient public BigDecimal getTotal() { return hotel.getPrice().multiply(new BigDecimal(getNights())); } @Transient public int getNights() { if (checkinDate == null || checkoutDate == null) { return 0; } else { return (int) (checkoutDate.getTime() - checkinDate.getTime()) / 1000 / 60 / 60 / 24; } } @Id @GeneratedValue(strategy = GenerationType.TABLE) public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Basic @Temporal(TemporalType.DATE) public Date getCheckinDate() { return checkinDate; } public void setCheckinDate(Date datetime) { this.checkinDate = datetime; } @ManyToOne public Hotel getHotel() { return hotel; } public void setHotel(Hotel hotel) { this.hotel = hotel; } @ManyToOne public User getUser() { return user; } public void setUser(User user) { this.user = user; } @Basic @Temporal(TemporalType.DATE) public Date getCheckoutDate() { return checkoutDate; } public void setCheckoutDate(Date checkoutDate) { this.checkoutDate = checkoutDate; } public String getCreditCard() { return creditCard; } public void setCreditCard(String creditCard) { this.creditCard = creditCard; } @Transient public String getDescription() { DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM); return hotel == null ? null : hotel.getName() + ", " + df.format(getCheckinDate()) + " to " + df.format(getCheckoutDate()); } public boolean isSmoking() { return smoking; } public void setSmoking(boolean smoking) { this.smoking = smoking; } public int getBeds() { return beds; } public void setBeds(int beds) { this.beds = beds; } public String getCreditCardName() { return creditCardName; } public void setCreditCardName(String creditCardName) { this.creditCardName = creditCardName; } public int getCreditCardExpiryMonth() { return creditCardExpiryMonth; } public void setCreditCardExpiryMonth(int creditCardExpiryMonth) { this.creditCardExpiryMonth = creditCardExpiryMonth; } public int getCreditCardExpiryYear() { return creditCardExpiryYear; } public void setCreditCardExpiryYear(int creditCardExpiryYear) { this.creditCardExpiryYear = creditCardExpiryYear; } @Transient public Amenity getAmenities() { return amenities; } public void setAmenities(Amenity amenities) { this.amenities = amenities; } public void validateEnterBookingDetails(ValidationContext context) { MessageContext messages = context.getMessageContext(); if (checkinDate.before(today())) { messages.addMessage(new MessageBuilder().error().source("checkinDate").code( "booking.checkinDate.beforeToday").build()); } else if (checkoutDate.before(checkinDate)) { messages.addMessage(new MessageBuilder().error().source("checkoutDate").code( "booking.checkoutDate.beforeCheckinDate").build()); } } private Date today() { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -1); return calendar.getTime(); } @Override public String toString() { return "Booking(" + user + "," + hotel + ")"; } }
Voyons la version Play! Framework de l’entité Booking maintenant :
package models; import play.db.jpa.*; import play.data.validation.*; import javax.persistence.*; import java.util.*; import java.text.*; import java.math.*; @Entity public class Booking extends Model { @Required @ManyToOne public User user; @Required @ManyToOne public Hotel hotel; @Required @Temporal(TemporalType.DATE) public Date checkinDate; @Required @Temporal(TemporalType.DATE) public Date checkoutDate; @Required(message="Credit card number is required") @Match(value="^\\d{16}$", message="Credit card number must be numeric and 16 digits long") public String creditCard; @Required(message="Credit card name is required") @MinSize(value=3, message="Credit card name is required") @MaxSize(value=70, message="Credit card name is required") public String creditCardName; public int creditCardExpiryMonth; public int creditCardExpiryYear; public boolean smoking; public int beds; public Booking(Hotel hotel, User user) { this.hotel = hotel; this.user = user; } public BigDecimal getTotal() { return hotel.price.multiply( new BigDecimal( getNights() ) ); } public int getNights() { return (int) ( checkoutDate.getTime() - checkinDate.getTime() ) / 1000 / 60 / 60 / 24; } public String getDescription() { DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM); return hotel==null ? null : hotel.name + ", " + df.format( checkinDate ) + " to " + df.format( checkoutDate ); } public String toString() { return "Booking(" + user + ","+ hotel + ")"; } }
Tout d’abord il manque des choses dans la version Play! Framework, et on ne compare donc plus exactement la même chose. Notez l’absence de l’entité Amenity. Ensuite, la version Spring utilise une approche différente pour l’attribut @Id et spécifie le générateur alors que la version Play! hérite de ce que la super-classe Model propose.
Ce qui me dérange dans la version Spring c’est la présence de la méthode validateEnterBookingDetails. Celle-ci est intéressante fonctionnellement, mais dépose une dépendance vers MessageContext un peu trop forte à mon goût.
Regardez aussi la différence sur la validation du numéro de carte de paiement. La version Play! utilise un moteur simple similaire à Hibernate Validation, mais propriétaire à Play! Au contraire la version Spring ne précise rien, et on imagine donc que la validation des 16 digits se fera dans la vue… et pas dans le modèle.
Conclusion : j’ai de plus en plus de mal avec la sauce des getters/setters, surtout lorsque les entités dépasse les « HelloWorld » et que l’on commence à sortir des cas d’usages compliqués. Souvenez-vous de ce que je vous disais : pour résoudre des problèmes compliqués il faut des solutions simples…
III. Le contrôleur
Après avoir regardé 2 entités différentes pour le modèle, voyons maintenant la différence d’approche entre Spring WebFlow et Play! Framework. Je vais étudier le cas de l’affichage de la liste des Hôtels, lorsque l’utilisateur clique sur le bouton Search par exemple.
III.1) Spring WebFlow
L’approche de Spring WebFlow est la suivante : afin d’éviter d’écrire un controller classique, Spring propose de définir les chemins logiques de navigation en XML, et d’associer directement des méthodes sur des services pour récupérer la partie Model. Cette approche a l’avantage de simplifier le développement de workflow webs complexes. Je vais vous présenter le concept.
Pour commencer, nous définissions un flow en XML pour l’action qui affiche la liste des Hotel correspondant à un critère de recherche. Je ne vous montre que la view-state « reviewHotels » afin de ne se concentrer que sur l’exemple :
« main-flow.xml »
<?xml version="1.0" encoding="UTF-8"?> <flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"> <var name="searchCriteria" class="org.springframework.webflow.samples.booking.SearchCriteria" /> <view-state id="enterSearchCriteria"> // Effacé pour simplifier l'exemple... </view-state> <view-state id="reviewHotels"> <on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" result-type="dataModel" /> </on-render> <transition on="sort"> <set name="searchCriteria.sortBy" value="requestParameters.sortBy" /> <render fragments="hotels:searchResultsFragment" /> </transition> <transition on="previous"> <evaluate expression="searchCriteria.previousPage()" /> <render fragments="hotels:searchResultsFragment" /> </transition> <transition on="next"> <evaluate expression="searchCriteria.nextPage()" /> <render fragments="hotels:searchResultsFragment" /> </transition> <transition on="select" to="reviewHotel"> <set name="flowScope.hotel" value="hotels.selectedRow" /> </transition> <transition on="changeSearch" to="changeSearchCriteria" /> </view-state>
Le point fort de Spring WebFlow/JSF ici : il sera possible de ne redessiner qu’une partie de l’interface. En effet, vous avez noté le tag <render fragments= » »/> ? Grâce à ce système très pratique avec Spring WebFlow et JSF, si vous avez une partie cliente en Ajax il devient alors possible de ne redéfinir qu’une partie de l’interface.
Dans la démo Play! Booking, ce mécanisme est implémenté « à la main » dans la page index.html. Tout est fait avec du jQuery et le tag jsAction de Play!, qui appelle des actions du controller Play. Celui-ci retourne alors des fragments de HTML, ce qui fait que la version Play! est aussi ajaxisé. Je reconnais que c’est moins évident et facile, mais les 2 se discutent.
Il y a donc un BookingService avec une méthode findHotels pour rechercher la liste des hôtels, d’après le Flow XML. Il s’agit d’une interface, ce qui permet de changer l’implémentation. Dans notre cas nous n’aurons que la version JPA. Si vous avez une forte envie de faire une version PasJPA notez que c’est super utile…
public interface BookingService { // j'ai effacé les autres méthodes ici pour ne garder que l'essentiel public List<hotel> findHotels(SearchCriteria criteria); }
Le service implémente l’interface, j’ai supprimé une bonne partie du code afin de ne vous laissez que ce qui nous intéresse. Mais croyez-moi, la version complète est assez verbeuse. Ce qui nous intéresse ici c’est uniquement la méthode findHotels
@Service("bookingService") @Repository public class JpaBookingService implements BookingService { private EntityManager em; @PersistenceContext public void setEntityManager(EntityManager em) { this.em = em; } // Note : j'ai effacé du code ici pour ne garder que quelques méthodes @Transactional(readOnly = true) @SuppressWarnings("unchecked") public List findHotels(SearchCriteria criteria) { String pattern = getSearchPattern(criteria); return em.createQuery( "select h from Hotel h where lower(h.name) like " + pattern + " or lower(h.city) like " + pattern + " or lower(h.zip) like " + pattern + " or lower(h.address) like " + pattern + " order by h." + criteria.getSortBy()).setMaxResults(criteria.getPageSize()).setFirstResult( criteria.getPage() * criteria.getPageSize()).getResultList(); } // Note 2 : j'ai effacé du code ici pour ne garder que quelques méthodes // helpers private String getSearchPattern(SearchCriteria criteria) { if (StringUtils.hasText(criteria.getSearchString())) { return "'%" + criteria.getSearchString().toLowerCase().replace('*', '%') + "%'"; } else { return "'%'"; } } }
Le plus intéressant est donc la méthode findHotels. Elle permet d’effectuer la recherche, de retourner une List d’Hotel à la vue, que nous verrons tout à l’heure si vous êtes toujours motivé.
III.2) Le contrôleur version Play! Framework
L’approche de Play! est un peu plus simple mais demande un peu plus de code Java.
Je précise tout de suite : j’ai modifié l’exemple afin de retirer la partie Authentification et pour ne me concentrer que sur le Use-Case de récupérer la liste des Hotel correspondant à un critère entré dans l’interface utilisateur.
public class Hotels extends Controller { public static void list(String search, Integer size, Integer page) { List hotels = null; page = page != null ? page : 1; if(search.trim().length() == 0) { hotels = Hotel.all().fetch(page, size); } else { search = search.toLowerCase(); hotels = Hotel.find("lower(name) like ? OR lower(city) like ?", "%"+search+"%", "%"+search+"%").fetch(page, size); } render(hotels, search, size, page); } ... // Note : code effacé pour ne conserver que la méthode list ... }
L’approche de Play! Framework est donc la suivante :
– écrire une classe dans le répertoire « controllers » qui étend la class Controller de Play!
– écrire une méthode public static avec en argument les paramètres de la vue. Play! se charge de vous récupérer ce que l’utilisateur a entré. Notez ensuite l’appel pour récupérer la liste des Hotels :
search = search.toLowerCase(); hotels = Hotel.find("lower(name) like ? OR lower(city) like ?", "%"+search+"%", "%"+search+"%").fetch(page, size);
La méthode find est une méthode statique définie dans la classe JPASupport. Elle permet d’exécuter des requêtes JPA. Vous pouvez aussi écrire une méthode dans l’entité Hotel pour éviter de placer ici du code technique par exemple. Les 2 approches se valent.
III.3) Différence d’approche
La version Spring WebFlow demande plus de XML, ce qui personnellement me dérange. Je trouve que la définition des flows n’est pas simple. C’est même une fausse bonne idée. Je préfère une approche fortement typée en Java, où par contre pour que la magie opère, je dois étendre des classes spécialisées de Play! Framework.
Côté test, il est assez simple de tester Spring WebFlow, mais je n’ai pas testé personnellement.
Je vais être méchant avec Spring WebFlow : je trouve cela bien compliqué pour résoudre un problème simple. Ici quel est l’apport du XML ? Des annotations ? Est-ce que notre vie est plus simple grâce à ce système ? Permettez-moi d’en douter.
C’est peut-être une révolution par rapport à l’approche Struts, mais tant qu’à faire la révolution, il ne faut pas s’arrêter à un concept basé sur un fichier XML pour définir la navigation d’une application Web.
III. 4) Synthèse
Avantages de Spring Web Flow
– possibilité de déclarer en quelques lignes un bout de code pour que la vue récupère le Model sans devoir définir un controller.
– gestion de la navigation de l’utilisateur fine dans le site
– se repose sur des Services facile à tester pour s’assurer que le Model est correct
– bien meilleur que l’approche classique où les DAO dans les Services ne servent à rien
– Spring MVC récupère les paramètres de la vue et les dénormalise, ce qui est très pratique.
– bonne intégration de la sécurité
Inconvénients de Spring Web Flow
– clairement plus compliqué que Play! sur ce point
– XML tu aimes ou tu n’aimes pas, mais c’est ton environnement de développement Web
– gestion pas facile pour les débutants entre les view-states, les action-states, les transitions etc.
– l’EntityManager à la main dans le service : pas top. Mais c’est quelque chose qui est souvent factorisé dans des classes de GenericService par exemple, avec un DAO… Bref encore 3 ou 4 classes Java en plus.
Avantages de Play! Framework :
– le controlleur déclare des méthodes static et public qui deviennent autant d’URL dans la vue
– le framework se charge de récupérer et dénormaliser les paramètres de la vue
– approche full java
– les entités ont un moteur comme GORM en Grails mais plus simple. Cela permet d’avoir des finders comme méthode statique sur l’entité Hotel. C’est cependant statique et plus limité que Grails/GORM.
Inconvénients de Play! Framework :
– le controller de base étend une class de Play! (si la spécialisation vous dérange plutôt que la composition)
– la gestion des workflows compliqué demande un effort de développement par rapport à Spring WebFlow, qui sera capable de retomber sur ses pieds.
IV. La Vue
Je vais maintenant évaluer plusieurs critères :
– la création d’une page pour chacune des technos
– la qualité de l’expérience utilisateur avec l’utilisation d’Ajax
– la taille des pages webs et les temps de chargement
IV.1) Créer une page avec Spring Faces
Basé sur JSF, cette partie sera donc essentiellement une discussion sur JSF plutôt que sur Spring. Je parlerai un peu de la taglibs de Spring pour la partie validation du côté client, que je trouve très bien fait. Mais l’essentiel du débat porte plus sur JSF ici.
Dans le projet Spring Webflow, les pages sont construites à partir d’un template, avec la technologie Facelets. Très pratique, cela vous permet de construire le canevas de votre site, puis de spécialiser vos vues en écrivant peu de code.
Le template standard.xhtml :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <f:view xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:c="http://java.sun.com/jstl/core" xmlns:sf="http://www.springframework.org/tags/faces" contentType="text/html" encoding="UTF-8"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Spring Faces: Hotel Booking Sample Application</title> <sf:includeStyles/> <sf:resourceGroup> <sf:resource path="/css-framework/css/tools.css"/> <sf:resource path="/css-framework/css/typo.css"/> <sf:resource path="/css-framework/css/forms.css"/> <sf:resource path="/css-framework/css/layout-navtop-localleft.css"/> <sf:resource path="/css-framework/css/layout.css"/> </sf:resourceGroup> <sf:resource path="/styles/booking.css"/> <ui:insert name="headIncludes"/> </head> <body class="tundra spring"> <div id="page"> <div id="header" class="clearfix spring"> <div id="welcome"> <div class="left">Spring Travel: A Spring Faces Reference Application</div> <div class="right"> <c:if test="${not empty currentUser.name}"> Welcome, ${currentUser.name} | <a href="${request.contextPath}/spring/logout">Logout</a> </c:if> <c:if test="${empty currentUser.name}"> <a href="${request.contextPath}/spring/login">Login</a> </c:if> </div> </div> <div id="branding" class="spring"> <a href="#{request.contextPath}"><img src="${request.contextPath}/resources/images/header.jpg" alt="Spring Travel"/></a> </div> </div> <div id="content" class="clearfix spring"> <div id="local" class="spring"> <a href="http://www.thespringexperience.com"> <img src="${request.contextPath}/resources/images/diplomat.jpg" alt="generic hotel" /> </a> <a href="http://www.thespringexperience.com"> <img src="${request.contextPath}/resources/images/tse.gif" alt="The Spring Experience" /> </a> <p> </p> </div> <div id="main"> <ui:insert name="content"/> </div> </div> <div id="footer" class="clearfix spring"> <a href="http://www.springframework.org"><img src="${request.contextPath}/resources/images/powered-by-spring.png" alt="Powered by Spring" /></a> </div> </div> </body> </html> </f:view>
La page « reviewHotels.xhtml » avec 52 balises HTML+JSF permet d’afficher la liste des Hôtels :
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:sf="http://www.springframework.org/tags/faces" template="/WEB-INF/layouts/standard.xhtml"> <ui:define name="content"> <h:form id="hotels"> <div class="section"> <h2>Hotel Results</h2> <p> <sf:commandLink value="Change Search" action="changeSearch"/> </p> <ui:fragment id="searchResultsFragment"> <div id="searchResults"> <h:outputText id="noHotelsText" value="No Hotels Found" rendered="#{hotels.rowCount == 0}"/> <h:dataTable id="hotels" styleClass="summary" value="#{hotels}" var="h" rendered="#{hotels.rowCount > 0}"> <h:column> <f:facet name="header"> <sf:commandLink id="sortByNameLink" action="sort" value="Name" processIds="*"> <f:param name="sortBy" value="name" /> </sf:commandLink> </f:facet> #{h.name} </h:column> <h:column> <f:facet name="header">Address</f:facet> #{h.address} </h:column> <h:column> <f:facet name="header"> <sf:commandLink id="sortByCity" action="sort" value="City, State" processIds="*" > <f:param name="sortBy" value="city" /> </sf:commandLink> </f:facet> #{h.city}, #{h.state}, #{h.country} </h:column> <h:column> <f:facet name="header">Zip</f:facet> #{h.zip} </h:column> <h:column> <f:facet name="header">Action</f:facet> <sf:commandLink id="viewHotelLink" value="View Hotel" action="select"/> </h:column> </h:dataTable> <div class="buttonGroup"> <sf:commandLink id="previousPageLink" value="Previous results" action="previous" rendered="#{searchCriteria.page > 0}"/> <sf:commandLink id="nextPageLink" value="More Results" action="next" rendered="#{not empty hotels and hotels.rowCount == searchCriteria.pageSize}"/> </div> </div> </ui:fragment> </div> </h:form> </ui:define> </ui:composition>
Ce que j’apprécie à titre personnel avec JSF :
– possibilité d’écrire du code de mise en forme sans avoir besoin de faire trop de CSS
– moteur de construction de page assez proche d’une vue client lourd
– possibilité d’écrire avec la syntaxe JSF et d’avoir un rendu riche sans se prendre la tête avec du Javascript ou des feuilles de style.
<HUMOUR>De toutes les façons, je suis pas développeur Web. Donc mort aux CSS…</HUMOUR>
Ce qui me fait un peu tiquer, c’est que l’on dit au développeur Java : tu n’auras pas besoin d’apprendre à faire une mise en page compliquée en HTML et CSS…
Mais bon, vous vous foutez pas un peu de nous là ?
La page demande 52 balises. Et ce dataTable pourrait très bien être un simple TABLE en HTML avec une CSS non ? Ce qui d’ailleurs est le cas au final non ?
IV.2) Créer une page avec Play! Framework
Attention cela pique les yeux. Comme JSF ou Wicket, vous pouvez utiliser un éditeur HTML pour construire votre page. En effet, le format des pages de Play! Framework est très original puisque c’est du HTML ni plus, ni moins. Il y a cependant l’utilisation de Groovy qui risque de faire peur à quelques uns. Je note cela comme un inconvénient par rapport à JSF.
La version livrée avec Play! utilise jQuery et Ajax à fond. Il y a en fait une page principale, puis un ensemble de fonction Javascript pour ne charger rapidement que ce qui change dans un DIV au format HTML. C’est radicalement différent, et peut-être moins facile d’abord pour des gens n’ayant jamais fait de Web. Je vous montre donc plutôt ici ma version « gars qui a fait du Java et du Web et qui découvre Play »
Play! Framework dispose aussi d’un moteur de template. Je vais donc vous montrer 2 fichiers. Le fichier main.html est un template de base que j’utilise pour le site :
<!DOCTYPE html> <html> <head> <title>#{get 'title' /}</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <link rel="stylesheet" type="text/css" media="screen" href="@{'/public/stylesheets/main.css'}"> #{get 'moreStyles' /} <link rel="shortcut icon" type="image/png" href="@{'/public/images/favicon.png'}"> <script src="@{'/public/javascripts/jquery-1.3.2.min.js'}" type="text/javascript" charset="utf-8"></script> <script src="@{'/public/javascripts/sessvars.js'}" type="text/javascript" charset="utf-8"></script> #{get 'moreScripts' /} </head> <body> <div id="header"> <h1>play framework booking demo</h1> #{if user?.id} <div id="options"> Connected as ${user.username} | <a href="@{Hotels.index()}">Search</a> | <a href="@{Hotels.settings()}">Settings</a> | <a href="@{Application.logout()}">Logout</a> </div> #{/if} </div> <div id="content"> #{if flash.error} <p class="fError"> <strong>${flash.error}</strong> </p> #{/if} #{if flash.success} <p class="fSuccess"> <strong>${flash.success}</strong> </p> #{/if} #{doLayout /} </div> <div id="footer"> Created with <a href="http://www.playframework.org">play framework</a> and really inspirated from the booking sample application provided by <a href="http://seamframework.org/">seam framework</a> </div> </body> </html>
Voici la page qui affiche la liste des Hotels. Le fichier s’appelle « list.html » comme le nom de la méthode list du Controller Hotels que nous avons vu plus haut. Il est bien entendu possible de déclarer un nom différent et de configurer une autre route avec Play! Framework.
Le fichier est assez simple : il reçoit un objet List<Hotel>, il itère en Groovy avec un tag spécifique à Play et il affiche le résultat dans un tableau HTML :
#{extends 'main.html' /} #{set title:'Search result'/} #{if hotels} <table> <thead> <tr> <th>Name</th> <th>Address</th> <th>City, State</th> <th width="8%">Zip</th> <th width="13%">Action</th> </tr> </thead> <tbody> #{list hotels, as:'hotel'} <tr> <td>${hotel.name}</td> <td>${hotel.address}</td> <td>${hotel.city}, ${hotel.state}, ${hotel.country}</td> <td>${hotel.zip}</td> <td> <a href="@{show(hotel.id)}">View Hotel</a> </td> </tr> #{/list} </tbody> </table> <p> <a id="nextPage" href="${page+1}">More results</a> </p> #{/if} #{else} <p> No more results </p> #{/else}
La version Play! est très dépouillée. L’inconvénient : il sera nécessaire dans les 2 cas (JSF comme Play!) d’apprendre la technologie de rendu et les tags pour travailler efficacement. Mais Play! n’est pas orienté composant comme JSF. Il n’y a pas d’utilisation de l’Expression Language, ici c’est Groovy. Ce moteur Groovy est un poil plus lent que Japid, un autre moteur Java qui est disponible pour Play!
Regardez aussi la liste des modules de Play! Framework. Il y a du lourd comme le support de Scala, le moteur NoSQL MongoDB, Guice, GWT ou le module Search avec Lucene (comme Hibernate Search en plus simple).
Un inconvénient de Play! pour les personnes qui n’ont jamais fait de Web : la mise en page et la partie CSS. Lorsque je monte un projet avec Play! Framework, j’utilise par exemple CSS 960 grid system. Je connais ces systèmes car j’ai un gros background Web. Pour le CSS, pour sélectionner des couleurs qui ne font pas vomir l’utilisateur, j’ai pleins d’outils comme Kuler d’Adobe pour préparer des styles… Donc pour moi qui suit très Web : je peux faire un truc joli avec Play! Framework. Je pourrai styliser mes tableaux sans soucis. Je pense que ce qui est bien avec JSF, c’est que cela vous aide à monter vos pages facilement, à construire vos formulaires ou vos tableaux sans devoir devenir une bête en HTML/CSS. Mais c’est un peu dommage, lorsque l’on voit le potentiel d’HTML 5 pour les années qui viennent.
IV.3) Expérience utilisateur
J’ai lancé les 2 applications et j’ai effectué une comparaison en tant qu’utilisateur. Je donne l’avantage à la version Spring Faces. Je trouve que la librairie de Spring de validation du côté client est très sympa.
En terme de rapidité, la version Play! Framework est vraiment plus réactive. Si je clique comme un dingue avec un script Selenium, la version Spring WebFlow est à la peine alors que la version Play! se tourne les pouces sans s’essoufler.
IV.5) Taille des pages Webs
La page d’affichage de la liste des Hôtels sur Spring WebFlow (cliquez sur l’image pour l’afficher en grand) :
Mes tests avec YSlow en effectuant 10 chargements de la page puis en prenant la moyenne :
Moyenne des temps de chargement de la page liste des Hotels pour 10 hotels:
– Play! Framework en mode Dev ou Prod : 0,234 secondes
– Spring WebFlow avec Jetty : 1.286 secondes
– Spring WebFlow avec Tomcat 6.0.20 : 1.092 secondes
Le mode Prod de Play! ne prend qu’un peu plus de temps à démarrer, mais je n’ai pas constaté de différence une fois le serveur lancé.
Volume de la partie HTML sans CSS/Images et temps de chargement :
Si je ne regarde que la partie HTML sans prendre en compte le temps de chargement des images, je teste réellement que la partie serveur.
Play! Framework : chargement de 3.1 kb en un temps moyen de 14ms
Tomcat+Spring : chargement de 14kb en un temps moyen de 52 ms
Donc pour un affichage similaire, vous voyez le souci ? la version Spring Faces de cette démonstration est 5 fois plus lourde et demande 4 à 5 fois plus de temps. Je ne dis pas qu’en général c’est le cas, mais pour notre exemple oui c’est le cas. Et nous sommes sur le même cahier des charges : afficher dans un tableau une liste d’Hôtel.
V. Conclusion
Si vous avez pris le temps de lire cet énorme article jusqu’au bout, merci. Revenez voir le code des deux version en détail. Téléchargez chacune des versions et donnez vos propres arguments techniques en faveur de l’un ou de l’autre.
J’ai envie de taper du poing sur la table : il est temps de revoir le développement Web en Java.
Le développement Web demande des compétences. Si vous ne les avez pas, ne prenez pas une techno pour couvrir vos lacunes. Apprenez à faire du Web. Désolé, le XML ne m’excite pas. Play! est plus facile à apprendre que toute la stack Spring que je vous ai présenté. Car elle est plus simple et tout aussi puissante.
Pour construire une application Web compliquée ou d’entreprise, est-ce que dans 5 ans nous ferrons toujours du JSF ? ou est-ce que nous ferrons du HTML+CSS+jQuery ? Je pose la question. Evidemment, Play! Framework doit encore faire ses preuves et on peut me retourner l’argument. Mais c’est plus simple, plus réaliste et plus facile à maintenir.
Le point faible de Play! par rapport à un framework comme Spring Faces+Spring WebFlow+JSF c’est qu’il faut plus d’efforts pour construire ses composants dans la partie vue. D’un autre côté, lorsque vous avez un framework aussi puissant que JSF, vous avez envie de prendre des tableaux avancés, des richTables, ou des composants plus puissants. Cela entraîne parfois le risque de prendre des composants qui font trop par rapport à un tableau simple. D’ailleurs, si vous ne preniez pas ces composants, quel intérêt à utiliser une librairie JSF ? Je pose la question.
Un risque est donc d’utiliser des composants riches trop facilement dans une application Web avec JSF. Vous vous retrouvez avec un tableau avec des colonnes triables, alors que vous n’en n’avez pas besoin. Et votre page pèse 15-20kb au lieu de peser 2-5kb, et demande de la mémoire et du CPU du côté serveur.
Le site « eXpress-Board » tourne sur la même machine que le blog « Le Touilleur Express » avec 3 parts chez Gandi, soit 768Mo. Le process Java tourne avec 128 Mo de mémoire, et prend entre 5 et 16% de l’utilisation de la machine, bien loin de MySQL qui est très sollicité par WordPress pour le Blog.
Je vais me faire taper par les maîtres Javas qui ne vont pas être d’accord. Mais c’est la première fois où j’en ai rien à faire, car je suis sûr de mes arguments.
Allez SuperTroll, sort du bois, je t’attends.
On va bien s’amuser…
0 no like
Merci pour l’article Nicolas. C’est très intéressant.
Cependant je me permet de corriger quelques erreurs sur la partie Model.
> Moi depuis que l’encapsulation n’est plus à la mode en soirée, j’ai arrêté.
En fait dans la version play l’encapsulation est respectée. Les getter/setter sont générés par défaut. Est il est possible de les surcharger en les définissant soit même si besoin. Ca ressemble a la gestion de properties en JavaScript ou en Scala.
D’ailleurs ce n’est pas du tout obligatoire de faire ça avec play, et le Java bean de l’exemple spring fonctionnerait tout aussi bien. Juste question de style.
> Le bean de Spring n’étend pas une classe de base contrairement à Play!, ce qui permet d’étendre éventuellement Hotel.
Dans la version play aussi. Tu peux également étendre Hotel avec une autre entity. Elle héritera du coup également de Model donc tout se passe bien.
Après sur les composants riche l’approche de play c’est d’utiliser des composants riches coté client. Par exemple si on a besoin d’une rich table on peut en trouver plein plein. Par exemple un plugin JQuery.
Enfin, même si ça semble plus compliquer de designer le workflow dans play, un avantage certain et que toutes les requêtes sont réutilisable hors contexte. Par exemple si tu voulais extraire une API pour lister les hotels ou faire une reservation, ou même simplement faire du mashup web depuis un autre site, c’est très simple. Chaque requête est indépendante.
Guillaume.
Toutle monde sera d’accord avec ta conclusion.
Utiliser JSF ou Webflow pour faire un site web c’est juste totalement inadapté c’est évident. Oui certains en abusent, c’est mal 🙂
Clairement pour réaliser un site web il faut utiliser des outils qui y excellent comme play!, grails, rails, django, cake, symphony, lift ou même côté java avec Spring MVC, Struts2.
Chacun de ces outils fait largement l’affaire et chacun d’entre nous pourra trouver chaussure à son pied.
JSF, Wicket ou Webflow eux sont utilisés dans un contexte différent pour nous aider à pouvoir répondre à des besoins clients qui sont diamétralement opposés à la philosophie web.
Hello Nicolas,
merci pour l’article, très intéressant, par contre 3 remarques:
» getter/setter »
Pour moi, c’est THE endroit constamment sous exploité où peuvent s’implémenter les règles basiques voire bien plus compliquées de validation/business. Tout le monde laisse les setters vides, alors qu’il y a de quoi faire dans ces petites méthodes…
Dans commande.setProduit , par exemple, implementer
if produit.getCurrentStock = 0 throw YaPlusRienEnStockException (business, not RunTimeException)…
Ton bean Session (stateful bien sur) catch l’exception, délègue à un autre service pour déclencher une commande et sait avertir l’utilisateur ….
L’autre service n’a que faire de Play! mais sait exploiter l’entité Produit (POJO sinon il faut veiller à déployer Play!.jars partout dans le monde)
pourquoi chercher à déporter ce code ailleurs?
« – Play! Framework étend la classe « play.db.jpa.Model » qui fournit le support JPA (nous verrons pourquoi plus loin) »
Il faudrait ajouter à ton article un gros Warning indiquant les effets de bords négatifs d’un tel héritage technique(petite illustration faite précédemment). Ce n’est pas pour rien que JPA a pour but principal la persistance de POJO, tes entités n’en sont plus ici si j’ai tout compris. Si j’ai mal compris, veuillez m’excuser.
« Le bean de Spring n’étend pas une classe de base contrairement à Play!, ce qui permet d’étendre éventuellement Hotel. »
Qu’est ce qui t’empêche d’étendre Hotel avec Play!? Play! n’autorise pas l’utilisation des annotations dédiées à l’héritage? Désolé si j’ai manqué qqch d’évident.
« Mais est-ce que c’est quelque chose que nous faisons pour des Entities ? Je me pose la question. Perso je décourage l’héritage avec JPA. FBI = Fausse bonne idée. »
Shocking 🙂 Pourrais tu élaborer? L’héritage est au coeur de la conception OO.
Pourquoi serait-ce une mauvaise idée de modéliser ClientParticulier et ClientEntreprise extends Client par exemple? Puis d’avoir PorteFeuilleClient 1–* Client (zoup polymorphisme… super utile).
Après, point de vue relationnel, une table par hiérarchie ne pose aucun soucis, ça boost. Je vois pas.
Sinon la partie bench est assez impressionnantes.,
Très intéressant ce comparatif!
Ta conclusion correspond bien à l’idée que je me fais de Play! depuis que je m’amuse avec.
Quand on code avec ce framework on a vraiment une sensation de simplicité (je dirais même de plaisir) et on se demande même pourquoi on a pas fait comme ça depuis le début. Pourquoi pendant toutes ces années a-t-on eu envie de pondre les architectures les plus complexes possible en java ?
Mais les habitudes ont la vie dure et je vois encore des gens qui pensent qu’il est nécessaire d’avoir un maximum de couches techniques, même ils elles ne font rien dans une appli java…
C’est vrai qu’avec Play il est plus long de construire des composants qu’avec des librairies JSF prêtes à l’emploi, mais je pense qu’on peut profiter du gain de temps apporté par Play sur tous les autres aspects pour se faire des petits tags de composants personnalisés et réutilisables, sur le long terme on devrait y gagner par rapport à wicket (que j’apprécie pourtant beaucoup), JSF et autres. Apres si on veut vraiment des composants ultra riches, comme tu dis ce n’est plus du web classique et là autant passer sur du flex/silverligt ou sur du GWT + GXT…
@framiere : notre boulot c’est pas de convaincre les clients de changer ? Lorsque Spring est sorti, que la guerre contre J2EE et les EJB 2.1 a débuté, les gens applaudissaient des 2 bras. Pourquoi ne pas faire pareil pour le Web ?
@Anthony : j’ai envie de dire que les POJOs dans le cadre d’une application Web Java, c’est pourri. Je pense que le Web a des besoins fonctionnels et des utilisateurs qui sont différent d’une plateforme orientée Service. Donc oui, tu aurais 2 entités dans mon idée : une pour le Web et une pour le service. Si tu as des POJO anémiques pour la partie Web, qui ne font rien, tu dois alors avoir des classes de Service+DAO pour perfuser ce POJO et le faire vivre pour le Web. Dans l’approche de Play! on a des concepts du Domain Driven Design, où mes Entités ont la possibilité d’embarquer des services et sont dotées de super-pouvoir. C’est pour cette raison que tu as effectivement un « extends Model ». Ensuite je suis d’accord, j’ai dis une grosse bêtise concernant l’héritage. Mea culpa.
Faudra qu’on en parle autour d’une bière à l’occase 🙂
Merci pour ton retour, qui équilibre un peu le débat dans l’autre sens.
@Loic : GWT et Play! Framework, il semble que cela marche. Il y a un module Play, je n’ai pas testé, ni entendu de retours pour l’instant. Il y a aussi un gars qui a fait du Flex, avec donc une partie serveur écrite en Play, qui balance du XML pour la partie Play. Et enfin, j’ai vu un article qui parle d’ext-js (la lib JS uniquement) et de l’intégration avec différents frameworks Webs. A creuser donc.
Ah oui c’est vrai qu’on peut aussi faire du GWT avec Play, guillaume bord en avait parlé chez les cast codeurs, et je crois qu’il y a un exemple dans les sample de Play. Si j’ai bien compris GWT ne générant que du code client (html/js/css) il peut se substituer facilement aux templates html utilisés par défaut. Bon dans ce cas Play répond à tous les besoins CQFD 😀
@nicolas: Nous devons conseiller nos client bien entendu. Il se trouve que la plupart du temps les fonctionnalités complexes demandées sont conservées. Lorsque l’on est dans ce cas là … on doit faire avec.
C’est là ou je trouve que les frameworks web pure-player à la play, grails etc. montrent la limite de ce qu’ils nous apportent. Les frameworks stateful sont dans ce cas une approche plus adaptée.
« Donc oui, tu aurais 2 entités dans mon idée : une pour le Web et une pour le service. »
Ahhhhhh au secours, on nous refait le coup des DTO donc? Ou alors 2 branches ‘entité’ pluggées sur le même datastore à maintenir?
Donc à nuancer, la productivité accrue niveau web est donc en partie annulée par l’overhead d’une couche d’adaptation requise entre le web et le service. Faut être complet dans un tel débat.
De ce que je comprends, c’est génial pour une appli purement web qui n’a pas à s’intégrer dans une urba existante, n’a pas à exposer de services, en gros une appli isolée. Elle pourra invoquer des services extérieurs mais au cout d’une certaine couche d’adaptation. Ne nous leurrons pas, même avec les POJOS c’est souvent le cas mais peut être plus facile.
Dans ce contexte, je conçois très facilement la valeur ajoutée sinon personnellement c’est un inconvénient de taille.
Ce n’est pas spécifique à Play!, c’est commun à tous les FWKS qui imposent justement qqch au modèle.
Pour revenir au terme Model Driven Design, ce ne serait pas tout l’opposé? S’affranchir de toute considération technique pour effectuer une conception purement centrée sur les problématiques métier? D’ailleurs la littérature que j’ai lue sur le sujet expliquait bien qu’on a pas besoin de connaitre la technique pour concevoir le ‘modèle’. Viennent ensuite les autres phases du projet où l’on s’adapte mais le tout reste guidé par ces modèles métier initiaux.
Note: méthodo mise en application sur divers projets, je ne me suis pas contenté de ‘lire’ :-).
Enfin et la c’est le plus important, tu es le bienvenu pour une bonne bière lors de ta prochaine visite sur Lille! C’est moi qui régale, avec plaisir.
Attention, pour le model, il n’est pas requit d etendre la class model, donc je ne comprends pas trop le debat… On peut tout aussi bien utiliser de simple POJO a la JPA. La class Model permet juste de partir sur de bonne base.
Seulement les controllers doivent etendre la class controlleur de base.
Great with a detailed comparisson, but for my high school french this is a bi hard to follow. For the greater Play and Spring communities (and others evaluating this) it would be better for this to be in english. Any one care to post a translation somewhere?
Article intéressant, un vrai test de charge avec JMX serait intéressant.
Quand au code de la partie JSF, il y a plein de choses à redire… L’utilisation de JSTL (beurk), les validateurs spring. Et on peux clairement faire quelque chose plus concis.
Alors tu nous le fais quand l’article basé sur des techno un peu plus up to date (JSF 2 + weld + EJB 3.1 ) ?
@Nicolas Leroux : article corrigé, j’ai barré ce qui n’est pas bon. Merci Nicolas 🙂
@cespasdur : excellente idée. Il y a aussi une personne qui veut faire une version RESTHub (un autre framework comme Play!) afin de comparer. Mes mesures sont simples : c’est le navigateur en local, ce qui reflète les temps de chargement pour une appli web.
Nicolas, merci pour cet excellent article.
Je me pose simplement certaines questions sur la mise en oeuvre de Play!. Personnellement, je n’ai pas encore eu le temps d’investiguer mais ce framework me séduit. Par contre je me pose des questions sur les capacités de Play! à évoluer.
Changement technologique ou ouverture, nombreux sont les clients qui, en partant d’une application Web, vous demandent de s’intégrer avec un portail, d’offrir des end points pour s’intégrer avec une applications tierce, de réaliser une version en client lourd …
C’est ce que je n’arrive pas à voir avec Play!? Alors qu’avec des technos comme Spring, il est possible et parfois facile de faire évoluer son système.
Il s’agit là d’une question et non d’une affirmation.
De plus cet article augmente encore mes incertitudes technologiques. Si aujourd’hui on me demande de partir from scratch je ne sais pas quels technologies / frameworks choisir à la fois pour le front end et pour le backend. Mais il s’agit peut être là d’un autre échange d’idées qui sera peut-être abordé sur un futur post sur ce blog.
@Anthony Patricio : merci de ne pas confondre MDD et DDD. DDD s’appuie intensivement sur le code pour représenter le métier, vu que son concept de base est justement de vouloir fusionner le modèle d’analyse avec le modèle de conception. Ce que tu décris ressemble justement plutôt à de l’anti-DDD : tenir bien séparer le métier du code qui le représente. J’encourage la lecture du bouquin de référence d’Evans pour éviter les confusions.
Pour ajouter au débat des frameworks Web, pourquoi Restlet n’est-il jamais cité? Si on parle des standards du Web, je ne connais pas d’autres frameworks en java qui les respectent autant.
Merci pour cet article Nicolas. On sent que tu t’es bien investi sur le sujet.
Je trouve effectivement que la dimension HTML/CSS/JS manque aux développeurs JEE. C’est souvent considéré comme « sale », laissé aux intégrateurs Web. Faut dire aussi qu’un profil Web est souvent beaucoup moins facturé qu’un profil JEE.
@Guillaument Bort : « En fait dans la version play l’encapsulation est respectée. Les getter/setter sont générés par défaut. » => Je ne suis pas sûr de comprendre. A quel moment les getter/setters sont-ils générés ? Au lancement de l’application ? Quel intérêt ? Est-ce que cela a vraiment un rapport avec l’encapsulation, ou est-ce pour d’autres raisons ?
Pour l’intégration Play! / GWT, la doc est ici : http://www.playframework.org/documentation/1.0.1/gwt , mais je ne suis pas sûr qu’elle soit à jour. Utiliser un backend développé en Play!, pourquoi pas. Mais dommage de ne pas proposer un mécanisme de sérialisation alternatif à GWT-RPC.
C’est pour moi le gros défaut de GWT actuellement : de base, le transfert de données est en GWT-RPC, ça offre des facilités côté GWT client, mais par contre ça rend quasi impossible la réutilisation des services exposés par un tiers (par ex, une appli Android).
J’ai testé Restlet (http://www.restlet.org/) hier, ça avait l’air bien dans l’idée, mais la doc est bien trop sporadique, du coup dès qu’on sort du chemin tracé du tuto on ne sait plus quoi faire.
Dsl, jcrois que j’ai fait un ptit HS 🙂 .
@Jean-Baptiste Dusseaut: my bad, désolé pour la confusion, tu as tout a fait raison. Et la littérature dont je parlais et effectivement celle d’Evans. Ca m’apprendra à commenter en faisant 36 trucs en même temps.
@Nicolas Leroux: info intéressante et primordial qui mériterait une note dans l’article initial. Effectivement, +1 pour Play! dans ce cas.
@Jean-Baptiste Dusseaut : RESTlet respecte les standard du web mais pas les standards Java. Pour moi Jersey (l’implémentation de référence de JAX-RS 1.1) le bat sans forcer.
Comme je le disais à Nicolas sur Tweeter, j’ai commencé à porter l’application booking en utiliant la stack Spring 3 – Jersey – GenericDao/Service/Controller – Jquery UI qu’on propose dans RESThub au lieu de Play!
Les points marquants pour l’instant :
– La transcription de l’exemple Play en utilisant les annotations standards J2EE6 comme @Inject, @Entity, @Named, les beans validation, confirme ce que je pensais : on peut faire aussi simple que Play avec une bonne stack Java + une bonne stack Javascript, du moment qu’on utilise des classes génériques, pas de framework Java MVC et qu’on fait du vrai RIA (sesssion côté client, IHM générée côté client, etc.)
– Les templates Play! et ceux en ExtendedJS utilisés par RESThub sont très proches. 2 différences majeures : on utilise juste Javascript à la place de Groovy, et les templates sont rendus côté clients (je sais, ça fait bizarre la première fois 😉
– Je trouve très limite en terme de sécurité le sessionsvar.js. OK, c’est très malin d’utiliser window.name pour stocker des megas de données, mais c’est pas secure du tout ! Sur la version resthub, on utilisera le sessionStorage uniquement, avec les restictions en terme de navigateurs qui vont avec (IE8, FF3, Safari 4 ou Chrome minimum).
– Un truc qui sera pas fait pour la demo mais sur lequel on bossera un peu après : un plugin jQuery qui récupère automatiquement les informations disponibles dans le modèles annoté avec bean validation pour valider les formulaires.
– On pourrait merger les 2 mais je préfère garder 2 couches DAO et Services séparées
– Je garde mes getters et setters sur mon modèles, ça prend 1 seconde à générer dans mon IDE Java et ça me permet en + d’overrider les annotations dans les classes filles. Je serais peut être pas à la mode mais bon tant pis !
@Piwaï les getters/setters sont générés au runtime (play modifie les fichiers .java avant de les compiler), l’intérêt est que si tu dans ton code tu as une classe « Person » avec un « public String name » à l’interieur, si tu appeles myPerson.name, le code compilé correspondra à myPerson.getName().
Donc si un jour tu as envie de faire un getter pour le champ name pour par exemple renvoyer le nom en majuscule, tu n’auras qu’à créer ce getter, les classes qui accèdent à ce champ ne seront pas impactées et tu pourras continuer à écrire « String name = myPerson.name » dans ton contrôleur (par exemple)
@Bouiaw J’ai pas encore eut le tps de regarder RESTHub, mais je compte bien le faire. Pour JAX-RS, c’est une autre approche qui est tres orientee modele et c’est quand meme bien complique pour pas grand chose des fois (et bonjour le nombres d’annotations au final)…
sessionStorage oui tu as raison. sessionsvar est juste donne a titre d’example.
Pour la validation cote client a partir des metadata du modele, je travaille justement sur un module pour faire ca (faut que je le finisse d’ailleur), donc nos idees se rejoignent encore une fois 😉
La derniere fois je pensais justement a cette idee de rendu de template cote client, ca me parait une bonne idee. Faut vraiment que je regarde RESTHub 😉
« on peut faire aussi simple que Play avec une bonne stack Java + une bonne stack Javascript » Oui enfin, tu auras besoin de connaitre toutes les stacks et surement une tonne d’annotations a la fin je pense que apprendre toutes les stacks (jax-rs, bean val, spring 3, etc) ca prends bcp + de tps que d apprendre play!. Et tu auras toujours un cycle + long de deployment (coder, compiler, deployer (meme a chaud)) meme si peut etre maintenant ca se fait tres rapidement.
Mais sinon, oui, c’est une bonne idee ce projet RESTHub, c’est ce que je faisais en quelque sorte manuellement avant.
@Nicolas Leroux La stack prend effectivement un peu plus de temps à apprendre dans l’absolu que Play!, par contre avec une équipe de développeurs Java déjà formés à du Maven/Spring/JPA ce qui correspond à pas mal de monde je pense, ça se vaut.
Il faut se méfier des préjugés. Je me rapelle l’an dernier pour faire des webservices REST j’étais parti sur RESTlet sans même me donner le peine de regarder JAX-RS. Je me suis aperçu un peu plus tard que JAX-RS était bien plus simple !
Quand dans l’application booking j’ai converti les annotations de validations Play en annotations beans validation J2EE6, c’était juste de la traduction 1 pour 1.
Je dis pas que c’est pas le cas pour tout hein, Play! reste plus simple sur certains aspects notamment grâce à son aspect « tout intégré » et à sa excellente documentation.
Concernant le temps de déploiement qui est un problème récurent dans le monde Java, on le « résout » de manière très pragmatique. En fait, on a une séparation très claire entre le serveur Java (DAO/Service/Webservice REST) et client Javascript/HTML/CSS (La vue). Donc en pratique on monte notre partie serveur avec des tests unitaires (dont certains génériques dont on a juste à hériter et à compléter) très rapides à lancer.
Côté client, ce qui correspond à la plupart des cycles de déploiement, on est en full javascript donc un rechargement c’est un F5 dans le navigateur à effet immédiat !
Pour compléter, il faudrait juste un agent capable de remplacer à chaud les implémentations Java pour lesquelles l’interface ne change pas. Un genre de JRebel light open source. Je crois que Play a ça …
@Anthony Patricio : désolé à la relecture mon message sur DDD paraît un peu violent, merci de l’avoir bien pris ^^
@Bouiaw : l’argument sur les standards me paraît un peu mal placé dans une discussion sur des frameworks qui justement prennent un malin plaisir à inventer des manières plus simples que les standards de faire les choses 🙂 Restlet 2.0 est vraiment d’une simplicité enfantine, si on accroche aux concepts ROA, il n’y a rien à apprendre, il se comporte juste comme il le doit, sans prise de tête, sans configuration. Je n’ai pas d’actions chez eux, mais je trouve qu’il laisse Jersey loin derrière.
Merci pour cet article Nicolas !
– Je pense également que les frameworks comme Play! signent (enfin?) le glas des développements (pseudo) web comme ceux réalisés à base de JSP et consort. Le bouquet aura été sans nul doute GWT ! Je pense ne pas avoir été le seul à avoir cru, que faire du Struts/JSF/JSP/.., c’était faire du web ! Et quelle ne fut pas ma surprise, lorsque j’avais à mettre une pincée de JS ou CSS dans mes softs, j’avais l’impression de salir mon code « web » à base de petits tag bien indentés, en fait c’était l’inverse qui se passait.
– Quoiqu’il en soit, je pense qu’un frmk comme Play! redonne ses lettres de noblesses au dév. web et qu’il démontre que l’on peut faire du Java et du vrai Web dans le même monde !
Ulrich
Bravo pour l’article, et merci à ceux qui font les frameworks Play! et Resthub. Ca fait du bien de voir arriver ces frameworks, « respectueux » des technos web!
Les frameworks comme GWT ou wicket, qui encapsulent le web dans des API Java, sont magnifiques en terme d’architecture, mais devraient être utilisés en dernier recourt je pense.
@Bouiaw : Concernant les templates en ExtendedJS, ca ne gêne pas l’indexation Google?
@Jean-Baptiste Dusseaut je précise que c’est Restlet 1.0 que j’avais testé à l’époque. Je trouve la façon de faire à la JAX-RS plus simple mais bon c’est subjectif j’en conviens. Restlet reste un bon projet qui permet notamment du faire du REST avec autre chose que http, avec une API cliente assez classe, etc.
@Olivier effectivement, je ne pense pas que les moteurs de recherche soit assez évolués pour dynamiser le DOM avec du Javascript avant de le parser. Pour répondre à ce genre de problématique, on avait en tête de permettre la dynamisation des templates côté client ET serveur. Dans le cas de embeddedJS, on doit pouvoir faire ça avec Rhino (l’interpréteur Javascript écrit en Java). On rajoutera une règle de ré-écriture au niveau du serveur web pour rediriger vers la bonne version.
C’est un peu la stratégie utilisée par Opéra Mini pour afficher des sites complexes sur des mobiles limités en terme de navigateur HTML.
Hello,
Personnellement je ne comprends pas le sens de ton bench.
Il aurait fallu comparer ce qui est vraiment comparable, par exemple la gestion des flows avec Spring Web Flow (qui est fait pour ça) versus la gestion de flow avec Play (qui j’ai l’impression n’est de toute façon pas fait pour ça)
Avec Webflow tu peux gérer simplement
– une navigation très complexe (très simplement)
– un persistance contexte étendu au flow (très simplement)
– l’intégration avec Spring Security, pour limiter l’accès à certaines vues/transitions (aussi très simplement)
– note que le XML peut se recharger à chaud… c’est assez pratique (SWF 3 proposera une version Java)
Coté vue, tu peux faire du JSF/RichFaces (avec SpringFaces) ou du JSP + JQuery par exemple… c’est assez ouvert en fait… Tu peux même même mettre SWF en contrôleur d’autres FWK web.
Bref, que propose Play coté gestion des flows? De faire les flows complexes à la mano en Java?
Le mieux serait probablement que Play propose une extension Spring Web Flow.
Petite remarque sur Spring: quand tu écris:
« Dans notre cas nous n’aurons que la version JPA. Si vous avez une forte envie de faire une version PasJPA notez que c’est super utile… »
Je sens une pointe d’ironie… les interfaces permettent effectivement de changer d’implémentation. On peut par exemple utiliser des implémentations ‘proxy’ pour gérer les transactions, la sécurité etc. Bref avec Spring c’est tellement transparent que même les meilleurs finissent par l’oublier 😉
Bref tu attendais des trolls mais j’ai l’impression que c’est toi qui trolle un peu, non?
Viva Spring Web Flow, Viva Spring
Nicolas.
Un complément sur les remarques sur GWT.
D’une part le choix est clairement sur le cas d’utilisation. Si je prend en exemple un site d’ecommerce pour la partie vente front-office il ne faut surtout pas utiliser GWT (pas d’indexation, de navigation pages à pages ==> pas de référencement), par contre pour la partie back-office c’est un très bon candidat.
D’autre part, faire du GWT sans connaitre le web c’est pas forcément le bon plan. Il faut quand bosser sur le CSS et donc bien connaitre ce que génère chaque widget et panel GWT (et ce la évitera de faire une application web avec 20 tableaux imbriqués qui fait ramer le navigateur).
@Nicolas @GuillaumeBort Quid du module de sécurité de Play! par rapport à Spring Security (qualité et modules optionnels comme OpenID) ?
@youen: juste pour information, google indexe désormais l’ajax.
Plus d’info ici http://code.google.com/intl/fr/web/ajaxcrawling/docs/getting-started.html
Play! me plait dans son ensemble, à un détail près : je pense que l’utilisation des méthodes statiques (list, find) rend les tests très difficiles, pas possibilité de mocker, ni de surcharger. comment testez vous vos controleurs, vos vues? merci pour le retour 🙂
Nicolas,
Toi tu vas avoir de gros problemes!
La prochaine fois que tu postes du code avec la concatenation de parametres dans une chaine JP-QL, je te casse le pouce de la main droite.
Utilise les parametres BORDEL:
– c’est plus performant
– et surtout ca evite qu’un idiot te fasse de l’injection SQL à Papa et vide ta base sans que tu comprennes (voir photo )
Tu crois qu’on a crée query.setParameter pour faire plaisir à ta grand-mère?
C’est pas de sa faute, Mr Bernard, c’est du code exemple de chez SpringSource…
Enfin ceci dit font pas mieux chez JBoss hein :
hotels = em.createQuery("select h from Hotel h where lower(h.name) " + "like #{pattern} or lower(h.city) like #{pattern} " + "or lower(h.zip) like #{pattern} or " + "lower(h.address) like #{pattern}")
…Tout droit sorti de Seam Booking :p
Et sinon tu réagis pas à ça :
« Perso je décourage l’héritage avec JPA. FBI = Fausse bonne idée. »
Ca veut dire que tu cautionnes? 😀
@Bernard
Dans Seam, la grande difference c’est que le code empêche les injections SQL. On connait notre métier 🙂 Tu remarqueras que les parametres sont injectés et non concaténés et donc on a une couche qui les transforment en paramètres JPA.
Sinon l’héritage et JPA est un sujet compliqué mais qui permet de mapper naturellement un certain nombre de pattern classiques en design de schema de base de donnnées. C’est comme tout, si on connait pas, mieux vaut pas toucher. Mais ca vaut le coup de passer du temps dessus à mon avis. Surtout quand on mappe des applications au modèles compliqués.
Et bam!
@Emmanuel
Ha ok, merci pour le détail sur la ruse au niveau de l’injection.
Pour l’héritage, j’ai quand même du mal à voir comment tu peux faire sans pour modéliser le domaine d’une application d’entreprise, aussi petite soit-elle…
Merci pour cette excellente présentation.
Je ne connais pas tous les frameworks évoqués, je me cantonnerai donc à des questions sur des bonnes pratiques purement « Java ».
– Play! semble utiliser énormément l’héritage technique (sur le modèle, sur les controllers …). Ces dépendances sont particulièrement difficiles à casser avec le temps et tolèrent très mal la montée en complexité (je ne parle pas ici d’héritages entre entités métiers).
– Play! utilise des appels statiques sur les classes du modèle : Hotel.all().fetch(). Comment peut-on tester unitairement avec ce genre de constructions ?
J’avoue être clairement influencé par les apports de Spring sur ces sujets et donc pas très objectif 🙂 Dépendance au code technique réduite le plus possible, peu de statiques … Je suis donc étonné de retrouver ce genre de construction dans un framework « post-spring ».
La concision et la simplicité du code sont indéniables, mais avez-vous une idée de sa capacité à vieillir correctement ?
@florent merci pour l’info! après reste à savoir si un référencement d’un site au contenu charger par de l’ajax est aussi performant qu’un site classique. (Mais ce n’est pas le sujet du débat lancé par nicolas)
@Alex: l’héritage sur la classe du Modèle est optionnel (contrairement à mes premiers commentairs). Seul le Controleur fait appel à l’héritage. Mais ce n’est pas un souci, son rôle devant être cantonné à passer de la vue au modele et vice-versa.
Les méthodes statiques ne posent pas de problèmes, Play! vient avec une intégration intelligente de JUnit. Essayez de lancer Play! avec la commande « play test » puis ouvrez la page http://localhost:9000/@tests, vous aurez une surprise !
Je viens de répondre à ton article sur mon blog: http://www.responcia.fr/blog/2010/05/17/le-touilleur-express-se-trompe-completement-sur-spring/
+1 pour Julien, tu compares des choux avec des chaussures.
On peut comprendre que tu sois séduit par Play! De là à en perdre tout sens critique, c’est inquiétant …
@Jean-Baptiste Dusseaut « Restlet 2.0 est vraiment d’une simplicité enfantine » => après avoir testé Restlet 2.0 et Jersey, j’ai beaucoup plus accroché avec le second.
Le premier a l’air sympa au premier abord, mais souffre d’un gros manque de documentation (faut dire aussi, la 2.0 est pas encore stable). Restlet propose de nombreuses manières de faire la même chose, et ça peut-être un inconvénient : plus de codes à maintenir (=> plus de bugs), plus de complexité, plus de doc à écrire.
Par ailleurs, il ne faut pas oublier que Restlet implémente JAX-RS (entre autre) 🙂 .
Jersey/Jax-RS de base est très simple à mettre en place et à utiliser. Les choses se compliquent quand on veut utiliser des filtres, il n’y a pas de mécanisme standard prévu par la norme.
@Nicolas Martignole : la rançon du succès, c’est de ne plus pouvoir écrire un article technique sans se faire corriger par plein de gens qui éventuellement se tirent dans les pattes. L’avantage, c’est que les commentaires sont parfois aussi passionnants que l’article.
@Julien Dubois + @All : peut-etre y’a-t-il un problème si meme une personne comme Nicolas, qui suit qd meme de pret les choses, passe « a coté » de Spring Webflow comme tu le dis.
En généralisant un peu, à mon sens, le pb vient au moins en partie du fait que depuis qq années on est submergé de frameworks open source qui se recouvrent fonctionellement trés souvent. De plus, ces éditeurs open qui prétendent tous révolutionner le monde ont un dont certains pour nous pondre des desription de produits pour le moins peu modestes et surtout pas franchement pragmatiques. Et meme qd on se « restreind » à un éditeur, déméller le spaghetti relève souvent de l’exploit (cf VMWare avec Grails / Roo / Spring MVC / Webflow etc etc…)
C’est bien d’avoir le choix, mais trop de choix peut des fois faire pire que mieux.
Merci pour cet article.
Mais je pense vraiment qu’il faudrait aussi comparer avec Tapestry.
Je pense que ça allie le meilleur de chacun de ces frameworks :
– convention de nommage (page-classe)
– possibilité d’annoté @Property pour les attribut d’une Pojo du modèle pour s’affranchir des getter/setter
– pages de présentation en html
– existance de plusieurs composants (facile à créés en plus) beaneditform par exemple pour créer facilement un formulaire
– chargement à chaud lors du dev
– avec Spring, Spring-Security, JPA, Hibernate, GWT… http://wiki.apache.org/tapestry/Tapestry5HowTos
Je ne crois pas que j’aurais le temps de faire l’appli test rapidement. Si il y a un volontaire… Sinon faudra être patient 😉
En tout cas, j’approuve la nécessaire prise de conscience que doivent avoir les développeurs en ce qui concernent le Web. Mais je sens que ça bouge… 😉
Quelques précisions sur Restlet suite aux derniers commentaires. La version 1.0 a été conçue en 2005, et à l’époque c’était le premier framework conçu à partir de REST. Depuis il a beaucoup évolué avec version 2.0.0 finale toute proche (2.0 RC4 dispo).
Cette version 2.0 apporte beaucoup d’innovations (API de sécurité intégrée, utilisation parcimonieuse des annotations, 5 éditions cohérentes pour Java SE/EE, GAE, GWT et Android, conversion automatique entre beans Java et documents XML/JSON, etc.).
Le tout fonctionne côté client et serveur (services web, client web, sites web statiques ou dynamiques, proxy web sont faciles à concevoir) avec la même API et un grand respect des standards clés autour de REST (HTTP, URI). Par exemple, on couvre , très simplement. De nombreuses extensions sont disponibles pour supporter d’autres standards (JAAS, Atom, RDF, OData, etc.) et s’intégrer avec les technos les plus courantes (Spring, Servlet, FreeMarker, Jetty, etc.).
La documentation s’est beaucoup étofée avec un user guide en constante amélioration et un livre Restlet in Action en early access chez Manning. On va continuer a travailler sur ce point, je suis d’accord on peut faire mieux encore.
Je ne connais pas assez bien Play! pour en faire une analyse détaillée, mais le côté « haute productivité » me semble très bien pensé et communiqué. Je suis moins impressionné par les fondations (API) qui ne semblent que vaguement inspirées par REST. Un élément révelateur est le besoin de faire apparaître le paradigme MVC alors que REST n’en a pas besoin.
Arretons d’adapter les anciens paradigmes (RPC, MVC) au web alors qu’ils n’ont pas été conçus pour cela et utilisons simplement mais pleinement REST: ça suffit!
Nicolas, je suis prêt à t’aider sur une version Restlet de cette application de démo si tu es intéressé. A+