samedi 28 novembre 2009

GWT et Spring

Je n'ai jamais pu utiliser GWT dans un cadre professionnel, mais le peu de fois que j'ai pu le tester à titre personnel, j'en ai été pleinement satisfait. Je dois vous l'avouer, je n'aime pas flash et Silverlight, et ce, en tant qu'utilisateur. Les sites en Flash sont souvent lourds, lents, et gourmands en mémoire et en CPU. Alors certes sans ces plugins, la vidéo et la musique sur le net n'aurait pas l'essor que l'on connait. Mais j'avoue avoir une nette préférence pour les sites utilisant ces plugins avec parcimonie. Quoiqu'en dise les benchmarks, l'ajax donne une impression de légèreté que je peine a retrouver avec le flash et silverlight. Malgré tout je dois reconnaître que programmer en Javascript est vraiment chiant, je trouve ça nocif pour la productivité. Il faut jouer avec le respects des standards et les performances des différents navigateurs. Alors certes, il y a pleins de librairies permettant de faire de jolis sites en ajax et qui sont compatibles avec la plupart des navigateurs (même Internet Explorer 6!!), mais pour les utiliser il faut quand même coder en Javascript pour les utiliser. Je ne sais pas comment vous (chers lecteurs) faites vos sites web, mais moi je n'utilise le Javascript uniquement pour la vu, le reste est fait en Java ou en .Net. Du coup ça m'oblige a écrire du code Javascript dans une String Java ou au sein d'une page JSP...  Il faut aussi formater les données pour les envoyer du serveur au clients, et si vous avez quelques notions d' ajax vous savez que ça se fait en XML. Il faut donc depuis le code Java générer du XML, ça implique de passer du temps pour bien former son code XML (qui est bien souvent présent dans le code Java sous forme de String...) afin que la partie cliente en Javascript puisse correctement l'interpréter. Le typage faible de Javascript n'arrange en rien les choses. J'ai pu travailler sur des sites utilisant,  Javascript,  Silverlight et Flex. Et si l'internaute que je suis n'est pas fada de Flash et Silverlight, le programmeur que je suis les préfèrent largement au Javascript. Ne serait-ce que pour s'épargner le formatage des données nécessaires à la communication du  serveur vers le client. Dans le cas de Flex c'est entre autre BlazeDS qui nous facilite les communications, il nous permet d'appeler des méthodes Java depuis du code Flex comme si il s'agissait que code ActionScript. Problème, lorsque le passe des objets en paramètres a ces méthodes ou que l'on veut récupérer les objets renvoyés par la méthode, on doit les déclarer en ActionScript presque comme s'il il s'agissait d'objets ActionScript à part entière. Certes ce n'est ni trop long ni trop compliqué, mais on se passerait bien de le faire. De plus, je n'ai pas retrouvé d'environnement de développement intégré offrant les même fonctionnalités aux développeurs ActionScript (et Flex de manière générale) qu' Eclipse pour les développeurs Java. Non, FlexBuilder ne m'a pas convaincu.

Je dois avouer GWT a quelques avantages qui font défauts aux outils précédemment cités. Tout d'abord le code généré est en Xhtml et Javascript, donc on s'épargne la lourdeur relative de Flash et Silverlight. D'un autre coté, lorsqu'on utilise GWT on code en Java, cela permet donc d'utiliser son EDI Java habituel (au moins Eclipse et Netbeans) et de jouir de la productivité qui en découle.
S'agirait-il du cadriciel parfait pour l' IHM ? Pas tout a fait, pour chaque service, 2 deux interfaces et une classe doivent êtres implémentés, le service doit en plus être référencé dans le fichier « web.xml ». Alors certes, si vous utilisez Eclipse, l'interface asynchrone est généré à partir de l'autre interface, mais ils vous devez quand même référencer votre service dans le « web.xml » qui va vite devenir volumineux. De plus si GWT résout pas mal de problème au niveau de l' IHM il ne fait rien pour la persistance de données, rien pour implémenter le motif MVC, et il me semble qui ne fait pas grand chose non plus pour gérer les transactions. Bref, une application web écrite en Java ne peut se contenter de GWT elle devra en plus disposer d'un ORM comme EclipseLink ou Hibernate et d'un cadriciel comme Spring. Si la documentation de Seam nous montre qu'il peut interagir avec GWT, il n'en est rien pour Spring, il va donc falloir faire du bidouillage pour faire fonctionner les deux de concert. Certaines personnes l'ont déjà fait, et ça fonctionne pas trop mal :


L'inconvénient de ces deux approches (très similaires...) et qu'elles s'appuient sur une configuration des services dans les fichiers XML de Spring. Alors certes nos classes qui implémentent les services ne sont plus obligées d'hériter de la classe RemoteServlet, on peut donc câbler GWT a du code existant, mais bon, le fait de passer par des fichiers XML nuit à la productivité et risque de provoquer des conflits au sein des gestionnaires de configuration (en effet, un fichier est édité par plusieurs personnes a de grande chance de causer des conflits). C' est d'autant plus dommage de passer par des fichiers XML pour configurer les dépendances entre les différents beans qu'il existe une alternative crédible : les annotations.

La solution que je vous propose repose sur le même principe que les deux précédentes, Spring va intercepter toutes les requêtes du client vers le serveur. Ceci va nous éviter de déclarer chaque service dans le " web.xml ". Notre fichier " spring-servlet.context.xml " qui contient toute notre configuration Spring. Il est très bref, il contient la déclaration de deux beans du cadriciel Spring, une balise lui disant que nous allons utiliser les annotations, et une autre lui disant dans quel package il va trouver les classes annotées. En utilisant Spring ainsi, le fichier " spring-servlet.context.xml " ne devrait pas changer au cours d'un projet, ou alors très rarement.
Nous allons nous aussi utiliser la pièce maitresse des deux méthodes d'intégration avec Spring précédemment citées : la classe "GwtRpcController". Cette classe change un peu, elle devient abstraite et utilise un accesseur abstrait pour accéder au " RemoteService ". Il n'y a pas d'accesseur pour accéder a la classe de RemoteService, parce que c'est tout simplement inutile, un simple "getClass() " fait la même chose. Cette classe ne pouvant être instancié elle n'est jamais utilisée directement par Spring. Pour chaque service nous allons créer des contrôleurs Spring héritant de la classe "GwtRpcController ". Le but des contrôleurs dans notre application est d'associer une URL à une classe service (implémentant l'interface " RemoteService "). Chaque contrôleur utilisera donc l'annotation "@ RequestMapping " pour s'associer à une URL et se fera injecter une instance de la classe de service par l'annotation " @Ressource ", c'est cette même instance qui sera retournée par l'accesseur " getRemoteService ()". Vous devinez déjà les obligations auxquelles doivent se soumettre nos classes de services, elle doivent bien évidement implémenter l'interface RemoteService (ce qui n'est pas vraiment une contrainte) et elle doit être annotée afin d'être un bean Spring.Je crois que cette dernière obligation est de loin la plus restrictive... Dans cette solution j'ai voulu faire en sorte à ce que l'on ai besoin d'écrire le moins de code XML possible, mais j'admets que ce que l'on écrit pas en XML au final, on l'écrit en Java. Finalement l'intégration de Gwt avec Spring est tellement simple que l'on se demande à quoi servent les cadriciels qui prétendent le faire à notre place.

QuoteService.java :

package com.example.integration.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;


@RemoteServiceRelativePath("quote.rpc")
public interface QuoteService extends RemoteService {
       public String getQuote();
}


QuoteServiceAsync.java :

package com.example.integration.client;

import com.google.gwt.user.client.rpc.AsyncCallback;


public interface QuoteServiceAsync {


     void getQuote(AsyncCallback callback);


}



Integration.java :

package com.example.integration.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;




public class Integration implements EntryPoint {


     private static final String SERVER_ERROR = "An error occurred while "
              + "attempting to contact the server. Please check your network "
              + "connection and try again.";
     private final GreetingServiceAsync greetingService = GWT
              .create(GreetingService.class);


     public void onModuleLoad() {
         final Label quoteText = new Label();

        Timer timer = new Timer() {


            public void run() {
                // create an async callback to handle the result:
                AsyncCallback callback = new AsyncCallback() {

                    public void onFailure(Throwable t) {
                        // display error text if we can't get the quote:
                        quoteText.setText("Failed to get a quote");
                    }

                    public void onSuccess(String result) {
                        // display the retrieved quote in the label:
                        quoteText.setText(result);
                    }
                };
                QuoteServiceAsync service = (QuoteServiceAsync) GWT.create(QuoteService.class);
                service.getQuote(callback);
            }
        };

        timer.scheduleRepeating(3000);
        RootPanel.get().add(quoteText);
         }

     }


Integration.gwt.xml :

<xml version="1.0" encoding="UTF-8"?>
<>DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 1.7.1//EN" "http://google-web-toolkit.googlecode.com/svn/tags/1.7.1/distro-source/core/src/gwt-module.dtd">
<module rename-to='integration'>

 
  <inherits name='com.google.gwt.user.User'/>

 
  
 
  <inherits name='com.google.gwt.user.theme.standard.Standard'/>
 
 

 

 
  <entry-point class='com.example.integration.client.Integration'/>
</module>


GwtRpcController.java :

package com.example.integration.server;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;


public abstract class GwtRpcController extends RemoteServiceServlet implements
         Controller, ServletContextAware {


     private ServletContext servletContext;


     @RequestMapping(method = RequestMethod.POST)
     public ModelAndView handleRequest(HttpServletRequest request,
              HttpServletResponse response) throws Exception {
         super.doPost(request, response);
         return null;
     }


     @Override
     public String processCall(String payload) throws SerializationException {
         try {

              RPCRequest rpcRequest = RPC.decodeRequest(payload,
                        getRemoteService().getClass());

              // delegate work to the spring injected service
              return RPC.invokeAndEncodeResponse(getRemoteService(), rpcRequest
                        .getMethod(), rpcRequest.getParameters());
         } catch (IncompatibleRemoteServiceException ex) {
              getServletContext()
                        .log(
                                 "An IncompatibleRemoteServiceException was thrown while processing this call.",
                                 ex);
              return RPC.encodeResponseForFailure(null, ex);
         }
     }


     @Override
     public ServletContext getServletContext() {
         return servletContext;
     }


     public void setServletContext(ServletContext servletContext) {
         this.servletContext = servletContext;
     }


     public abstract RemoteService getRemoteService();


}



QuoteController.java :

package com.example.integration.server;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.google.gwt.user.client.rpc.RemoteService;


@Controller
@RequestMapping("/integration/quote.rpc")
public class QuoteController extends GwtRpcController {
    
     @Resource
    private RemoteService quoteService;


     @Override
     public RemoteService getRemoteService() {
         return quoteService;
     }
}




QuoteServiceImpl.java :


package com.example.integration.server;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.springframework.stereotype.Service;

import com.example.integration.client.QuoteService;


@Service("quoteService")
public class QuoteServiceImpl implements QuoteService {
     private Random randomizer = new Random();
     private static List quotes = new ArrayList();


     static {
          quotes.add("No great thing is created suddenly - Epictetus");
         quotes.add("Well done is better than well said - Ben Franklin");
         quotes.add("No wind favors he who has no destined port - Montaigne");
         quotes.add("Sometimes even to live is an act of courage - Seneca");
         quotes.add("Know thyself - Socrates");
     }

     public String getQuote() {
         return quotes.get(randomizer.nextInt(quotes.size()));
     }
}



Web.xml:

<xml version="1.0" encoding="UTF-8"?>
<>DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  
    <servlet>
        <servlet-name>springservlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
        <load-on-startup>1load-on-startup>
    </servlet>

   
    <servlet-mapping>
        <servlet-name>springservlet-name>
        <url-pattern>*.rpcurl-pattern>
    </servlet-mapping>
 
  <welcome-file-list>
    <welcome-file>Integration.htmlwelcome-file>
  </welcome-file-list>
</web-app>





spring-servlet.xml:

<xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    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">

   

<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>

    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

<context:component-scan base-package="com"/>


 <context:annotation-config/>
</beans>


pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0modelVersion>
  <groupId>IntegrationgroupId>
  <artifactId>IntegrationartifactId>
  <packaging>warpackaging>
  <version>0.0.1-SNAPSHOTversion>
  <dependencies>
    <dependency>
      <groupId>org.springframeworkgroupId>
      <artifactId>spring-webmvcartifactId>
      <version>2.5.6.SEC01version>
    <dependency>
  <dependencies>
<project>


Integration.html :

<>DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">

   
   
   
    <link type="text/css" rel="stylesheet" href="Integration.css">

   
   
   
    <title>Web Application Starter Projecttitle>
   
   
   
      
    <script type="text/javascript" language="javascript" src="integration/integration.nocache.js">script>
  <head>

 
 
 
 
 
  <body>

   
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0">iframe>

    <h1>Web Application Starter Projecth1>

    <table align="center">
      <tr>
        <td colspan="2" style="font-weight:bold;">Please enter your name:td>       
      <tr>
      <tr>
        <td id="nameFieldContainer">td>
        <td id="sendButtonContainer">td>
      <tr>
   < table>
  <body>
<html>