Stripes + Spring + JPA

Suivre des bonnes pratiques nous aidera à créer des applications plus rapides, faciles à maintenir en laissant des possibilités d'évolution imprévues dans le futur. Le but de l'application exemple minimal qui suit est une introduction pour construire des applications évolutives utilisant Stripes, Spring et JPA avec un minimum de configuration.

Stripes est un framework de présentation pour construire des applications web en utilisant les technologies Java les plus récentes. Spring nous aide à créer et à gérer des objets business réutilisables, ainsi que des objets de data-access (DAO) qui ne sont pas liés aux services spécifiques Java EE. Les objets Spring peuvent être réutilisés dans tout environnement Java EE (Web ou EJB), applications autonomes et environnements de test. JPA, un standard Java qui fait partie de la spécification EJB 3.0, nous aide à implémenter une couche de persistance neutre à l'égard des fournisseurs, ce qui nous permet de basculer entre les fournisseurs de persistance tels qu'Hibernate, Toplink, iBatis ou OpenJPA.

Spring et JPA encouragent tout les deux l'utilisation d'une architecture disposée de plusieurs couches qui aide à réduire la complexité du développement des applications Java EE, et, avec Stripes dans le coup (et des annotations Java 5), nous sommes maintenant capables de construire des applications Java EE à grande échelle avec un minimum de configuration pour un maximum de productivité.

L'application exemple consiste en l'organisation des fichiers suivants (excluant le répertoire META-INF) :

 
index.jsp 
/WEB-INF 
|-> applicationContext.xml 
|-> web.xml 
|-> /lib 
| |-> commons-logging.jar 
| |-> cos.jar * 
| |-> log4j.jar * 
| |-> openjpa.jar * 
| |-> persistence.jar 
| |-> spring.jar 
| |-> stripes.jar 
|-> /classes 
| |-> StripesResources.properties 
| |-> /action 
| | |-> ActionExample.class 
| | |-> BaseActionBean.class 
| |-> /dao 
| | |-> /impl 
| | | |-> Dao.class 
| | | |-> DaoExample.class 
| | |-> IDao.class 
| | |-> IDaoExample.class 
| |-> /model 
| | |-> /impl 
| | | |-> ModelExample.class 
| | |-> IModelExample.class 
| |-> /service 
| | |-> /impl 
| | | |-> ServiceExample.class 
| | |-> IServiceExample.class 

* Les fichiers jar du répertoire lib avec un astérisque peuvent être remplacés par d'autres implémentations. Le reste des jars est requis pour que l'application exemple fonctionne (sauf spring.jar qui contient plus que de nécessaire). L'utilité de cos.jar et log4j.jar est expliquée sur la page Guide De Démarrage Rapide. openjpa.jar est juste une implémentation du JPA dans un seul jar.

Configuration Stripes et Spring

Nous commençons dans le fichier web.xml par la configuration du ContextLoaderListener [1] et contextConfigLocation [2] de Spring. Ensuite nous configurons le filtre Stripes pour scanner le répertoire action de nos ActionBean [3] et pour utiliser le SpringInterceptor [4]. Le reste est de la configuration web.xml classique qui demande à Stripes d'intercepter des requêtes *.action et *.jsp, avant de terminer avec un fichier d'accueil index.jsp [5].

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

<listener> 
<listener-class> <!-- [1] --> 
org.springframework.web.context.ContextLoaderListener 
</listener-class> 
</listener> 

<context-param> <!-- [2] --> 
<param-name>contextConfigLocation</param-name> 
<param-value>/WEB-INF/applicationContext.xml</param-value> 
</context-param> 

<filter> 
<display-name>Stripes Filter</display-name> 
<filter-name>StripesFilter</filter-name> 
<filter-class> 
net.sourceforge.stripes.controller.StripesFilter 
</filter-class> 
<init-param> <!-- [3] --> 
<param-name>ActionResolver.Packages</param-name> 
<param-value>action</param-value> 
</init-param> 
<init-param> <!-- [4] --> 
<param-name>Interceptor.Classes</param-name> 
<param-value> 
net.sourceforge.stripes.integration.spring.SpringInterceptor 
</param-value> 
</init-param> 
</filter> 

<filter-mapping> 
<filter-name>StripesFilter</filter-name> 
<url-pattern>*.jsp</url-pattern> 
<dispatcher>REQUEST</dispatcher> 
</filter-mapping> 

<filter-mapping> 
<filter-name>StripesFilter</filter-name> 
<servlet-name>StripesDispatcher</servlet-name> 
<dispatcher>REQUEST</dispatcher> 
</filter-mapping> 

<servlet> 
<servlet-name>StripesDispatcher</servlet-name> 
<servlet-class> 
net.sourceforge.stripes.controller.DispatcherServlet 
</servlet-class> 
<load-on-startup>1</load-on-startup> 
</servlet> 

<servlet-mapping> 
<servlet-name>StripesDispatcher</servlet-name> 
<url-pattern>*.action</url-pattern> 
</servlet-mapping> 

<welcome-file-list><!-- [5] --> 
<welcome-file>index.jsp</welcome-file> 
</welcome-file-list> 
</web-app> 

Références de Configuration

Pour plus d'informations consultez les références de configuration appropriées pour Stripes et Spring.

Configuration Spring

Dans le fichier applicationContext.xml spécifié par le paramètre de contexte contextConfigLocation, nous demandons tout simplement à Spring de scanner deux de nos répertoires où se trouvent nos objets business réutilisables [1] et nos objets data-access (DAO) [2].

"applicationContext.xml"
 
<beans xmlns="http://www.springframework.org/schema/beans" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:context="http://www.springframework.org/schema/context" 
xsi:schemaLocation=" 
http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 
http://www.springframework.org/schema/context 
http://www.springframework.org/schema/context/spring-context-2.5.xsd"> 
<context:component-scan base-package="service" /> <!-- [1] -->
<context:component-scan base-package="dao" /> <!-- [2] --> 
</beans> 

La JSP

Nous utilisons une JSP minimale afin de soumettre un formulaire à notre ActionBean qui s'appelle ActionExample. Lors de la soumission du formulaire nous serons encore forwardés vers cette JSP et une liste de messages apparaîtra.

 
<%@taglib prefix="stripes" 
uri="http://stripes.sourceforge.net/stripes.tld" %> 
<html><head><title>TestAction</title></head><body> 
<stripes:form beanclass="action.ActionExample"> 
Messages : ${actionBean.messages}<br /> 
<stripes:submit name="searchNumbers" value="Go !" /> 
</stripes:form> 
</body></html> 

Ce guide n'étant pas une 'bonne pratique' pour l'utilisation de balises Stripes, nous avons spécifié la valeur de la balise <stripes:submit /> avec l'attribut value. Il aurait mieux valu que nous utilisions un ResourceBundle, tout comme nous aurions dû spécifier un doctype etc, etc.

L'ActionBean

Ensuite nous avons la classe ActionExample qui, bien qu'elle étende notre propre classe BaseActionBean, n'a besoin que d'implémenter l'interface ActionBean pour devenir un ActionBean Stripes. Le fait d'utiliser notre propre classe mère nous permet de cacher toute personnalisation à l'ActionBeanContext qu'on pourrait faire, comme cette personnalisation qui facilite la gestion d'état des objets et les tests unitaires.

 
package action; 

import java.util.List; 

import model.IModelExample; 
import net.sourceforge.stripes.action.DefaultHandler; 
import net.sourceforge.stripes.action.ForwardResolution; 
import net.sourceforge.stripes.action.HandlesEvent; 
import net.sourceforge.stripes.action.Resolution; 
import net.sourceforge.stripes.integration.spring.SpringBean; 
import service.IServiceExample; 

public class ActionExample extends BaseActionBean { 
@SpringBean 
private IServiceExample serviceExample; 
private List<IModelExample> modelExamples; 

@DefaultHandler 
public Resolution welcome() { 
return new ForwardResolution("/index.jsp"); 
} 

@HandlesEvent(value="searchNumbers") 
public Resolution searchNumbers() { 
modelExamples = serviceExample.searchMessages(); 
return new ForwardResolution("/index.jsp"); 
} 

public List<IModelExample> getMessages() { 
return modelExamples; 
} 

protected IServiceExample getServiceExample() { 
return serviceExample; 
} 

protected void setServiceExample(IServiceExample serviceExample) { 
this.serviceExample = serviceExample; 
} 
} 

La meilleure partie de l'exemple ci-dessus est l'annotation @SpringBean de Stripes. Stripes utilise le SpringInterceptor afin d'injecter les beans Spring dans des ActionBeans après que l'ActionBean soit instancié. Pour une information plus spécifique consultez Stripes avec Spring.

Le reste de l'ActionBean est du code classique qui est aussi expliqué dans le Guide De Démarrage Rapide de Stripes.

"action.BaseActionBean.java"
 
package action; 

import net.sourceforge.stripes.action.ActionBean; 
import net.sourceforge.stripes.action.ActionBeanContext; 

public class BaseActionBean implements ActionBean { 
private ActionBeanContext context; 

public ActionBeanContext getContext() { 
return context; 
} 

public void setContext(ActionBeanContext context) { 
this.context = context; 
} 
} 

ActionBeanContext Personnalisé

Pour plus d'exemples de bonnes pratiques consultez le guide State Management qui nous permet d'abstraire la façon dont on stocke les objets dans l'HttpSession qui ensuite facilite les tests unitaires de nos ActionBeans.

La Couche Service

Venons-en maintenant à l'interface et la classe d'exemple service :

"service.IServiceExample.java"
 
package service; 

import java.util.List; 

import model.IModelExample; 

public interface IServiceExample { 
List<IModelExample> searchMessages(); 
} 
"service.impl.ServiceExample.java"
 
package service.impl; 

import java.util.List; 

import model.IModelExample; 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; 

import service.IServiceExample; 

import dao.IDaoExample; 

@Service 
public class ServiceExample implements IServiceExample { 
@Autowired 
private IDaoExample daoExample; 

public List<IModelExample> searchMessages() { 
return daoExample.searchMessages(); 
} 

public IDaoExample getDaoExample() { 
return daoExample; 
} 

public void setDaoExample(IDaoExample daoExample) { 
this.daoExample = daoExample; 
} 
} 

Dans l'exemple ci-dessus nous utilisons deux annotations stereotype de Spring. L'annotation @Service nous permet de déclarer un objet business réutilisable géré par Spring, que l'on injecte dans nos ActionBeans avec l'annotation @SpringBean de Stripes. L'annotation @Autowired nous permet d'injecter notre DAO géré par Spring, que nous détaillerons plus bas.

Bonnes Pratiques

L'annotation @Service de Spring nous permet, de manière transparente, d'utiliser la gestion des transactions déclarative avec un peu plus de configuration Spring.

La Couche DAO

Au lieu de re-coder les mêmes mécanismes DAO basiques, encore et encore, nous utilisons une interface générique (et générifié (générique du sens Java 5)) qui cache les détails du mécanisme de persistance en dessous. Ceci nous laisse non seulement nous concentrer sur du code spécifique pour nos besoins métier, mais nous permet également de tester les couches services et DAO plus aisément avec des objets farceurs (mock) dynamiques.

"dao.IDaoExample.java"
 
package dao; 

import java.util.List; 

import model.IModelExample; 
import model.impl.ModelExample; 

public interface IDaoExample { 
List<IModelExample> searchMessages(); 
} 
"dao.impl.DaoExample.java"
 
package dao.impl; 

import java.util.Arrays; 
import java.util.List; 

import model.IModelExample; 
import model.impl.ModelExample; 

import org.springframework.stereotype.Repository; 

import dao.IDaoExample; 

@Repository 
public class DaoExample extends Dao<ModelExample, Long> 
implements IDaoExample { 
public List<IModelExample> searchMessages() { 
IModelExample modelSpring = new ModelExample(); 
modelSpring.setMessage("Spring"); 

IModelExample modelJPA = new ModelExample(); 
modelJPA.setMessage("JPA"); 

return Arrays.asList(modelSpring, modelJPA); 
} 
} 

Afin que l'application reste simple, nous ne nous servirons pas de notre classe mère DAO générifiée. Montrer comment faire pour lier le tout est facile si on ne s'occupe pas des détails de la configuration d'une DataSource ou de l'EntityManagerFactory. Par exemple, la méthode searchMessages() ci-dessus aurait pu être codée comme ce qui suit :

 
public List<IModelExample> searchMessages() { 
return findAll(); 
} 

Sinon, la couche service aurait pu être codée avec return daoExample.findAll(). La méthode findAll() est détaillée plus bas avec la classe et l'interface mère DAO générique générifiée.

Plus de Bonnes Pratiques

L'annotation @Repository de Spring nous permet, d'une manière transparente, d'uniformiser la gestion des exceptions afin de nous éviter de lier inutilement notre application à une stratégie d'implémentation d'exceptions spécifique à un fournisseur.

"dao.IDao.java"
 
package dao; 

public interface IDao<T, PK> { 
void persist(T entity); 

void remove(T entity); 

void merge(T entity); 

T find(PK id); 

List<T> findAll(); 
} 
"dao.impl.Dao.java"
 
package dao.impl; 

import java.lang.reflect.ParameterizedType; 
import java.util.List; 

import javax.persistence.EntityManager; 
import javax.persistence.PersistenceContext; 
import javax.persistence.Query; 

import dao.IDao; 

public abstract class Dao<T, PK> implements IDao<T, PK> { 
protected Class<T> entityType; 

//@PersistenceContext 
protected EntityManager entityManager; 

@SuppressWarnings("unchecked") 
public Dao() { 
ParameterizedType genericSuperclass = 
(ParameterizedType) getClass().getGenericSuperclass(); 
this.entityType = (Class<T>) genericSuperclass 
.getActualTypeArguments()[0]; 
} 

public T find(PK id) { 
return entityManager.find(entityType, id); 
} 

public void persist(T entity) { 
entityManager.persist(entity); 
} 

public void remove(T entity) { 
entityManager.remove(entity); 
} 

public void merge(T entity) { 
entityManager.merge(entity); 
} 

@SuppressWarnings("unchecked") 
public List<T> findAll() { 
String all = "select x from " + 
entityType.getSimpleName() + " x"; 
Query query = entityManager.createQuery(all); 
return query.getResultList(); 
} 
} 

L'implémentation du DAO générifié ci-dessus est loin d'être exhaustive, cependant c'est un bon début. L'annotation @PersistenceContext a été mise en commentaire pour ce guide, ce qui empêche Spring de lancer une exception quand il ne retrouve pas un EntityMangerFactory configuré. Ceci limite notre configuration Spring au minimum.

Le Modèle

L'exemple modèle n'est qu'une enveloppe autour d'un simple message. Si nous voulons le persister, il faut une table qui s'appelle ModelExample qui contient deux colonnes (un integer (d'une taille suffisante pour un Long) et un varchar), nommées respectivement id et message.

"model.IModelExample.java"
 
package model; 

public interface IModelExample { 
String getMessage(); 

void setMessage(String message); 
} 
"model.impl.ModelExample.java"
 
package model.impl; 

import javax.persistence.Entity; 
import javax.persistence.Id; 

import model.IModelExample; 

@Entity 
@Table 
public class ModelExample implements IModelExample { 
@Id 
private Long id; 
@Column 
private String message; 

public Long getId() { 
return id; 
} 

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

public String getMessage() { 
return message; 
} 

public void setMessage(String message) { 
this.message = message; 
} 

@Override public String toString() { 
return getMessage(); 
} 
} 

L'annotation @Entity de JPA désigne une classe comme entité persistante. Les annotations @Table, @Id et @Column décrivent le "où" et le "comment" persister la data contenue dans la classe.

Référence JPA

Pour une information plus détaillée à propos des annotations JPA, consultez la Section JPA du Tutoriel Java EE 5.

Notez qu'il n'y a pas d'annotations @GeneratedValue ou @SequenceGenerator à côté de l'annotation @Id. Bien que les gens qui codent avec Stripes adorent les annotations, spécifier les stratégies de génération de séquences dans un orm.xml externe via le fichier persistence.xml aidera l'application à évoluer dans le temps. Vu que les évolutions imprévues n'arrivent que trop souvent, c'est mieux de séparer ce genre d'informations de nos détails de persistance. Comme ça, un changement de base de données ou de la stratégie de génération de séquences laissera notre modèle inchangé.

Vous noterez aussi que nous n'avons pas parlé du fichier persistence.xml ou orm.xml. En effet nous ne voulons pas proposer une certaine manière de configurer JPA en tant que 'bonne pratique', nous voulons aussi réduire ce guide à un minimum en termes de configuration. Par contre, si vous voulez voir 3 façons de configurer JPA dans un environnement Spring, jetez un coup d'œil à ceci.

L'application présentée ci-dessus compilera et s'exécutera avec succès en utilisant des beans, gérés par Spring, injectés dans nos ActionBeans de Stripes. L'étape suivante serait de configurer un EntityManagerFactory JPA avec une implémentation puis choisir/créer une base de données dans laquelle on peut persister le modèle.