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 :
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 :
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.
- renseignez le nom du projet et choisissez un Target Runtime. Si rien n'est défini, cliquez sur new.
- choisissez un runtime (ici tomcat 5.5). Attention, il est nécessaire d'avoir installé le serveur.
- renseignez les informations demandées et cliquez sur Finish.
- de retour à l'interface de la Figure2, cliquez sur Next puis encore sur Next.
- 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.
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.
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 :
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.
V-C. Configuration de la connexion▲
- Créer un répertoire config et l'inclure en tant que source folder.
- Sélectionner le répertoire config et cliquer sur File/New/Other. Puis choisir Hibernate/Hibernate configuration file.
- Configurer la connexion à la BDD en fonction des paramètres locaux. Penser à bien cocher la case 'Create Console configuration'
- 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 :
- Cliquer sur Classpath/Add JAR et ajouter le driver BDD puis cliquer sur Finish.
Le fichier de configuration Hibernate a été créé comme ceci :
<?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.
V-D. Génération de la couche model et du mapping O/R▲
- Lancer la génération de code automatique :
- Configurer la génération de code comme ceci :
- Cliquer sur Exporters et cocher uniquement la génération de Domain code. Cliquer sur Run.
- Le code de la classe User a été généré avec l'ensemble des annotations nécessaires au mapping Hibernate.
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.
<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
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
- 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.
Voici les interfaces et classes créées. On en profite pour ajouter tout de suite le code dont on aura besoin par la suite :
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);
}
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.
<?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.
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);
}
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.
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 :
<?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 :
<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 :
<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 :
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 :
<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 :
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.
# 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 :
- Créer un fichier nommé Login.html sous WEB-INF :
<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 :
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
<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 :
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.
<?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 :
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.
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.
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.
<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 :
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 :
<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 :
- Entrer ensuite le bon login/password ('test', 'test') et valider. On est bien redirigé vers la page Home :
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 :
<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é :
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 :
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 :
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 =
1073256708139002061
L;
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;
}
}
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 =
-
8905167784828935704
L;
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 :
<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.
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 :
<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 :
@Override
public
String toString
(
) {
return
"User: [id: "
+
idUser+
",login: "
+
loginUser+
"]"
;
}
À l'exécution, on obtient l'écran suivant sans problème :
On va maintenant essayer d'afficher le détail des droits pour un User.
- Modifier les fonctions toString des classes User et Rights :
@Override
public
String toString
(
) {
return
"User: [id: "
+
idUser+
",login: "
+
loginUser+
", rights: "
+
rights+
"]"
;
}
@Override
public
String toString
(
) {
return
"Rights: [ id: "
+
id+
", label: "
+
label+
"]"
;
}
À l'exécution, on obtient l'erreur suivante :
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 :
@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é :
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.