mercredi 1 juin 2011

Configuration d'une application Struts2, Spring et Hibernate

Depuis quelques temps, je vois des articles qui expliquent comment créer le squelette d'une application Struts2. Malheureusement, ils s'en tiennent à la partie purement Web et négligent l'intégration avec Spring,  Hibernate (en supposant qu'on en aie besoin), et avec Maven...

Le but de cet article est de décrire, étape par étape, la création d'un squelette d'application mêlant Struts2, Spring et Hibernate, développé sur Eclipse avec le plugin d'intégration Maven et immédiatement déployable sur Tomcat (parce qu'il est bien intégré à Eclipse, mais l'application peut - sous certaines conditionss - se déployer sur n'importe quel serveur).

Allez, au boulot !

Environnement

L'environnement de développement visé est composé de :
  • jdk1.6 (mais ça fonctionne avec un jdk1.5)
  • Eclipse 3.6 (mais la procédure fonctionne globalement avec les versions 3.5 et 3.4, les plugins doivent éventuellement être adaptés). J'utilise la perspective Java EE.
  • le plugin maven-integration for Eclipse 0.12 (http://m2eclipse.sonatype.org/sites/m2e/)
  • le plugin maven-integration for WTP 0.12 ( à partir de http://m2eclipse.sonatype.org/sites/m2e-extras/)
  • un Tomcat 6 et donc jee5.

Suppositions et partis pris

Je pars du principe que vous savez utiliser Eclipse et Maven, que vous savez comment développer une application Web en Java et que vous savez déployer la dite application sur un Tomcat depuis Eclipse.

Les plugins d'intégration Maven ont été correctement installés.

Enfin, ce n'est pas non plus un tutorial pour utiliser Struts2, Spring ou Hibernate...

Création du projet de base

C'est un projet Maven. Donc, menu File>New>Other, choisir Maven Project (filtrer éventuellement sur Maven). Ne surtout pas cocher "create a simple project" parce qu'on va utiliser un archetype.

Next >

Dans la fenêtre suivante, sélectionner l'archetype. Pour cela, filtrer sur "webapp" pour plus de facilité et sélectionner org.codehaus.mojo.archetypes:webapp-jee5. (Voir note ci-dessous en cas de problème.)

Next >

Remplir les groupId, artifactId et version du projet (la valeur par défaut est généralement acceptable: 0.0.1-SNAPSHOT). Dans mon cas, j'ai choisi "be.fabrice" pour le groupId et "zeDemo" pour l'artifactId.

Le package se rempli automatiquement à partir de ces deux noms. Si ce n'est pas le cas, vous pouvez le remplir manuellement, mais l'archetype n'en fait a priori aucun cas.

Note

Il est commun dans certaines entreprises d'avoir un proxy. Dans certains cas, le plugin d'intégration Maven ne fonctionne pas correctement, en particulier en ce qui concerne les archetypes. C'est le cas pour la version 0.12. J'espère que la version suivante réglera ça.

Eventuellement, il est possible d'utiliser la ligne de commande et de créer le projet avec, en ligne de commande:

mvn archetype:generate -DgroupId=be.fabrice -DartifactId=zeDemo -DarchetypeGroupId=org.codehaus.mojo.archetypes -DarchetypeArtifactId=webapp-jee5 -DarchetypeVersion=1.2

Remplir les données demandées. Pour terminer, il faudra faire un "import existing maven projects" dans Eclipse.

Néanmoins, certaines facilités offertes par le plugin Maven ne seront pas disponibles.

Configuration de base

J'ai déjà remarqué qu'en fonction de la version plugin maven et de la version de l'archetype utilisé, certains éléments pouvaient ne pas exister. Notamment certains répertoires sources.

Dans mon cas, j'ai les répertoires src/main/java et src/test/java, mais pas les "resources". Il suffit de faire un clic droit sur le répertoire src/main pour y ajouter le répertoire "resources" et la même chose pour src/test.

Ces répertoires ne sont pas encore reconnus par Eclipse comme des répertoires sources. Il suffit d'un clic doit sur le projet et, dans le menu contextuel de choisir Maven > Upade Project Configuration.



Le projet est maintenant configuré.

Ajout des dépendances Maven

J'ai un principe quand j'ajoute les dépendances: réduire leur nombre au minimum et profiter des dépendances transitives.

Par exemple, pour utiliser Struts2, dans la mesure où l'intégration avec Spring doit se faire, la dépendance struts2-spring-plugin est suffisante. Elle importe de manière transitive struts2-core et la plupart des modules de spring 2.5.6.

Pour cela, ouvrir le fichier pom.xml dans l'éditeur par défaut (celui qui vient du plugin). Choisir l'onglet "Dependencies", cliquer sur le bouton Add et dans le champ de saisie du dialogue qui s'affiche, sélectionner struts2-spring. Rapidement, le plugin filtre les dépendances disponibles. Développez l'entrée org.apache.struts:struts2-spring-plugin et sélectionnez la version 2.2.3.



Note: de nouveau, il peut y avoir des problèmes avec un proxy... Il faudra cliquer sur "create" plutôt  que "add" et remplir les informations "à la main"...

Confirmez et sauvez le projet.

Vérifiez toujours les dépendances transitives. Les versions évoluant, elles changent parfois.

Pour cela, cliquez sur l'onglet "Dependency hierarchy". Vous y verrez la liste des dépendances importées transitivement par ce premier ajout.


Il reste à présent à ajouter les dépendances Hibernate. Souvent les annotations propres à Hibernate seront nécessaires en plus de celles venant de JPA pour pouvoir faire la jonction entre les entités et un schéma DB imposé.

Pour cela, la dépendance à importer est (même opération que pour Struts2) org.hibernate:hibernate-annotations:3.5.6-Final.

De nombreuses dépendances Spring ont déjà été importées, mais pas toutes celles dont nous avons besoin.

Pour que ce projet fonctionne, il faut encore ajouter org.springframework:spring-orm:2.5.6, org.springframework:spring-aop:2.5.6 et org.springframework:spring-jdbc:2.5.6 (apparemment nécessaire pour logback).

Car slf4j étant une dépendance transitive de nos framework, il lui faut un logger. Pour ma part, j'utilise logback et j'ajoute donc ch.qos.logback:logback-classic:0.9.17.

Je laisse de côté les dépendances de test.

Configuration xml

La configuration du projet se fait dans trois fichiers xml différents:

web.xml

Deux éléments à configurer obligatoirement: Struts2 et Spring.

Spring se charge à l'aide d'un ServletContextListener implémenté par une classe de Spring: ContextLoaderListener.

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

Par défaut, cette configuration ira chercher le fichier applicationContext.xml à la racine du WEB-INF. La pratique veut toutefois que les fichiers de configuration (à l'exception du web.xml) soient placés dans le répertoire src/main/resources, ce qui aura pour effet de les placer dans le WEB-INF/classes. Il arrive aussi parfois que l'on souhaite changer le nom du fichier de configuration de spring.

Il importe donc d'ajouter les lignes suivantes:

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

Le <param-value> est à adapter en fonction de la configuration. Celui-ci cherchera le fichier applicationContext.xml situé à la racine du classpath (soit WEB-INF/classes).

Il faut enfin configurer l'application pour qu'elle utilise Struts2. Cela se fait en configurant un filtre:

<filter>
       <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>

<filter-mapping>
       <filter-name>struts2</filter-name>
       <url-pattern>/*</url-pattern>
</filter-mapping>

OpenSessionInViewFilter?

Une pratique assez courante dans les applications Web "simples" avec Hibernate, est d'ajouter l'OpenSessionInViewFilter qui permet de garder "en vie" la session Hibernate dans le thread d'exécution de la requête. Cela permet notamment d'accéder aux objets lazy-loadés d'Hibernate de manière transparente partout dans l'application.

La configuration se fait à l'aide d'un filtre, placé AVANT le filtre de Struts2. Il est ici configuré pour n'intercepter que les requêtes se terminant par .demo, requêtes qui seront gérées par Struts2 (comme nous le verrons dans un instant).

<filter>
       <filter-name>hibernate</filter-name>
       <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>

<filter-mapping>
       <filter-name>hibernate</filter-name>
       <url-pattern>*.demo</url-pattern>
</filter-mapping>

Fichier final

Pour récapituler voici le fichier web.xml final.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  <display-name>ZeDemo demo application</display-name>
  <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:applicationContext.xml</param-value>
  </context-param>

  <filter>
      <filter-name>hibernate</filter-name>
      <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
  </filter>

  <filter>
      <filter-name>struts2</filter-name>
      <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
  </filter>

  <filter-mapping>
      <filter-name>hibernate</filter-name>
      <url-pattern>*.demo</url-pattern>
  </filter-mapping>

  <filter-mapping>
      <filter-name>struts2</filter-name>
      <url-pattern>/*</url-pattern>
  </filter-mapping>

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

applicationContext.xml

Voici le fichier de configuration de Spring. Il sera placé dans src/main/resources.

Il est configuré pour fonctionner de la manière suivante:
  • les beans sont configurés à l'aide d'annotations (@Component et dérivées) à l'exception de certains beans provenant de frameworks. Spring scannera les packages be.fabrice.zeDemo pour les trouver.
  • un pool de connexions doit exister dans le serveur et être exposé comme une ressource JNDI du nom de jdbc/zeDemoDatasource.
  • les transactions seront définies à l'aide de l'annotation @Transactionnal.
  • le gestionnaire de base de données que j'utilise est PostgreSQL et je souhaite mettre le schéma à jour en fonction des entités (nous sommes en phase de développement).
  • la configuration des entités Hibernate se fait par annotations et la SessionFactory scannera les packages be.fabrice.zeDemo.entities pour les trouver.

<?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:tx="http://www.springframework.org/schema/tx"
   xmlns:context="http://www.springframework.org/schema/context"
   xmlns:jee="http://www.springframework.org/schema/jee"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd"
        default-autowire="autodetect">

   <context:component-scan base-package="be.fabrice.zeDemo.*" annotation-config="true" />
   
   <jee:jndi-lookup id="datasource" jndi-name="jdbc/zeDemoDatasource" />
   
   <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
       <property name="dataSource" ref="datasource" />
       <property name="hibernateProperties">
           <value>
               hibernate.show_sql=true
               hibernate.hbm2ddl.auto=update
               hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
           </value>
       </property>
       <property name="packagesToScan" value="be.fabrice.zeDemo.entities" />
   </bean>

   <tx:annotation-driven transaction-manager="transactionManager" />
   
   <bean id="transactionManager"
       class="org.springframework.orm.hibernate3.HibernateTransactionManager">
       <property name="sessionFactory" ref="sessionFactory" />
   </bean>
</beans>

Config sans ressource JNDI

Une petite note pour indiquer les modifications à effectuer si on ne souhaite pas utiliser une ressource JNDI pour la configuration.

Il faut modifier le pom.xml pour y ajouter, par exemple, les apache-commons DBCP et le driver du gestionnaire de la base de données.

Dans le fichier applicationContext.xml, il faut remplacer:

<jee:jndi-lookup id="datasource" jndi-name="jdbc/zeDemoDatasource" />

par

<bean id="datasource" class="org.apache.commons.dbcp.BasicDataSource">
       <property name="driverClassName" value="driverClass" />
       <property name="username" value="username" />
       <property name="password" value="password" />
       <property name="url" value="urlDb" />
</bean>

Les valeurs "driverClass", "username", "password" et "urlDB" sont à remplacer par les valeurs adéquates...

struts.xml

Le dernier fichier à configurer est le fichier struts.xml, lui aussi dans les src/main/resources. Le seul élément que nous devons configurer pour le moment, c'est que les requêtes gérées par Struts2 doivent se terminer par "demo":

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.1.7.dtd">

<struts>
   <constant name="struts.action.extension" value="demo" />

   <include file="struts-default.xml" />

   <package name="default" extends="struts-default" >
   </package>
</struts>


logback.xml

Pas absolument nécessaire mais ça vaut mieux si vous utilisez logback. Toujours dans src/main/resources, le fichier logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 <appender name="STDOUT"  class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
     <Pattern>ZeDemo - %d{HH:mm:ss.SSS} %-5level %logger{55} - %msg%n</Pattern>
    </layout>
 </appender>

 <root>
    <level value="INFO" />
    <appender-ref ref="STDOUT" />
 </root>
</configuration>

Test

Testons que l'application est correctement configurée. Pour cela, nous devons configurer le serveur et créer une action de test. On pourrait aller plus loin en créant un bean Spring et vérifier qu'il est bien injecté dans l'action, mais ce n'est pas nécessaire.

Configuration du serveur

Généralement, je n'ai pas grand chose à faire de ce côté car le serveur est prêt. Néanmoins, voici les éléments indispensables.

Driver de la base de données
Lorsqu'on utilise une ressource JNDI pour définir la DataSource, il faut que le driver du gestionnaire de base de données soit dans le classpath du serveur et non de celui de l'application.

Le jar du driver doit être copié dans le TOMCAT_HOME/lib, tout simplement...

Ressource JNDI
Lorsque vous créez un runtime de serveur dans Eclipse, un dossier "Servers" est créé dans le "Project Explorer". Il y a un sous-répertoire pour le runtime du Tomcat et dans ce répertoire plusieurs fichiers de configuration, dont context.xml.

La configuration de la ressource JNDI consiste à ajouter dans ce fichier les lignes suivantes:

<Resource name="jdbc/zeDemoDatasource" auth="Container" type="javax.sql.DataSource"
              maxActive="30" maxIdle="10" maxWait="10000"
              username="username" password="password" 
driverClassName="org.postgresql.Driver"
              url="urlDb"/>

Le nom de la ressource doit être le même que celui configuré dans l'applicationContext.xml. Les propriétés "username", "password" et "urlDB" sont à modifier en fonction de vos propres valeurs.

Pour que le déploiement fonctionne, la base de données doit exister.

Action de test

Il ne s'agit pas de faire compliqué.

Pour l'action, une classe Java:

package be.fabrice.zeDemo;

public class TestAction {
   public String execute(){
       return "success";
   }
}

et une JSP test.jsp (que je mets dans le WEB-INF pour le moment):

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>OK</title>
</head>
<body>
Ca marche...
</body>
</html>

Bref, ça ne fait rien, mais permet de valider la configuration.

Pour terminer, il faut ajouter l'action dans l'élément <package> du fichier struts.xml :

<action name="test" class="be.fabrice.zeDemo.TestAction">
       <result name="success">/WEB-INF/test.jsp</result>
</action>

Les valeurs sont à modifier en fonction de vos propres packages...

Déploiement

Dans la view "Servers", clic droit sur le runtime du serveur, "Add and Remove". Sélectionner le nouveau projet (zeDemo) dans la liste de gauche et cliquer sur Add puis finish.

Il suffit de lancer le serveur. Souvent, pour éviter le timeout du serveur, je force le "publish" avant.

Si tout s'est bien passé, il suffit d'entrer l'adresse suivante dans le navigateur: http://localhost:8080/zeDemo/test.demo

Normalement, "Ca marche" devrait s'afficher...

Ouf...

Aucun commentaire:

Enregistrer un commentaire