GWT Tips – 2/2 – JNDI, DI avec Spring et Dé/Sérialisation

Après avoir configuré un projet GWT avec Maven, le mode debug dans Eclipse, ainsi que défini une journalisation serveur à partir de messages clients, nous allons aborder trois nouveaux sujets : la configuration d’une datasource par JNDI, l’injection de dépendances Spring dans une servlet GWT, et enfin la sérialisation/désérialisation dans la partie cliente.

L’ensemble des sources sont disponibles via GitHub, chaque partie ayant sa propre branche contenant le code des parties précédentes.

Datasource et JNDI

[ Code Source (tar.gz) ] [ Code Source (zip) ]

Généralement, les informations de connexion à une base de données, ainsi que le « connection pooling » sont configurés par JNDI. Or, en phase de développement, si vous utilisez le plugin GWT pour maven, vous n’avez pas besoin d’avoir un serveur, ou plus précisément d’un serveur externe.

En « Development Mode », GWT utilise Jetty 6. Malheureusement, il est légèrement plus difficile de configurer un JNDI que pour un Jetty externe.

Commençons par le fichier web.xml, qui n’a rien de particulier :

<!-- Extrait du fichier web.xml -->
<resource-ref>
  <description>Database connection</description>
  <res-ref-name>jdbc/gwttips</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>

Il nous faut ensuite créer un fichier jetty-web.xml qui contiendra les informations de connexion :

<!-- Fichier jetty-web.xml -->
<Configure id="gwttips">
  <New id="datasourceGwtTips">
    <Arg></Arg>
    <Arg>java:comp/env/jdbc/gwttips</Arg>
    <Arg>
      <New>
        <Set name="Url">jdbc:h2:[chemin]gwt-tips/.h2/gwt-tips;MVCC=TRUE</Set>
        <Set name="Username">gwt-tips</Set>
        <Set name="Password">gwt-tips</Set>
      </New>
    </Arg>
  </New>
</Configure>

Note : [chemin] est le chemin menant au projet “gwt-tips“.

Pour pouvoir utiliser cette configuration, il nous faut trois nouvelles dépendances :

<!-- Extrait du pom.xml -->
<dependency>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>jetty-naming</artifactId>
  <version>6.1.26</version>
</dependency>
<dependency>
  <groupId>org.mortbay.jetty</groupId>
  <artifactId>jetty-plus</artifactId>
  <version>6.1.26</version>
</dependency>
<dependency>
  <groupId>commons-dbcp</groupId>
  <artifactId>commons-dbcp</artifactId>
  <version>1.4</version>
</dependency>

L’utilisation de commons-dbcp n’est pas une obligation. Je vous invite à consulter la documentation de Jetty sur la configuration des datasources [1].

Pour qu’une relation soit établie entre tous ces éléments, il est nécessaire d’ajouter un paramètre au lancement de la JVM :

<!-- Extrait du fichier pom.xml -->
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>gwt-maven-plugin</artifactId>
  <version>2.5.0</version>
  <configuration>
    <runTarget>gwttips.html</runTarget>
    <extraJvmArgs>-Djava.naming.factory.initial=org.mortbay.naming.InitialContextFactory</extraJvmArgs>
  </configuration>
</plugin>

Les valeurs dans le tag <extraJvmArgs> doivent être obligatoirement sur une seule ligne.

Pour finir, nous allons exclure les jar et le fichier jetty-web.xml du WAR généré, puisqu’ils nous sont utiles qu’en Development Mode.

<!-- Extrait du fichier pom.xml -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-war-plugin</artifactId>
  <version>2.3</version>
  <configuration>
    <packagingExcludes>WEB-INF/lib/jetty-*-6.1.26.jar,WEB-INF/jetty-web.xml</packagingExcludes>
    <webxml>src/main/webapp/WEB-INF/web.xml</webxml>
  </configuration>
</plugin>

Note sur le code : Sur tous mes projets, j’ai l’habitude d’utiliser une base de données embarquée (H2 [2]) et un plugin Maven pour le serveur d’application (inutile dans le cas de GWT). Ceci a un avantage non négligeable. L’installation d’un poste de développement est extrêmement rapide. Après avoir installé un client de gestionnaire de sources (Svn, Git, Hg, etc.), récupéré les sources et configuré Maven, il suffit de lancer 2 ou 3 commandes et votre application est prête à être utilisée.

Dans le cas présent, je part du principe que vous avez déjà Maven configuré. Une fois les sources téléchargées, il vous suffit de lancer les 3 commandes suivantes :

$ mvn sql:execute
$ mvn install
$ mvn gwt:run

mvn sql:execute initialise la base de données, mvn install compile l’application (entre autre) et mvn gwt:run lance GWT en Dev Mode.

Si vous souhaitez vérifier que la base de donnée a bien été créée, il faut vérifier qu’il existe un répertoire .h2 dans le même répertoire que le pom.xml.

Pour vérifier que les données ont bien été insérées, vous devez télécharger H2 [2] (je vous conseille la version zippée), et exécuter le fichier “<chemin>/h2/bin/bat.[sh|bat]” correspondant à votre système d’exploitation. Une nouvelle page apparaît dans votre navigateur.

JDBC URL : jdbc:h2: <chemin>/gwt-tips/.h2/gwt-tips
User Name : gwt-tips
Password : gwt-tips

Comme vous pouvez le constater dans le pom.xml, j’ai utilisé une technique un peu différente de celle présentée dans cet article. Au lieu d’exclure les éléments dont je n’ai pas besoin dans le WAR final, je ne les ai pas inclus en tout premier lieu. Je me suis basé sur des profils Maven : un profil “dev” qui est le profil par défaut, et un profil “prod”. Le premier contient toute la configuration jetty et H2, alors que la seconde n’a rien de particulier, hormis quatre paramètres de logging.

L’injection de dépendances avec Spring

[ Code Source (tar.gz) ] [ Code Source (zip) ]

Utiliser l’injection de dépendances de Spring dans la partie cliente n’est pas possible. Pour cela, il est nécessaire d’utiliser GIN [3]. Si vous souhaitez en savoir plus sur le sujet, je vous invite à consulter le site officiel. Leur Wiki explique simplement comment l’utiliser.

Dans la partie serveur, la problématique vient du fait que les servlets ne sont pas managées par Spring. Il nous faut donc trouver une autre solution pour injecter les différents beans utilisés dans ces servlets. Pour ce faire, il suffit que toutes nos servlets, souhaitant utiliser des beans managés par Spring, héritent de la classe suivante :

/*
 * org.isk.gwttips.server.common.AutoinjectingRemoteServiceServlet
 */
public class AutoinjectingRemoteServiceServlet extends RemoteServiceServlet {
  @Override
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    final ServletContext servletContext = config.getServletContext();
    final WebApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
    final AutowireCapableBeanFactory beanFactory = ctx.getAutowireCapableBeanFactory();
    beanFactory.autowireBean(this);
  }
}

Bien que ceci règle notre souci d’injection dans une servlet lorsqu’une application est déployée, nous ne pouvons pas créer facilement des tests unitaires.

Selon moi, la solution la plus simple est de déporter la logique métier dans des services Spring. Les servlets deviennent par conséquent de simples façades.

Par exemple :

/*
 * org.isk.gwttips.server.servlet.ConnectionServlet
 */
public class ConnectionServlet extends AutoinjectingRemoteServiceServlet
 implements ConnectionService {
  @Inject
  private UserService userService;

  public UserUI connect(final String username, final String password)
    throws ClientException {
    return this.userService.connect(username, password);
  }
}

 

/*
 * org.isk.gwttips.service.UserService
 */
public interface UserService {
  UserUI connect(String login, String password) throws ClientException;
}

 

/*
 * org.isk.gwttips.service.impl.UserServiceImpl
 */
@Named("userService")
@Singleton
public class UserServiceImpl implements UserService {

  private final static Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

  @Inject
  private UserRepository userRepository;

  @Override
  public UserUI connect(String login, String password) throws ClientException {
    try {
      final User user = this.userRepository.connect(login, password);
      return new UserUI(user.getFirstName(), user.getLastName());
    } catch (UserException e) {
      LOGGER.info(e.getMessage());
      throw new ClientException(e.getMessage());
    }
  }
}

Il est à présent très simple de tester le service org.isk.gwttips.service.impl.UserServiceImpl.

Note sur le code : Pour que l’exemple soit le plus proche de la réalité, j’ai divisé le projet en trois modules Maven ( persistence-core, persistence et front). De plus, à la place de la méthode “old-school” d’accès à la base de données de l’exemple précédent, j’ai utilisé Hibernate. Et, pour les tests unitaires, j’ai rajouté une deuxième base de données H2, ce qui me permet d’avoir un jeu de tests ne rentrant pas en conflit avec les modifications effectuées en base lors de la navigation sur l’application. Cette base de tests créée dans le répertoire /persistence/target/.h2 est initialisée lors de la compilation, le goal sql:execute ayant été ajouté à la phase initialize du cycle d’éxécution de Maven.

Si l’on reprend l’exemple précédent, il est important de noter que la classe org.isk.persistence.repository.impl.UserRepositoryImpl doit être mockée. Il n’y a absolument aucune raison d’effectuer des tests sur les accès en base de données depuis le module “front”.

Pour compiler et lancer l’application :

$ mvn install sql:execute -P dev
$ cd front
$ mvn gwt:run -P dev

Sérialisation/Désérialisation côté client

La sérialisation et désérialisation côté client est utile dans plusieurs situations.

Ceci est le cas si, par exemple, nous souhaitons “bookmarker” les paramètres d’un formulaire de recherche, pouvoir sérialiser l’objet contenant les critères de recherche, puis l’encoder en Base 64, et finalement le placer dans l’URL en tant que token. Ensuite, lorsqu’une requête HTTP sera effectuée avec ce token, il sera désérialisé permettant de pré-remplir les champs du formulaire, et de lancer la requête effectuant la recherche.

Le cas le plus courant étant de sérialiser un objet en JSON pour pouvoir transférer des donnés côté serveur en se passant du framework RPC de GWT. Ceci permet, entre autre, d’utiliser des frameworks de type Spring MVC.

Malheureusement, GWT n’inclut aucun outil nous permettant de sérialiser simplement un objet, que ce soit en JSON ou en binaire, et les objets JsonArray/JsonObject ou les Autobeans ne sont en rien simple d’utilisation.

Il est par conséquent nécessaire de se reporter sur des librairies externes, qui ne sont pas nombreuses et qui nous obligent à implémenter une interface sur les Beans à sérialiser ou désérialiser.

Néamoins, ceci n’est pas vraiment un problème, puisque généralement nous ne partageons pas les beans entre plusieurs applications. Ceci est dû aux contraintes imposées par GWT, telles que la présence des fichiers *.java ou, si ces beans sont dans un jar, le fait que ce dernier doive correspondre à un module GWT.

Les librairies que j’ai pu tester sont les suivantes :

  • gwt-streamer [4]
  • GWTProJSONSerializer [5]
  • json4g [6]

Je vous laisse vous reporter à leur documentation pour plus de détails sur leur utilisation, qui se résume à trois choses :

  • Hériter du module dans votre fichier *.gwt.xml.
  • Implémenter une interface, qui permettra au Generator de GWT d’identifier les classes à générer suivant un modèle défini par la librairie.
  • Appeler une méthode statique pour encoder et décoder le bean.

Conclusion

Développer avec GWT demande un certain temps d’adaptation. Sa complexité effrayante au premier abord, et accentuée de part l’utilisation de plus en plus courante du MVP, peut encourager à nous demander si tout ceci en vaut la peine. Cependant, une fois ce cap franchi, qui je l’avoue peut prendre un certain temps, nous pouvons constater que nous avons aujourd’hui tous les outils nécessaires pour nous simplifier la vie et améliorer notre productivité, et que malgré tout GWT est un framework offrant de nombreuses possibilités plus qu’intéressantes.

Ressources

[1] http://docs.codehaus.org/display/JETTY/DataSource+Examples

[2] http://www.h2database.com/html/main.html

[3] http://code.google.com/p/google-gin/

[4] http://code.google.com/p/gwt-streamer/

[5] http://code.google.com/p/gwtprojsonserializer/

[6] http://code.google.com/p/json4g/

Nombre de vue : 72

AJOUTER UN COMMENTAIRE