I. Datasource et JNDI▲
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 :
<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 :
<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 :
<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.
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 :
<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 ne nous sont utiles qu'en Development Mode.
<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 deux ou trois commandes et votre application est prête à être utilisée.
Dans le cas présent, je pars du principe que vous avez déjà Maven configuré. Une fois les sources téléchargées, il vous suffit de lancer les trois 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 autres) et mvn gwt:run lance GWT en Dev Mode.
Si vous souhaitez vérifier que la base de données 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 (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.
II. L'injection de dépendances avec Spring▲
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. 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 :
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 :
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);
}
}
public
interface
UserService {
UserUI connect
(
String login, String password) throws
ClientException;
}
@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'exé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
III. 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ées côté serveur en se passant du framework RPC de GWT. Ceci permet, entre autres, 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 simples d'utilisation.
Il est par conséquent nécessaire de se reporter sur des bibliothèques 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 bibliothèques que j'ai pu tester sont les suivantes :
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 bibliothèque ;
- appeler une méthode statique pour encoder et décoder le bean.
IV. Conclusion▲
Développer avec GWT demande un certain temps d'adaptation. Sa complexité effrayante au premier abord, et accentuée de par 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.
V. Remerciements▲
Cet article a été publié avec l'aimable autorisation de la société SoatSoat.
Nous tenons à remercier ClaudeLELOUP pour sa relecture orthographique attentive de cet article et Mickael Baron pour la mise au gabarit.