Premier projet avec Tapestry5, Spring et Hibernate

Baptiste Meurant

Ce tutoriel a pour objectif de guider vos premiers pas dans la création d'un projet Web basé sur Tapestry5, Spring et Hibernate à travers un exemple très simple de page de login.

Ce tutoriel a fait l'objet d'une mise à jour technologique en collaboration avec Loïc Frering : ici.

- Titre : Premier projet avec Tapestry5, Spring et Hibernate

- Auteur : Baptiste Meurant

- Parution : 3 octobre 2007

- Mise à jour : 19 février 2020

- Licence : Copyright ® 2007 Baptiste Meurant. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Prérequis

- II-A. Installations

- II-B. Préparation de la BDD

- III. Création du projet

- IV. Architecture logicielle

- V. Mise en place d'Hibernate

- V-A. Installation d'Hibernate Tools

- V-B. Ajout des bibliothèques

- V-C. Configuration de la connexion

- V-D. Génération de la couche model et du mapping O/R

- V-E. Ajout des classes mappées à la configuration Hibernate

- VI. Mise en place de Spring

- VI-A. Couche d'accès aux données

- VI-B. Couche de services

- VI-C. Gestion des transactions

- VI-D. Intégration de Spring au lancement du projet

- VII. Mise en place de Tapestry

- VII-A. Simple login

- VII-B. Intégration avec Spring

- VIII. Mise en place du chargement retardé pour Hibernate

- IX. Conclusion

- X. Remerciements

- XI. Dans la même série…

I - Introduction

Ce tutoriel propose une application blanche pour un projet Tapestry5-Spring-Hibernate. Elle propose une page de login et une page principale et recherche l'existence du user en BDD. Le tutoriel se décompose en deux parties : une application blanche Spring-Hibernate autonome puis l'intégration de Tapestry5 à cette application. Ceci permettra de réutiliser la première partie avec un autre framework front.

Les sources de ce tutoriel sont téléchargeables ici [miroir http]
la version pdf de ce tutoriel est disponible ici [miroir http]

II - Prérequis

II-A - Installations

- Installez Eclipse et WTP téléchargeables ici ou à défaut, sur le site officiel d'eclipse.

- Installez Tomcat 5.5, téléchargeable ici.

- Installez mySQL, téléchargeable ici.

II-B - Préparation de la BDD

- Créez une base de données et créez la table user :

création de la table user

CREATE TABLE `user` (
`id_user` int(10) unsigned NOT NULL auto_increment,
`login_user` varchar(25) NOT NULL default '',
`password_user` varchar(25) NOT NULL default '',
PRIMARY KEY  (`id_user`)
) ENGINE=InnoDB;

Remarque : les tables MySQL doivent être au format InnoDB pour que la génération des classes du modèle (voir mapping Hibernate) fonctionne correctement sous Eclipse.

- Créez un utilisateur :

insertion du user de test

INSERT INTO user VALUES ('test', 'test');

III - Création du projet

Note : si vous avez déjà un projet existant, passez directement au chapitre suivant.

- Créez un nouveau projet de type Dynamic Web : allez dans File -> new Project et choisissez Web -> Dynamic Web Project. Cliquer sur Next.

Figure1 : Création du projet

- renseignez le nom du projet et choisissez un Target Runtime. Si rien n'est défini, cliquez sur new.

Figure2 : Configuration du projet

- choisissez un runtime (ici tomcat 5.5). Attention, il est nécessaire d'avoir installé le serveur.

Figure3 : Choix du runtime

- renseignez les informations demandées et cliquez sur Finish.

Figure4 : Configuration du serveur

- de retour à l'interface de la Figure2, cliquez sur Next puis encore sur Next.

Figure5 : Finalisation du projet

- changez le content directory à 'docs' (non obligatoire). Cliquez sur Finish.

- à l'issue de ces étapes, le package explorer devrait avoir l'allure de la Figure6 : un projet de type Dynamic Web créé ainsi qu'une configuration Server intégrée à Eclipse.

Figure6 : Projet vide

IV - Architecture logicielle

L'un des intérêts de l'intégration de ces différents frameworks est de permettre la mise en place d'une architecture logicielle rigoureuse, de manière à garantir la maintenabilité, l'évolutivité et l'exploitabilité des applications. La figure ci-dessous montre l'architecture qui sera mise en place dans le cadre de ce tutoriel. Ce type d'architecture est très largement admise comme efficace et est généralisable à n'importe quel projet Web.

Figure7 : Architecture logicielle

La principale caractéristique de cette architecture est la séparation des préoccupations (données vs métier vs web) grâce à la séparation stricte des couches applicatives. En effet, on peut observer sur la figure ci-dessus, les trois couches de cette application :

- couche DAO : cette couche permet les accès à la BDD à travers le framework Hibernate ;

- couche Service : cette couche contient l'ensemble du code métier de l'application, elle organise et orchestre les accès à la couche DAO et ses aspects transactionnels. Ces différents aspects sont gérés et organisés par le framework Spring ;

- couche Front : cette couche est la couche d'entrée dans l'application du point de vue du client. Elle appelle les traitements de la couche Service en fonction des actions effectuées par le client et récupère les données retournées. Elle met ensuite en forme ces données pour les afficher au client. Cette couche est réalisée grâce au framework Tapestry5.

Ces trois couches sont rigoureusement séparées les unes des autres en ce sens qu'il ne doit exister idéalement aucune dépendance entre elles. Concrètement, chaque couche ne connaît que les interfaces de la couche immédiatement inférieure. Ainsi la couche Service ne connaît que les interfaces de la couche DAO et la couche Front ne connaît que les interfaces de la couche Service. De cette manière, chaque couche publie, via ses interfaces, l'ensemble des traitements qu'elle met à disposition des couches supérieures. Le lien entre interface et implémentation, qui introduit un couplage technologique (c'est dans l'implémentation uniquement que l'on fait référence à Hibernate par exemple) est géré par Spring.

C'est en effet Spring qui va nous permettre d'effectuer, au chargement de l'application, le lien entre Interface et Implémentation grâce à son moteur d'Inversion de Contrôle et d'injection de dépendances (voir plus loin). Le développeur ne travaille ensuite que sur des appels d'interfaces et non sur l'implémentation directement ce qui favorise grandement, on l'imagine, l'évolutivité et le faible couplage d'une application.

Enfin, on remarque une couche particulière, la couche model. Cette couche est la seule couche transverse à l'application puisqu'elle permet en effet de faire correspondre au modèle BDD le modèle objet que l'on va utiliser dans l'application pour manipuler les entités métier. Chaque couche peut donc naturellement manipuler les différentes entités métier représentées par une hiérarchie de JavaBean correspondant chacun à une entité relationnelle.

Cette correspondance est appelée mapping Object/Relationnel et est permise par l'utilisation d'Hibernate. Chaque notion relationnelle est ainsi représentée sous sa forme objet ainsi que ses dépendances. Cela permet ensuite de faire persister les objets vers la BDD de manière complètement transparente. Ainsi, comme on le verra plus tard, chaque modification effectuée sur un objet mappé dans un contexte transactionnel sera automatiquement répercutée en base de données sans action explicite de la part du développeur. C'est ce que l'on appelle la persistance.

Insistons quelques instants sur la manipulation de la BDD. Le schéma et les explications ci-dessus montrent en effet que cette manipulation comporte deux aspects :

- la couche DAO publie des méthodes d'accès à la BDD de type création, recherche et suppression d'enregistrements (CREATE, SELECT et DELETE). En bref, cette couche permet de récupérer des instances d'objets à partir d'enregistrement BDD, de créer des nouvelles instances d'objets en créant les enregistrements BDD ou de supprimer des instances existantes en supprimant les enregistrements BDD :

- le mapping O/R et la persistance des données permettent, dans un contexte transactionnel, d'effectuer toutes les opérations de type mise à jour (UPDATE). En effet, une fois récupérées des instances d'objets persistés grâce à la couche DAO, toute modification de l'instance entraînera une modification de l'enregistrement mappé.

Ainsi :

- aucune méthode de mises à jour d'instances n'est nécessaire pour les objets qui ont été obtenus dans un context transactionnel depuis la couche DAO ;

- cependant, les méthodes de création d'objets correspondant à des INSERT en base de données restent necessaires, car même dans un contexte transactionnel la couche Services ne peut décider à votre place s'il faut persister ou non une nouvelle instance de la couche model. Lorsque l'on crée un objet du model, il peut en effet s'agir d'une variable de travail temporaire par exemple qu'on ne souhaite donc pas enregistrer.

Ces différents aspects seront détaillés par la suite.

V - Mise en place d'Hibernate

Note : la version d'Hibernate considérée ici est une version 3.2 ou supérieure.

Dans cette partie nous allons nous charger de générer le modèle objet à partir du modèle relationnel et de mettre en place la persistance de ce modèle grâce à Hibernate.

Télécharger Hibernate Core, Hibernate Annotations et Hibernate Tools sur http://www.hibernate.org/30.html ou récupérer l'archive du projet.

V-A - Installation d'Hibernate Tools

Hibernate Tools est un plugin Eclipse permettant de générer automatiquement les classes de mapping du modèle à partir de la BDD.

- Extraire le zip téléchargé.

- Copier les deux répertoires plugins et features qu'il contient dans le répertoire d'installation d'Eclipse.

- Redémarrer Eclipse.

- Un certain nombre de nouveaux éléments apparaissent dans l'interface, prouvant l'installation du plugin :

Figure8 : Hibernate Tools Installé

V-B - Ajout des bibliothèques

- Récupérer le driver de votre BDD (pour MySQL, voir ici) et le copier dans WEB-INF/lib.

- Faire un refresh sur le projet dans Eclipse, le jar doit apparaître dans les Web App bibliothèques.

- Inclure l'ensemble des jar nécessaires à Hibernate.

Figure9 : Ajout des jar Hibernate

V-C - Configuration de la connexion

- Créer un répertoire config et l'inclure en tant que source folder.

Figure10 : Création du répertoire config

- Sélectionner le répertoire config et cliquer sur File/New/Other. Puis choisir Hibernate/Hibernate configuration file.

Figure11 : Création du fichier de configuration Hibernate

- Configurer la connexion à la BDD en fonction des paramètres locaux. Penser à bien cocher la case 'Create Console configuration'

Figure12 : Configuration des paramètres de connexion

- Nous allons en profiter pour configurer la console Hibernate Tools afin de disposer d'un outil simple de génération de mapping. Configurer la console comme indiqué ci-dessous :

Figure13 : Configuration de la console

- Cliquer sur Classpath/Add JAR et ajouter le driver BDD puis cliquer sur Finish.

Figure14 : Création du lien entre la console et le driver

Le fichier de configuration Hibernate a été créé comme ceci :

Fichier de configuration Hibernate - hibernate.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.password">root</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/experiments</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
</session-factory>
</hibernate-configuration>

- Ouvrir la vue Console et explorer la BDD. Le résultat devrait être celui-ci. Dans le cas contraire, vérifier les paramètres de connexion.

Figure15 : Exploration de la BDD via la console

V-D - Génération de la couche model et du mapping O/R

- Lancer la génération de code automatique :

Figure16 : Lancement de la génération automatique

- Configurer la génération de code comme ceci :

Figure17 : Configuration de la génération automatique 1

- Cliquer sur Exporters et cocher uniquement la génération de Domain code. Cliquer sur Run.

Figure18 : Configuration de la génération automatique 2

- Le code de la classe User a été généré avec l'ensemble des annotations nécessaires au mapping Hibernate.

Figure19 : Génération du JavaBean User mappé

Classe User générée

package tuto.webssh.domain.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "user", catalog = "experiments")
public class User implements java.io.Serializable {

private int idUser;
private String loginUser;
private String passwordUser;

public User() {
}

public User(int idUser, String loginUser, String passwordUser) {
this.idUser = idUser;
this.loginUser = loginUser;
this.passwordUser = passwordUser;
}

@Id
@Column(name = "id_user", unique = true, nullable = false)
public int getIdUser() {
return this.idUser;
}

public void setIdUser(int idUser) {
this.idUser = idUser;
}

@Column(name = "login_user", nullable = false, length = 25)
public String getLoginUser() {
return this.loginUser;
}

public void setLoginUser(String loginUser) {
this.loginUser = loginUser;
}

@Column(name = "password_user", nullable = false, length = 25)
public String getPasswordUser() {
return this.passwordUser;
}

public void setPasswordUser(String passwordUser) {
this.passwordUser = passwordUser;
}

}

On note les annotations :

- @Entity qui permet de déclarer la classe comme une entité persistante Hibernate ;

- @Table qui permet d'associer l'entité à une table d'un catalogue, d'un schéma, d'une database donnée ;

- @Id qui définit une propriétaire comme identifiant de l'entité (clé primaire) ;

- @Column qui associe la propriété avec un champ BDD et gère un certain nombre d'options. La propriété id par exemple, est unique et ne peut être commitée à null. On peut ainsi définir si la propriété peut être modifiée, etc. Cela permet à Hibernate d'effectuer les contrôles de cohérence et de sécurité en amont de l'enregistrement en base.

V-E - Ajout des classes mappées à la configuration Hibernate

Modifier le fichier hibernate.cfg.xml pour mapper la classe générée.

Modification du fichier hibernate.cfg.xml

<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.password">root</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/experiments</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<mapping class="tuto.webssh.domain.model.User"/>
</session-factory>
</hibernate-configuration>

Suite à cela, l'objet User est un objet persistant. Toute instance de User correspondra à un enregistrement BDD.

VI - Mise en place de Spring

Note : la version de Spring considérée ici est une version 2.0 ou supérieure.

- Télécharger Spring ici.

- Décompresser le zip et copier le spring.jar dans WEB-INF/lib

Figure20 : Ajout des bibliothèques Spring

Le framework Spring repose sur un moteur d'inversion de contrôle et d'injection de dépendances. Il s'agit de développer l'application en se basant uniquement sur des interfaces - ce qui permet de découper proprement l'application en couches bien distinctes. C'est ensuite le framework - en tant que contrôleur du flot d'exécution de l'application - d'injecter, au démarrage de l'application, le code de l'implémentation dans les différentes classes utilisant l'interface qu'elle étend. Ceci est fait grâce à un mécanisme d'injection de beans Spring grâce à des setters fournis par le développeur dans ses classes et à la configuration de chacun des couples interface/implémentation en tant que bean Spring configuré dans différents fichiers xml.

En se reposant sur ces mécanismes, nous allons créer deux couches distinctes : la couche d'accès aux données (dao) et la couche de services métiers (service) et leur permettre de travailler ensemble.

VI-A - Couche d'accès aux données

- Créer l'interface UserDao dans le package dao

Figure21 : Création de l'interface UserDao

- Créer l'implémentation de cette interface UserDaoImpl dans le package dao.hibernate3. Afin de bénéficier des fonctionnalités avancées et des API de Spring en ce qui concerne l'intégration avec Hibernate, faire dériver cette classe de la classe HibernateDaoSupport.

Figure22 : Création de l'implémentation UserDaoImpl 1

Figure23 : Création de l'implémentation UserDaoImpl 2

Figure24 : Création de l'implémentation UserDaoImpl 3

Voici les interfaces et classes créées. On en profite pour ajouter tout de suite le code dont on aura besoin par la suite :

Interface UserDao

package tuto.webssh.domain.dao;

import org.springframework.dao.DataAccessException;

import tuto.webssh.domain.model.User;

/**
* Allows performing complex actions on persistent data
* @author bmeurant
*/
public interface UserDao {

/**
* Check if the login exists and if the password is correct in datasource.
* @param login : user login
* @param password : user password
* @return true if the login exists and if the password is correct.
* Otherwise, return false.
* @throws DataAccessException in case of Data access errors
* (database unreachable, etc.)
*/
public boolean checkLogin (String login, String password);

/**
* Return a User object from a given login.
* @param login : user login
* @return the corresponding user object.
* @throws DataAccessException in case of Data access errors
* (database unreachable, etc.)
*/
public User getUser(String login);

}

Implémentation UserDaoImpl

package tuto.webssh.domain.dao.hibernate3;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Expression;
import org.springframework.dao.DataAccessException;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import tuto.webssh.domain.dao.UserDao;
import tuto.webssh.domain.model.User;

/**
* Implements a strategy to perform complex actions on persistent data.
* @author bmeurant
*/
public class UserDaoImpl extends HibernateDaoSupport implements UserDao {

/**
* {@inheritDoc}
*/
public boolean checkLogin(String login, String password) {
if (null == login || null == password) {
throw new IllegalArgumentException("Login and password are mandatory. Null values are forbidden.");
}
try {
logger.info("Check user with login: "+login+" and password : [PROTECTED]");
Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
// create a new criteria
Criteria crit = session.createCriteria(User.class);
crit.add(Expression.ilike("loginUser", login));
crit.add(Expression.eq("passwordUser", password));

User user = (User)crit.uniqueResult();
return (user != null);
}
catch(DataAccessException e) {
// Critical errors : database unreachable, etc.
logger.error("Exception - DataAccessException occurs : "+e.getMessage()
+" on complete checkLogin().");
return false;
}
}

/**
* {@inheritDoc}
*/
public User getUser(String login) {
if (null == login) {
throw new IllegalArgumentException("Login is mandatory. Null value is forbidden.");
}
try {
logger.info("get User with login: "+login);
Session session = getHibernateTemplate().getSessionFactory().getCurrentSession();
// create a new criteria
Criteria crit = session.createCriteria(User.class);
crit.add(Expression.eq("loginUser", login));

User user = (User)crit.uniqueResult();
return user;
}
catch(DataAccessException e) {
// Critical errors : database unreachable, etc.
logger.error("Exception - DataAccessException occurs : "+e.getMessage()
+" on complete getUser().");
return null;
}
}
}

On remarquera :

- l'utilisation du logger (API commons-logging) nativement intégré du fait de l'extension de la classe HibernateDaoSupport - l'utilisation de l'API générique commons-logging permet de greffer un système de log beaucoup plus puissant comme log4j par exemple ;

- l'utilisation de la très riche API Hibernate Criteria permettant d'effectuer des requêtes en 'mode objet'. Noter que le même résultat peut être obtenu en HQL, mais paraît - de mon point de vue - beaucoup moins élégant ;

- l'utilisation des diverses interfaces Spring d'accès à la couche Hibernate qui permet de simplifier grandement les accès à la couche de mapping (par exemple le getHibernateTemplate(), etc.).

Créer un fichier xml ApplicationContextDao.xml dans WEB-INF et déclarer le couple interface implémentation sous la forme de bean Spring. Par défaut, les beans Spring sont des singletons et sont donc ThreadSafe. Cela garantit que le code peut être appelé sans risque de manière concurrente par deux process différents. Ce mécanisme est entièrement géré en interne par Spring.

Ce fichier définit un bean userDao correspondant à une interface et y associe l'implémentation correspondante. Ce bean contient une propriété (au sens des JavaBeans) sessionFactory définissant la configuration de la session Hibernate et notamment le lien vers le fichier de configuration Hibernate ainsi que la stratégie permettant de récupérer les différents objets mappés dans Hibernate. Ici, il s'agit de la stratégie basée sur les annotations.

Fichier xml Spring Généré - ApplicationContextDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<!-- Application context DAO layer -->

<beans>
<!-- General  -->
<bean id="userDao" class="tuto.webssh.domain.dao.hibernate3.UserDaoImpl">
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
</bean>

<!-- sessionFactory  -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation">
<value>classpath:hibernate.cfg.xml</value>
</property>
<property  name="configurationClass">
<value>org.hibernate.cfg.AnnotationConfiguration</value>
</property>
</bean>
</beans>

VI-B - Couche de services

Les éléments de la couche dao vont être utilisés par les services métier de la couche service supérieure.

- Dans deux nouveaux packages service et service.impl, créer l'interface UserManager et son implémentation UserManagerImpl.

Figure25 : Création du UserManager

Interface UserManager

package tuto.webssh.service;

import tuto.webssh.domain.model.User;

/**
* This interface publishes business features to handler users
* @author bmeurant
*/
public interface UserManager {

/**
* Check if the login exists and if the password is correct.
* @param login : user login
* @param password : user password
* @return true if the login exists and if the password is correct.
* Otherwise, return false.
*/
public boolean checkLogin (String login, String password);

/**
* Return a User object from a given login.
* @param login : user login
* @return the corresponding user object.
*/
public User getUser(String login);

/**
* Change the password to 'password' for the given login
* @param login : user login
* @param password : user new password
* @return the new User object
*/
public User changePassword (String login, String password);

}

Implémentation UserManagerImpl

package tuto.webssh.service.impl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import tuto.webssh.domain.dao.UserDao;
import tuto.webssh.domain.model.User;
import tuto.webssh.service.UserManager;

/**
* Implements business features to handler users
* @author bmeurant
*/
public class UserManagerImpl implements UserManager {

private final Log logger = LogFactory.getLog(UserManagerImpl.class);

/**
* {@inheritDoc}
*/
public boolean checkLogin (String login, String password) {
return userDao.checkLogin(login, password);
}

/**
* {@inheritDoc}
*/
public User changePassword(String login, String password) {
User user = userDao.getUser(login);
if (user != null) {
user.setPasswordUser(password);
}
return user;
}

/**
* {@inheritDoc}
*/
@SuppressWarnings("finally")
public User getUser(String login) {
return userDao.getUser(login);
}

}

On désire que la couche service accède à la couche dao. Cependant, partant du principe de séparation stricte des couches, l'ensemble des implémentations de services ne vont accéder directement qu'aux interfaces des dao et non aux implémentations. C'est Spring qui se chargera d'injecter les implémentations dans les objets (du type de l'interface) prévus à cet effet. Cette opération est possible grâce, d'une part à l'association faite entre l'interface et l'implémentation au niveau du fichier xml et, d'autre part par un injecteur (setter) prévu à cet effet dans chaque implémentation de la couche service.

- Éditer la classe UserManagerImpl pour créer l'injecteur qui lui permettra d'accéder aux dao. On en profite pour ajouter les méthodes dont on aura besoin par la suite.

Modification de UserManagerImpl

package tuto.webssh.service.impl;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import tuto.webssh.domain.dao.UserDao;
import tuto.webssh.domain.model.User;
import tuto.webssh.service.UserManager;

/**
* Implements business features to handler users
* @author bmeurant
*/
public class UserManagerImpl implements UserManager {

private final Log logger = LogFactory.getLog(UserManagerImpl.class);

private UserDao userDao = null;

/**
* setter to allows spring to inject userDao implementation
* @param userDao : object (implementation of UserDao interface) to inject.
*/
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

/**
* {@inheritDoc}
*/
public boolean checkLogin (String login, String password) {
return userDao.checkLogin(login, password);
}

/**
* {@inheritDoc}
*/
public User changePassword(String login, String password) {
User user = userDao.getUser(login);
if (user != null) {
user.setPasswordUser(password);
}
return user;
}

/**
* {@inheritDoc}
*/
@SuppressWarnings("finally")
public User getUser(String login) {
return userDao.getUser(login);
}
}

On remarque qu'il n'est fait mention ici d'aucune implémentation : seule l'interface est connue et accédée via la variable locale userDao. Au moment du démarrage de l'application, Spring utilisera le setter fourni pour injecter l'implémentation de l'interface UserDao définie dans le bean userDao du fichier ApplicationContextDao.xml dans l'objet userDao de la classe UserManagerImpl. Grâce à des appels à celui-ci et à la configuration effectuée dans le fichier ApplicationContextDao.xml, l'ensemble des méthodes de l'objet UserDao seront accessibles (attention : seules les méthodes déclarées dans l'interface pourront être utilisées).

Pour pouvoir effectuer cette injection correctement, Spring a besoin que soit défini le bean userManager et qu'il référence, en tant que propriété interne, le bean userDao.

- Créer un fichier ApplicationContext.xml dans WEB-INF et y placer le code suivant :

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>
<bean id="userManager" class=" tuto.webssh.service.impl.UserManagerImpl">
<property name="userDao">
<ref bean="userDao" />
</property>
</bean>
</beans>

On retrouve le mécanisme d'association interface/implémentation et on découvre l'injection de dépendance avec la définition de la propriété userDao qui fait référence au bean défini plus haut de même nom. Attention aux règles de nommage : le nom de la propriété du bean doit correspondre exactement au nom de la propriété du bean userManager. Pour y accéder, Spring transformera le nom de cette propriété en la faisant commencer par une majuscule puis en ajoutant 'set' devant.

VI-C - Gestion des transactions

Outre l'inversion de contrôle, l'injection de dépendances, la structuration en couche, Spring offre des mécanismes de gestion des transactions BDD très puissants et très élégants dont on souhaite pouvoir bénéficier ici. Ces transactions sont gérées par des mécanismes de proxy et de PAO (Programmation Orientée Aspect), mécanismes qui sont au cœur même du framework Spring. La configuration des transactions se fait en trois temps.

- Définition du proxy général abstrait de manière globale à l'application dans un fichier xml Spring : dans ApplicationContextDao.xml, ajouter :

applicationcontextDao.xml - configuration des transactions

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="transactionProxy" abstract="true"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager">
<ref bean="transactionManager"/>
</property>
<property name="transactionAttributes">
<props>
<prop key="insert*">PROPAGATION_REQUIRED</prop>
<prop key="update*">PROPAGATION_REQUIRED</prop>
<prop key="save*">PROPAGATION_REQUIRED</prop>
<prop key="*">PROPAGATION_REQUIRED, readOnly</prop>
</props>
</property>
</bean>

- Configuration de proxy spécifiques, héritant du proxy général, devant chaque bean potentiellement transactionnel. modifier ApplicationContext.xml :

configuration des transactions dans applicationcontextDao.xml

<beans>
<bean id="userManagerTarget" class="tuto.webssh.service.impl.UserManagerImpl">
<property name="userDao">
<ref bean="userDao" />
</property>
</bean>
<bean id="userManager" parent="transactionProxy">
<property name="target">
<ref bean="userManagerTarget"/>
</property>
<property name="transactionAttributeSource">
<bean class="org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"/>
</property>
</bean>
</beans>

On a donc défini un proxy transactionnel devant le bean. La configuration de la transaction est indiquée par la référence au bean transactionManager et on indique que la configuration détaillée de la transaction se fera par des annotations au niveau de l'interface du UserManager.

- Configuration par annotations au niveau de chaque implémentation de la couche métier pour la configuration détaillée de la transaction. Modifier UserManager :

gestion des transactions - modification de UserManager

package tuto.webssh.service;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import tuto.webssh.domain.model.User;

/**
* This interface publishes business features to handler users
* @author bmeurant
*/
@Transactional (readOnly=true, propagation=Propagation.REQUIRED)
public interface UserManager {

/**
* Check if the login exists and if the password is correct.
* @param login : user login
* @param password : user password
* @return true if the login exists and if the password is correct.
* Otherwise, return false.
*/
public boolean checkLogin (String login, String password);

/**
* Return a User object from a given login.
* @param login : user login
* @return the corresponding user object.
*/
public User getUser(String login);

/**
* Change the password to 'password' for the given login
* @param login : user login
* @param password : user new password
* @return the new User object
*/
@Transactional (readOnly=false)
public User changePassword (String login, String password);

}

Après ça, Spring se chargera tout seul de l'ensemble des mécanismes transactionnels : commit, rollback, persistance des objets mappés pour autant que la configuration choisie place le flot exécuté dans un contexte transactionnel. À noter que c'est l'exécution d'une méthode de service qui marque la fin de la transaction et non l'exécution d'une méthode de DAO.

Suite à cela, on peut examiner l'implémentation de la méthode changePassword plus haut et remarquer l'utilisation du caractère persistant de l'objet User : dans la méthode changePassword en effet, l'objet User est récupéré à partir du login fourni en paramètre. C'est ensuite le champ password de l'objet récupéré qui est alors modifié et aucun accès n'est fait à la couche DAO. Simplement, puisque cette méthode est transactionnelle (et n'est pas en lecture seule), toute modification effectuée sur l'objet sera persistée en BDD à la fermeture de la transaction (si celle-ci s'est déroulée correctement et qu'aucune exception n'a été générée), c'est-à-dire à la fin de l'exécution de la méthode. Le password sera alors modifié.

VI-D - Intégration de Spring au lancement du projet

La dernière étape consiste à intégrer ces deux fichiers xml de définition des beans ainsi que la configuration du filtre Spring dans le web.xml afin de demander à Spring de construire les beans au chargement de l'application :

web.xml - configuration de Spring

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml /WEB-INF/applicationContextDao.xml</param-value>
</context-param>

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

- Ajouter ensuite les dépendances de Spring :

Figure26 : Ajout des dépendances de Spring

Spring va alors charger l'ensemble de la configuration au démarrage de l'application en remontant les éventuelles erreurs. Il ne reste plus qu'à lancer le serveur et traquer les messages d'erreur.

Pour davantage d'informations, ajouter le jar log4j au classpath ainsi que le fichier log4j.properties dans le répertoire config.

Configuration de log4J

# Set root logger level to DEBUG and its only appender to CONSOLE.
log4j.rootLogger=DEBUG,CONSOLE_APP

# le appender CONSOL_APP est associé à la console
log4j.appender.CONSOLE_APP=org.apache.log4j.ConsoleAppender
# CONSOLE_APP utilise un PatternLayout qui affiche : le nom du thread, la priorité,
# le nom du logger et le message
log4j.appender.CONSOLE_APP.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE_APP.layout.ConversionPattern= %d{dd-MM-yyyy HH:mm:ss:SSS} %-4r %-5p %c %x - %m%n

# Change the level of messages for various packages.
log4j.logger.org.apache=DEBUG
log4j.logger.org.springframework=DEBUG
log4j.logger.org.hibernate.cache=DEBUG
log4j.logger.org.hibernate.cfg=DEBUG
log4j.logger.org.hibernate=DEBUG

Au démarrage, Spring doit loguer :

INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@bf7190:
defining beans [userManagerTarget,userManager,userDao,sessionFactory,transactionManager,transactionProxy];
root of factory hierarchy

VII - Mise en place de Tapestry

Note : la version de Tapestry considérée ici est la version 5 ou supérieure.

VII-A - Simple login

Maintenant que Spring et Hibernate sont configurés à blanc, il reste à intégrer le framework Front Tapestry à cette architecture.

- La première étape est de récupérer les jar Tapestry et de les ajouter au projet :

Figure27 : Ajout des jar Tapestry

- Créer un fichier nommé Login.html sous WEB-INF :

Login.html

<HTML xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Login</title>
</head>
<body>
<div id="login_box">
<t:form>
<t:errors />
<table>
<tr>
<td><label t:type="Label" for="login" class="login_label"/></td>
<td><input t:type="TextField" t:id="login" t:value="login" t:label="login "
class="login_input" /></td>
</tr>
<tr>
<td><label t:type="Label" for="password" class="login_label"/></td>
<td><input t:type="PasswordField" t:id="password" t:value="password" t:label="password "
class="login_input" /></td>
</tr>
<tr>
<td><input t:id="submitform" t:type="Submit" t:value="submit" class="login_submit"/></td>
</tr>
</table>
</t:form>
</div>
</body>
</HTML>

On note les attributs t :value qui permettent de lier un élément HTML à une variable java. Les attributs for permettent de lier un label à un input HTML.

- Créer un package pages et une classe Login.java qui est l'image Java du formulaire HTML :

classe Login

package tuto.webssh.pages;

import org.apache.tapestry.annotations.ApplicationState;
import org.apache.tapestry.beaneditor.Validate;

public class Login {

@ApplicationState
private String login;

private String password;

public String getLogin() {
return login;
}

@Validate("required")
public void setLogin(String login) {
this.login = login;
}

public String getPassword() {
return password;
}

@Validate("required")
public void setPassword(String password) {
this.password = password;
}

String onSuccess() {
//mon code métier
String ret = "Home";
return ret;
}
}

On note :

- l'annotation @ApplicationState qui permet de spécifier qu'une variable doit persister en session. Ainsi, au rechargement de la page login (ou d'une autre page contenant la même variable accompagnée d'une annotation @ApplicationState), la valeur de la variable sera récupérée depuis la session dans la variable locale.

- l'annotation @Validate(« required ») qui précise que le champ HTML associé est obligatoire. Ainsi, si au submit du formulaire, la valeur n'est pas définie, un message d'erreur sera affiché ;

- la méthode onSuccess qui est exécutée automatiquement au submit du formulaire et qui retourne le nom de la page vers laquelle l'utilisateur sera redirigé (ici Home) ;

- créer une page Home.html sous WEB-INF, page vers laquelle sera redirigé l'utilisateur après le login

Page Home.html

<HTML xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Congratulation</title>
</head>
<body>
Congratulations, you are logged with login: ${login} !!<br/><br/>
<span id="LogoutLink"><span class="moduleTitle"><t:actionlink
t:id="logout">Deconnexion</t:actionlink></span></span>
</body>
</HTML>

L'instruction ${login} permet de récupérer la valeur de la variable login dans la session, si elle est définie. La récupération de cette variable se fait grâce au contexte ApplicationState qui lui a été associé dans les classes Java associées. Il est important que la variable login soit présente dans les deux objets java et associée, dans ces deux classes, au contexte ApplicationState.

L'élément t :actionLink permet de définir un lien vers une autre page Tapestry.

- Créer un package pages et une classe Home.java à l'intérieur de ce package, image java du formulaire HTML :

page Home.java

package tuto.webssh.web.pages;

import javax.servlet.http.HttpSession;

import org.apache.tapestry.ComponentResources;
import org.apache.tapestry.Link;
import org.apache.tapestry.annotations.ApplicationState;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.OnEvent;
import org.apache.tapestry.services.RequestGlobals;

public class Home {

@Inject
private ComponentResources resources;

@Inject
private RequestGlobals requestGlobals;

@ApplicationState
private String login;

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}

@OnEvent(component = "logout")
public Link onLogout()
{
HttpSession session = requestGlobals.getHTTPServletRequest().getSession();
session.invalidate();
return resources.createPageLink("login", false);
}

}

On note :

- encore une fois, l'annotation @ApplicationState qui permet de récupérer la variable login depuis la session pour l'afficher ;

- l'annotation @Inject qui permet à Tapestry d'effectuer de l'injection de dépendances depuis des implémentations tierces (ici J2EE core) ;

- la méthode onLogout qui, grâce à l'annotation @onEvent(component=« logout ») est exécutée à chaque évènement défini (en fonction de son type) sur le composant spécifié. Ici, le composant est un lien et, à chaque clic sur ce lien, la session sera invalidée et l'utilisateur redirigé vers la page de login.

Éditer le web.xml pour configurer le filtre Tapestry et le package de base grâce auquel il sera capable de retrouver le package pages contenant l'ensemble des bean Java représentant des pages HTML.

web.xml - Intégration de Tapestry

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>BlankApplication</display-name>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml /WEB-INF/applicationContextDao.xml</param-value>
</context-param>

<context-param>
<param-name>tapestry.app-package</param-name>
<param-value>tuto.webssh.web</param-value>
</context-param>

<filter>
<filter-name>app</filter-name>
<filter-class>org.apache.tapestry.TapestryFilter</filter-class>
</filter>

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

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

<welcome-file-list>
<welcome-file>Login</welcome-file>
</welcome-file-list>
</web-app>

- Lancer le serveur et taper l'URL : http://localhost:8080/BlankApplicationTapestry/login. On obtient la page suivante :

Figure28 : Formulaire de login

On remarque au passage que si on clique sur Submit avant d'avoir rempli les deux champs login et password, des erreurs sont affichées. Ceci est le comportement et l'affichage par défaut de Tapestry suite à l'instruction @Validate.

Figure29 : validation du formulaire de login

Si les deux champs sont remplis et que l'on clique sur Submit, la page home s'affiche. On remarque que le login a bien été passé en session et qu'il est bien affiché sur la page home alors que renseigné sur la page Login.

Figure30 : Page Home

Le lien déconnexion nous ramène sur la page Login.

VII-B - Intégration avec Spring

Si l'on peut trouver que le résultat est déjà pas mal par rapport au travail effectué, il est tout à fait insuffisant. Il va maintenant s'agir d'aller chercher l'utilisateur en base et de vérifier son existence, la validité de son mot de passe, etc. Pour cela, nous allons traverser successivement les différentes couches de notre application, depuis la couche Tapestry (couche application) jusqu'à la couche dao en passant par la couche service.

Comme expliqué plus haut, la communication entre ces différentes couches - ainsi qu'un certain nombre de fonctions avancées associées (transactions, etc.) - sont gérées par Spring. Il va donc s'agir d'intégrer Spring et Tapestry. Cela est possible de manière native avec Tapestry5.

La première étape est la modification du filtre Tapestry dans le web.xml afin de permettre l'intégration avec Spring : il faut remplacer la classe org.apache.tapestry.TapestryFilter par org.apache.tapestry.spring.TapestrySpringFilter.

Intégration de Tapestry et Spring - modification du web.xml

<filter>
<filter-name>app</filter-name>
<!-- Special filter that adds in a T5 IoC module derived from the Spring
WebApplicationContext. -->
<filter-class>org.apache.tapestry.spring.TapestrySpringFilter</filter-class>
</filter>

Une fois que Tapestry est configuré pour fonctionner avec Spring, on doit injecter les beans Spring dans les pages Tapestry. Modifier le fichier Login.java :

Tapestry - classe Login.java

package tuto.webssh.pages;

import org.apache.tapestry.annotations.ApplicationState;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.Persist;
import org.apache.tapestry.annotations.Service;
import org.apache.tapestry.beaneditor.Validate;

import tuto.webssh.service.UserManager;

public class Login {

private static final String BAD_CREDENTIALS
= "Bad login and/or password. Please retry.";

@Persist
private boolean error = false;

@ApplicationState
private String login;

@Inject
@Service("userManager")
private UserManager userManager;

private String password;

public String getLogin() {
return login;
}

@Validate("required")
public void setLogin(String login) {
this.login = login;
}

public String getPassword() {
return password;
}

public String getErrorMessage() {
String ret = null;
if (error) {
ret = BAD_CREDENTIALS;
}
return ret;
}

@Validate("required")
public void setPassword(String password) {
this.password = password;
}

String onSuccess() {
String ret = "Login";
error=true;
if (userManager.checkLogin(login, password)) {
error= false;
ret = "Home";
}
return ret;
}
}

On note :

- les annotations @Inject et @Service qui permettent d'injecter un bean service (Manager) Spring dans Tapestry. Une fois ceci fait, il est possible d'utiliser dans cette classe Java l'ensemble des services définis dans les managers Spring. Ici on modifie la méthode onSucces pour qu'au post du formulaire, l'application appelle le service Spring userManager afin d'aller exécuter la méthode checkLogin et vérifier que les login et password sont corrects. Dans le cas positif, on redirige vers la page Home. Dans le cas contraire, on positionne une variable d'erreur pour afficher un message à l'utilisateur ;

- l'annotation @Persist positionnée sur l'attribut error permet de faire persister ce champ à l'intérieur de cette page : lors d'un rechargement de la page, la valeur de ce champ sera préservée. Ceci permet, qu'en cas d'erreur, on soit capable, via la méthode getErrorMessage, d'afficher le message d'erreur à l'utilisateur. Attention : à la différence de l'annotation @ApplicationState, @Persist ne définit la persistance d'une propriété qu'à l'intérieur d'une même page et non entre deux pages différentes ;

- la méthode getErrorMessage permet de publier à la page Login.html, la propriété errorMessage afin d'afficher le message. Pour cela, modifier la page Login.html :

Intégration de Tapestry et Spring - modification de la page Login.html

<HTML xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Login</title>
</head>
<body>
<div id="login_box">
<t:form>
<t:errors />
<label style="color:red; font-weight:bold;">${errorMessage}</label>
<table>
<tr>
<td><label t:type="Label" for="login" class="login_label"/></td>
<td><input t:type="TextField" t:id="login" t:value="login" t:label="login " class="login_input" /></td>
</tr>
<tr>
<td><label t:type="Label" for="password" class="login_label"/></td>
<td><input t:type="PasswordField" t:id="password" t:value="password" t:label="password " class="login_input" /></td>
</tr>
<tr>
<td><input t:id="submitform" t:type="Submit" t:value="submit" class="login_submit"/></td>
</tr>
</table>
</t:form>
</div>
</body>
</HTML>

- Lancer enfin le serveur et entrer l'URL http://localhost:8080/BlankApplicationTapestry/login. Entrer un login et/ou password incorrect, on est redirigé vers la page de login et le message d'erreur attendu s'affiche :

Figure31 : Échec de la connexion

- Entrer ensuite le bon login/password ('test', 'test') et valider. On est bien redirigé vers la page Home :

Figure32 : Connexion

VIII - Mise en place du chargement retardé pour Hibernate

Comme on a vu lors de la création de la couche de persistance avec Hibernate, chaque entité BDD est mappée par un objet Java. On obtient ainsi un graphe complet d'objets représentant les entités et leurs dépendances. Ainsi une entité relationnelle E1 ayant une relation 1,n avec une entité E2 se traduira par un objet JavaBean O1 de type T1 contenant une propriété O2 de type collection de T2… et ainsi de suite.

On se rend compte facilement que la gestion des dépendances peut rapidement devenir très lourde dès lors que pour charger un objet persistant en mémoire on doit charger l'ensemble de ses dépendances et donc l'intégralité du graphe d'objet associé. Pour remédier à cela, Hibernate fonctionne en mode par défaut lazy. Le lazy loading ou chargement retardé signifie que lorsque l'on charge un objet en mémoire, seul cet objet est chargé sans ses dépendances ; le graphe de ses dépendances ne sera récupéré que lorsque l'on essaiera d'accéder explicitement à la propriété contenant la dépendance. L'inverse du lazy loading est le mode eager loading : lors du chargement d'un objet, on charge également l'ensemble des graphes d'objets dépendants de cet objet persistant. Contrairement au mode lazy, cela se fait de manière explicite lors du mapping de la relation (1,n ; n,1 ou n,n) par l'annotation : @OneToMany, @ManyToMany ou @ManyToOne(cascade = {}, fetch = FetchType.EAGER).

Cependant, le mode lazy est nativement limité au contexte transactionnel en cours. Cela signifie qu'on pourra, après avoir récupéré un objet persistant en mode lazy, récupérer l'ensemble de ses dépendances par un simple accès à la propriété dépendante à l'intérieur même de la transaction dans laquelle l'objet a été récupéré. Cela signifie également que dès lors que l'on sort de la transaction, cette opération n'est plus possible. En effet, l'opération de chargement retardé n'est possible que parce que la session Hibernate est toujours active et ouverte, afin que le moteur puisse y accéder pour récupérer le graphe demandé. Or la session est fermée au sortir de la transaction et cette opération n'est plus possible ; une tentative d'utilisation déclenchera une exception de type LazyInitializationException.

Il existe cependant une technique permettant de prolonger la durée de vie de la session Hibernate jusque dans la couche front, permettant ainsi de ne charger réellement que ce dont on a besoin. Attention cependant, cela ne signifie en aucun cas que l'on peut effectuer un chargement retardé n'importe quand dans la couche front. En effet, la session n'est gardée active que dans le cadre d'une requête ; cela signifie que l'on pourra effectuer un chargement retardé juste après avoir récupéré l'objet dans une méthode transactionnelle de la couche service mais que l'on ne pourra en aucun cas récupérer un objet, le mettre en session puis le récupérer ailleurs et tenter un chargement retardé par exemple.

- Étendre la durée de la session Hibernate est une opération que Spring permet très facilement par la mise à disposition d'un filtre dédié : OpenSessionInViewFilter. Pour cela il suffit de rajouter au web.xml :

Modification du webxml pour le chargement retardé Hibernate

<filter>
<filter-name>Hibernate Session In View Filter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Hibernate Session In View Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

Attention à bien configurer le mapping de ce filtre avant le mapping du filtre Tapestry, afin que toute requête transite prioritairement par celui-ci avant d'atteindre le filtre Tapestry. Dans le cas contraire, une exception sera générée. Grâce à ce code on étend la durée de vie de la session Hibernate pour toutes les URL, autorisant ainsi l'utilisation du lazy loading dans la couche front aux conditions décrites ci-dessus.

- Pour tester ce nouveau filtre nous allons créer une nouvelle table 'rights' qui va contenir les droits d'un user donné :

Création d'une table rights

CREATE TABLE `experiments`.`rights` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`label` VARCHAR(45) NOT NULL,
`id_user` INTEGER UNSIGNED NOT NULL,
PRIMARY KEY(`id`),
CONSTRAINT `FK_user` FOREIGN KEY `FK_user` (`id_user`)
REFERENCES `user` (`id_user`)
ON DELETE CASCADE
)

- On insère ensuite un jeu de test pour notre user :

Insertion des données

INSERT INTO rights (label, id_user) VALUES ('USER', 1);
INSERT INTO rights (label, id_user) VALUES ('ADMIN', 1);

- On doit ensuite mapper un nouvel objet Java dans la couche model (pour la procédure, voir le chapitre dédié au mapping Hibernate). On obtient ainsi les nouvelles classes User et Rights suivantes :

Modification de la classe User

package tuto.webssh.domain.model;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name = "user", catalog = "experiments")
public class User implements java.io.Serializable {

private static final long serialVersionUID = 1073256708139002061L;

private int idUser;
private String loginUser;
private String passwordUser;
private Set<Rights> rights = new HashSet<Rights>(0);

public User() {
}

public User(int idUser, String loginUser, String passwordUser) {
this.idUser = idUser;
this.loginUser = loginUser;
this.passwordUser = passwordUser;
}

public User(int idUser, String loginUser, String passwordUser,
Set<Rights> rights) {
this.idUser = idUser;
this.loginUser = loginUser;
this.passwordUser = passwordUser;
this.rights = rights;
}

@Id
@Column(name = "id_user", unique = true, nullable = false)
public int getIdUser() {
return this.idUser;
}

public void setIdUser(int idUser) {
this.idUser = idUser;
}

@Column(name = "login_user", nullable = false, length = 25)
public String getLoginUser() {
return this.loginUser;
}

public void setLoginUser(String loginUser) {
this.loginUser = loginUser;
}

@Column(name = "password_user", nullable = false, length = 25)
public String getPasswordUser() {
return this.passwordUser;
}

public void setPasswordUser(String passwordUser) {
this.passwordUser = passwordUser;
}

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user")
public Set<Rights> getRights() {
return this.rights;
}

public void setRights(Set<Rights> rights) {
this.rights = rights;
}

}

Nouvelle classe Rights

package tuto.webssh.domain.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "rights", catalog = "experiments")
public class Rights implements java.io.Serializable {

private static final long serialVersionUID = -8905167784828935704L;

private int id;
private User user;
private String label;

public Rights() {
}

public Rights(int id, User user, String label) {
this.id = id;
this.user = user;
this.label = label;
}

@Id
@Column(name = "id", unique = true, nullable = false)
public int getId() {
return this.id;
}

public void setId(int id) {
this.id = id;
}

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_user", nullable = false)
public User getUser() {
return this.user;
}

public void setUser(User user) {
this.user = user;
}

@Column(name = "label", nullable = false, length = 45)
public String getLabel() {
return this.label;
}

public void setLabel(String label) {
this.label = label;
}

}

On remarque que deux associations ont été créées : l'une pour accéder aux droits du user, l'autre pour accéder aux users ayant un droit. La déclaration de ces associations se fait par l'ajout de l'annotation @JoinColumn au lieu de la simple annotation @Column. Une autre annotation est ajoutée qui permet de configurer plus précisément l'annotation (respectivement @OneToMany et @ManyToOne) en en définissant la cardinalité. On remarque enfin les attributs de ces annotations et la présence de l'attribut fetch précisant le type LAZY ou EAGER du chargement. Enfin, sur l'association de type OneToMany on retiendra la présence de l'attribut mappedBy qui, dans ce genre de cas détermine quelle est la propriété de la classe associée qui définit l'association entre les deux classes : ici la propriété user de l'objet Rights définit la relation entre un objet User et un objet Rights.

- Ajouter la nouvelle classe mappée au mapping Hibernate :

Modification de hibernate.cfg.xml

<mapping class="tuto.webssh.domain.model.Rights"/>

Pour tester le lazy loading, nous allons afficher des informations détaillées sur le user dans la page Home.

- Modifier Home.java : nous ajoutons une propriété user qui sera remplie au chargement de la page par un appel au UserManager grâce à l'annotation @PageLoaded.

Modification de Home.java

package tuto.webssh.web.pages;

import javax.servlet.http.HttpSession;

import org.apache.tapestry.ComponentResources;
import org.apache.tapestry.Link;
import org.apache.tapestry.annotations.ApplicationState;
import org.apache.tapestry.annotations.Inject;
import org.apache.tapestry.annotations.OnEvent;
import org.apache.tapestry.annotations.PageLoaded;
import org.apache.tapestry.annotations.Persist;
import org.apache.tapestry.annotations.Service;
import org.apache.tapestry.services.RequestGlobals;

import tuto.webssh.domain.model.User;
import tuto.webssh.service.UserManager;

public class Home {

@Inject
private ComponentResources resources;

@Inject
private RequestGlobals requestGlobals;

@Inject
@Service("userManager")
private UserManager userManager;

@ApplicationState
private String login;

@Persist
private User user;

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}

@PageLoaded
public void onLoad() {
user = userManager.getUser(login);

}

@OnEvent(component = "logout")
public Link onLogout()
{
HttpSession session = requestGlobals.getHTTPServletRequest().getSession();
session.invalidate();
return resources.createPageLink("login", false);
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

}

- Modifier Home.html :

Modification de Home.html

<HTML xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Congratulation</title>
</head>
<body>
Congratulations, you are logged with login: ${login} !!<br/><br/>
Details of logged user are: ${user} <br/><br/>
<span id="LogoutLink"><span class="moduleTitle"><t:actionlink t:id="logout">Deconnexion</t:actionlink></span></span>
</body>
</html>

- Modifier User.java : nous allons surcharger la méthode toString qui sera automatiquement appelée par l'instruction $(user). Dans un premier temps nous n'afficherons pas l'objet associé Rights :

Modification de User

@Override
public String toString() {
return "User: [id: "+idUser+",login: "+loginUser+"]";
}

À l'exécution, on obtient l'écran suivant sans problème :

Figure33 : Connexion avec détails

On va maintenant essayer d'afficher le détail des droits pour un User.

- Modifier les fonctions toString des classes User et Rights :

Modification de User

@Override
public String toString() {
return "User: [id: "+idUser+",login: "+loginUser+", rights: "+rights+"]";
}

Modification de Rights

@Override
public String toString() {
return "Rights: [ id: "+id+", label: "+label+"]";
}

À l'exécution, on obtient l'erreur suivante :

Erreur de chargement retardé

org.apache.tapestry.ioc.internal.util.TapestryException: failed to lazily initialize a collection of role:
tuto.webssh.domain.model.User.rights, no session or session was closed

Cela peut surprendre puisque nous avons configuré le filter OpenSessionInView, on pouvait penser que l'on était capable, comme ici, de récupérer un objet (ici rights) inclus dans un autre (ici user) en retardé. Un point fondamental à comprendre dans la gestion du lazy : cette fonctionnalité est disponible uniquement juste derrière la fermeture de transaction. Dans le cas présent, lors du chargement de la page HTML, la session a été fermée depuis longtemps, à la fin de la méthode onLoad().

- Pour s'en rendre compte, modifions une dernière fois la page Home.java :

Modification de la page Home.java

@PageLoaded
public void onLoad() {
User tempUser = userManager.getUser(login);
System.out.println(tempUser);
user = tempUser;

}

Dans le code ci-dessus, le lazy loading est déclenché par le print forcé au retour de l'appel au userManager.

À l'exécution, on obtient l'écran suivant qui prouve que le lazy loading s'est correctement déroulé :

Figure34 : Exécution du lazy loading

Cet exemple montre l'utilisation et les limitations du lazy loading. Il faut bien garder à l'esprit que la fonctionnalité n'est disponible que dans le cadre de la méthode appelante la méthode transactionnelle et pas plus loin. On peut objecter que dans ce cas le lazy loading ne sert à rien… je le concède volontiers d'autant que l'objet associé étant très léger, il n'y a aucun intérêt à définir l'association en LAZY, on aurait pu la définir en EAGER.

Cependant, dans de nombreux cas, il est très pratique - notamment lors de la manipulation de listes à gros volumes. Ceci dit, dans le cas présent, la définition de l'association en EAGER nous aurait obligés à définir une nouvelle méthode dans les couches Service et DAO pour récupérer les droits d'un user donné.

En conclusion, le chargement retardé peut s'avérer très pratique et très puissant, mais est à utiliser avec précautions compte tenu des erreurs fréquentes qu'il peut générer. Cependant, lorsque le principe est compris, les bénéfices sont indéniables.

IX - Conclusion

On a donc à présent une application blanche implémentant une forme simpliste de login grâce au trio Tapestry/Spring/Hibernate.

Ce tutoriel nous a permis de nous familiariser avec les bases de chacun de ces trois frameworks. Évidemment, leur portée et leur puissance sont bien supérieures à ce qui a pu être évoqué dans le cadre de ce tutoriel. Il est à noter également qu'un certain travail reste à faire pour disposer d'un réel environnement de production - essentiellement au niveau d'Hibernate : mise en place d'un vrai pool de connexion (c3p0 par exemple), d'une solution de cache de premier et de second niveau (ehcache, etc.), etc.

Globalement on peut cependant s'apercevoir, suite à ce tutoriel, que la mise en place de ces trois frameworks n'est pas extraordinairement complexe. En outre, il est certain que l'utilisation de Spring et Hibernate permettra un gain de temps très important de par l'ensemble des problématiques qu'ils adressent de manière interne, permettant ainsi aux développeurs de s'en décharger (mapping O/R, accès BDD, gestion des Transactions, cycle de vie des objets, ThreadSafe, etc.). En outre, il y a fort à parier que leur utilisation rende le code de l'application plus robuste - Spring et Hibernate addressant, sans doute, bien davantage de problèmes que ne peut le faire une équipe projet spécialisée dans son domaine métier.

Enfin, il est indiscutable que ces frameworks apportent, de par leur architecture propre, une structuration forte de l'architecture logicielle, une séparation des préoccupations (Technique vs Métier) des applications qui les utilisent et contribuent ainsi à leur robustesse, leur maintenabilité et leur évolutivité. Si l'on ajoute que l'ensemble de ces frameworks sont open source et supportés par de très grandes et très actives communautés (Spring, Hibernate/JBoss, Apache), il me semble qu'il ne peut être qu'extrêmement profitable d'en user largement.

X - Remerciements

Merci à Ricky81, Hikage et aznowiczpour leur aide, leurs conseils et leur relecture technique.
Merci à Dut pour la relecture orthographique.

Ce document a été publié avec l'autorisation de la société qui m'emploie : Atos Worldline.

XI - Dans la même série…

Ce tutoriel est le premier d'une série de trois. Accéder aux autres tutoriels de cette série :

- Premier volet : Premier projet avec Tapestry5, Spring et Hibernate ;

- Deuxième volet : Intégration simple et élégante d'AJAX avec DWR ;

- Troisième volet : Sécuriser vos applications Web avec Spring acegi security -> En Relecture.