jeudi 10 février 2011

MVP4G Spring et GWT

Parmis les plateformes de développement de clients riches (chaînon manquant entre client lourd et client léger), 2 sont en forte concurrence : Google Web Toolkit(alias GWT) et Adobe Flex.

Le but ici n'est pas de traiter qui est le meilleur, mais de faire un focus sur la mise en oeuvre de GWT dans le cadre d'une application métier complexe et critique. Dans ce sens, la grande quantité de composants dans l'application ainsi qu'une maintenance évolutive constante ont conduit à utiliser un framework permettant maîtriser les efforts de maintenance. Ce choix c'est porté sur le framework MVP4G.

Voici comment développer une application GWT reposant sur ce framework.

Instant zéro : les outils


Les outils utilisés dans le cadre de cette démarche sont:

Maven 3

Le couteau suisse par excellence

Eclipse Helios

Un des plus puissants environnements de développement gratuits

GWT

Le kit de développement GWT ainsi que le plugin pour Eclipse


Je ne détaillerai pas ici l'installation de ces outils, on se rendra sur les sites dédiés pour en savoir plus.

The Bigbang Theory


Boom!


Maintenant que les outils sont prêts, l'heure est venue de créer notre projet. Nous utiliserons maven qui dispose d'un plugin permettant de gérer les projets GWT.

> mvn archetype:generate

Cette commande vous propose de choisir le modèle de projet à générer parmi la pléthore d’archétypes disponibles. Il faut chercher dans cette liste le type de projet "gwt-maven-plugin" qui porte le numéro 235 lors de l'écriture de ces lignes. On choisira par la suite le numéro de version à utiliser (la dernière par défaut) puis l'assistant va demander quelques informations sur le projet : groupId, artifactId, version, ... et le nom du module GWT. Ce dernier donnera une classe <NOM_DU_MODULE>.java héritant de EntryPoint puis un ficher <NOM_DU_MODULE>.gwt.xml.

Premier run dans Eclipse


Nous venons de mettre au monde notre projet en quelques secondes, maintenant il convient de lui faire faire ses premiers pas. Pour cela on se rendra dans le dossier construit par maven que nous noterons par la suite <artifactId>.

> cd <artifactId>
> mvn eclipse:eclipse

On importera le projet dans Eclipse via Import ... / Existing project.
Il est possible que le fichier pom.xml ne soit pas tout à fait propre, j'ai entre-autre purifié son contenu pour prendre en compte la version Java 6 des sources en lieu et place de Java 5, puis customisé un peu la configuration du plugin maven-eclipse-plugin avant de relancer la commande précédente pour obtenir un projet "propre".
Pour lancer le projet dans Eclipse, un simple clic droit Run as ... / Web application et voila notre projet qui tourne. Il s'agit du projet de base créé également lorsque l'on utilise l'assistant Eclipse, le site officiel GWT en dit plus sur ce projet dans le guide du débutant.

Un peu de Spring


Le projet met en oeuvre un service GWT RPC afin d'illustrer ce fonctionnement. Ces services RPC sont des servlets à déclarer dans le ficher web.xml. Dans le cadre d'une application à grande échelle, une grande quantité de services vont être développés afin de convenir aux spécifications du métier. Dans ce sens il convient d'améliorer la productivité sur le développement et la mise en oeuvre de ces services. Le plus simple étant de laisser le framework Spring instancier ces services et l'on disposera alors d'une seule et unique servlet dispatchant les appels RPC au beans construits pour assurer l'implémentation de ces services.

"Dit à Maven de dire à Eclipse ..."


Première étape, on modifie le fichier pom.xml pour ajouter la dépendance vers les bibliothèques nécessaires:

<project>
...
<properties>
<springVersion>2.5.6.SEC02</springVersion>
<properties>
...
<dependencies>
...
<!-- Spring 4 GWT -->
<dependency>
<groupId>com.google.code</groupId>
<artifactId>spring4gwt</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${springVersion}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
...
</dependencies>
...
</project>

On met à jour le projet via la commande magique pour ajouter automatiquement les librairies indiquées dans Eclipse:

> mvn eclipse:eclipse

... puis Refresh ou F5 dans Eclipse et c'est parti!

Refactor


Notre projet n'a pas beaucoup changé, il est juste plus gros car embarque désormais plusieurs JAR supplémentaires.

applicationContext.xml


On ajoutera un fichier de description des beans Spring en prenant soin de faire appel à une fonction pratique : le scan automatique de composants. Ce fichier sera déposé dans le dossier src/main/resources du projet:

<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:component-scan
base-package="PAQUETAGE_DE_BASE_DE_MON_PROJET.server" />
</beans>

web.xml


Nous allons mettre à jour le descripteur de l'application afin de remplacer la Servlet existante par une Servlet un peu plus dynamique. Supprimer les lignes suivantes du fichier web.xml se trouvant dans src/main/webapp/WEB-INF/

<web-app>
...

<!-- Servlets -->
<servlet>
<servlet-name>greetServlet</servlet-name>
<servlet-class>PAQUETAGE_DE_BASE_DE_MON_PROJET.server.GreetingServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>greetServlet</servlet-name>
<url-pattern>/NOM_DE_MON_MODULE_GWT/greet</url-pattern>
</servlet-mapping>

...
</webapp>

... puis nous ajouterons les lignes suivantes:

<webapp>
...
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- SpringGwt remote service servlet -->
<servlet>
<servlet-name>springGwtRemoteServiceServlet</servlet-name>
<servlet-class>org.spring4gwt.server.SpringGwtRemoteServiceServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>springGwtRemoteServiceServlet</servlet-name>
<url-pattern>/NOM_DE_MON_MODULE_GWT/services/*</url-pattern>
</servlet-mapping>

<!-- Spring servlet filter -->
<filter>
<filter-name>springRequestFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>springRequestFilter</filter-name>
<servlet-name>springGwtRemoteServiceServlet</servlet-name>
</filter-mapping>
...
</webapp>

"Sinon le refactor c'est quand ?"


Nous allons mettre à jour le code du service en commençant par son interface. L'annotation permettant à GWT de connaître une partie de l'URL de ce dernier doit être mise à jour. Dans le fichier GreetingService.java l'annotation prendra la valeur suivante:

@RemoteServiceRelativePath("services/greet")

Le changement majeur sera pour l'implémentation du service. Jusqu'à présent, la classe GreetingServiceImpl héritait de RemoteServiceServlet, une classe propre à GWT. Nous allons briser cet héritage transformant ainsi la classe en un java bean classique.

...
@Service("greet")
public class GreetingServiceImpl
implements GreetingService {
...
}

Notre classe ne compile plus car elle faisait appel à des méthodes de RemoteServiceServlet afin d'accéder à des propriétés de la requête. Voici le nouveau code que nous allons mettre en place pour palier cette lacune:

...
HttpServletRequest request = ((ServletRequestAttributes)(RequestContextHolder.getRequestAttributes())).getRequest();
String serverInfo = request.getSession().getServletContext().getServerInfo();
String userAgent = request.getHeader("User-Agent");
...

Le refactor est maintenant terminé, on relance le serveur et on constate que l'application fonctionne comme avant. Dingue!

MVP4G


Nous rentrons maintenant dans le vif du sujet, nous pouvons ajouter autant de services que nous voulons sans trop d'efforts, faisons de même pour l'écran.

"Dit aussi à Maven de dire à Eclipse ..."


Nous ajoutons maintenant la dépendances nécessaire à l'utilisation du framework MVP4G dans le fichier pom.xml:

<project>
...
<dependencies>
...
<!-- MVP design pattern 4 GWT -->
<dependency>
<groupId>com.googlecode.mvp4g</groupId>
<artifactId>mvp4g</artifactId>
<version>1.2.0</version>
</dependency>
...
</dependencies>
...
</project>

... puis la commande magique (voir plus haut), maintenant nous sommes prêts.

Definition du module GWT


Nous ajouterons un lien vers le module GWT MVP4G via le descripteur de notre module GWT NOM_DE_NOM_MODULE_GWT.gwt.xml:

<module>
...
<!-- Inherit the Mvp4gModule stuff. -->
<inherits name='com.mvp4g.Mvp4gModule' />
...
</module>

Le présentateur


Pierre angulaire du pattern MVP, nous allons produire un présentateur reproduisant le comportement actuel de notre application :

/**
* Presenter de salutation.
*/
@Presenter(view = GreetView.class)
public class GreetPresenter
extends LazyPresenter<IGreetView, RootEventBus>
{
/**
* Specification d'une vue servant a la salutation.
*/
public static interface IGreetView
extends LazyView
{
/** Composant permettant la saisie du nom. */
HasValue<String> getUsername();

/** Indication d'une erreur éventuelle. */
HasText getErrorLabel();

/** Bouton soumettre. */
HasClickHandlers getButtonSend();

/** Désactive le bouton de soumission. */
void disableButtonSend();

/** Active le bouton de soumission. */
void enableButtonSend();
}

@Override
public void createPresenter() {
}

@Override
public void bindView() {
view.getButtonSend().addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
final String username = view.getUsername().getValue();
view.getErrorLabel().setText("");
if (!FieldVerifier.isValidName(username)) {
view.getErrorLabel().setText("Please enter at least four characters");
} else {
view.disableButtonSend();
greetService.greetServer(username, new AsyncCallback<String>() {
@Override
public void onSuccess(String result) {
greetSucceed(username, result);
}

@Override
public void onFailure(Throwable caught) {
greetFailed(username, caught);
}
});
}
}
});
}

/**
* @see RootEventBus#start()
*/
public void onStart() {
// Nothing special to do here.
}

protected void greetSucceed(String username, String result) {
final Label textToServerLabel = new Label(username);
final HTML serverResponseLabel = new HTML(result);
VerticalPanel widget = new VerticalPanel();
widget.addStyleName("dialogVPanel");
widget.add(new HTML("<b>Sending name to the server:</b>"));
widget.add(textToServerLabel);
widget.add(new HTML("<br><b>Server replies:</b>"));
widget.add(serverResponseLabel);
widget.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
showPopup("Remote Procedure Call", widget);
}

protected void greetFailed(String username, Throwable caught) {
final Label textToServerLabel = new Label(username);
final HTML serverResponseLabel = new HTML("An error occurred while "
"attempting to contact the server. Please check your network " "connection and try again.");
serverResponseLabel.addStyleName("serverResponseLabelError");
VerticalPanel widget = new VerticalPanel();
widget.addStyleName("dialogVPanel");
widget.add(new HTML("<b>Sending name to the server:</b>"));
widget.add(textToServerLabel);
widget.add(new HTML("<br><b>Server replies:</b>"));
widget.add(serverResponseLabel);
widget.setHorizontalAlignment(VerticalPanel.ALIGN_RIGHT);
showPopup("Remote Procedure Call - Failure", widget);
}

private void showPopup(String title, Widget widget) {
final DialogBox popup = new DialogBox();
popup.setText(title);
VerticalPanel container = new VerticalPanel();
container.add(widget);
Button closeButton = new Button("Close");
closeButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
popup.hide();
view.enableButtonSend();
}
});
container.add(closeButton);
popup.add(container);
popup.center();
}

@Inject
private GreetingServiceAsync greetService;
}

La vue


Notre code ne compile pas pour le moment car il nous manque le complice du présentateur : la vue. Nous allons maintenant lui donner vie.

/**
* Sample Greet view.
*/
public class GreetView
extends Composite
implements IGreetView
{
@Override
public HasValue<String> getUsername() {
return username;
}

@Override
public HasText getErrorLabel() {
return errorLabel;
}

@Override
public HasClickHandlers getButtonSend() {
return buttonSend;
}

@Override
public void disableButtonSend() {
buttonSend.setEnabled(false);
}

@Override
public void enableButtonSend() {
buttonSend.setEnabled(true);
}

/**
* @see com.mvp4g.client.view.LazyView#createView()
*/
@Override
public void createView() {
DockLayoutPanel container = new DockLayoutPanel(Unit.PX);
Widget title = new HTML("<h1>Web Application Starter Project</h1>");
container.addNorth(title, 140);
FlowPanel content = new FlowPanel();
username = new TextBox();
content.add(username);
buttonSend = new Button(messages.sendButton());
content.add(buttonSend);
errorLabel = new Label();
errorLabel.addStyleName("serverResponseLabelError");
content.add(errorLabel);
content.addStyleName("content");
container.add(content);
this.initWidget(container);
this.addStyleName("greetView");
}

private TextBox username;

private Label errorLabel;

private Button buttonSend;

@Inject
private Messages messages;
}

Le car ... non le bus!


La colonne vertébrale de notre application est un bus d’évènements permettant de connecter entre eux l'ensemble des composants de notre application à la manière d'une colonne vertébrale.

/**
* Bus d'événement global à l'ensemble de l'application.
*/
@Events(startView = GreetView.class)
public interface RootEventBus
extends EventBus
{
/**
* Premier événnement lancé au démarrage de l'application.
*/
@Start
@Event(handlers=GreetPresenter.class)
void start();
}

"Et maintenant on refactor ?"


Le point d'entrée d'une application est la classe qui étend EntryPoint, celle automatiquement générée lors de la création du projet concentre l'ensemble du comportement de l'application, ce que nous avons distribué dans le présentateur et la vue. Nous pouvons donc supprimer l'ensemble du contenu initial pour mettre le code suivant :

/**
* Point d'entrée du module GWT.
*/
public class NomDeMonModule
implements EntryPoint
{
/**
* This is the entry point method.
*/
public void onModuleLoad() {
// Instanciation du module racine MVP4G.
Mvp4gModule module = (Mvp4gModule) GWT.create(Mvp4gModule.class);
module.createAndStartModule();
// La vue du module principal est positionné dans la page.
RootLayoutPanel.get().add((Widget)module.getStartView());
}
}

Cette classe ne bougera plus à l'avenir. Avant de lancer l'application et constater les changements, il conviendra de nettoyer le fichier HTML en supprimant le contenu qui n'est plus utile, c'est à dire tout depuis le tag H1 jusqu'à BODY.

Bazinga!


Nous venons de migrer une simple application GWT vers un socle technique lui permettant d'accueillir à l'avenir une grande quantité de services RPC ainsi qu'une interface riche, complexe et souple.

1 commentaire:

  1. Bonjour
    Présentation intéressante ;-)
    T'aurais une archive du code source qui va avec?
    Histoire de voir comment tu as organisé ça un peu en package pour la partie mvp4j entre autre
    A+

    RépondreSupprimer