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].
<?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
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].
<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.
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 :
package service; import java.util.List; import model.IModelExample; public interface IServiceExample { List<IModelExample> searchMessages(); }
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.
package dao; import java.util.List; import model.IModelExample; import model.impl.ModelExample; public interface IDaoExample { List<IModelExample> searchMessages(); }
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.
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(); }
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
.
package model; public interface IModelExample { String getMessage(); void setMessage(String message); }
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.