Anda di halaman 1dari 32

Le mod`ele M.V.C.

de Spring
1

Introduction

Le framework Spring1 est une boite `a outils tr`es riche permettant de structurer, dameliorer et de simplifier
lecriture dapplication JEE. Spring est organise en module
-

Gestion des instances de classes (JavaBean et/ou metier),


Programmation orientee Aspect,
Mod`ele MVC et outils pour les application WEB,
Outils pour la DAO (JDBC),
Outils pour les ORM (Hibernate, iBatis, JPA, ...),
Outils pour les applications JEE (EJB, JTA, Servlet, JSP, ...),

Plus dinformations ici2 .

Pr
ealables

Preparez un environement pour tester une application WEB basee sur Spring-MVC :
Telechargez TomEE Plus3 (en local4 ) et decompressez larchive.
Preparez, dans Eclipse, une application WEB basee sur le conteneur TomEE (utilisez ladaptateur WTP pour
Tomcat en version 7).
Testez une page index.jsp tr`es simple.
<html>
<head><title>Example :: Spring Application</title></head>
<body>
<h1>Example - Spring Application</h1>
<p>This is my test.</p>
</body>
</html>

Verifiez que cette page est accessible. Faites quelques modifications mineures et testez le resultat. Parcourez
les traces de demarrage du serveur TomEE.
Preparez un onglet vers la documentation5 .
Preparez un onglet vers la Javadoc6 .

Mise en place de Spring MVC

3.1

Un premier contr
oleur

Telechargez Spring 3.2.57 et ajoutez les archives .jar `a votre repertoire WEB-INF/lib. Faites de meme pour
les librairies qui se trouvent dans le repertoire libs-utiles-pour-TP-Spring-MVC8 .
1

http ://www.springframework.org/
http ://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/html/overview.html
3
http://tomee.apache.org/
4
ress-spring/
5
http ://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/html/index.html
6
http ://docs.spring.io/spring/docs/3.2.5.RELEASE/javadoc-api/index.html
7
ress-spring/
8
ress-spring/libs-utiles-pour-TP-Spring-MVC/
2

Preparez le fichier WEB-INF/web.xml suivant :

<?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"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/jav
id="WebApp_ID" version="3.0">
<display-name>test-mvc</display-name>

<servlet>
<servlet-name>springapp</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springapp</servlet-name>
<url-pattern>*.htm</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>

Cette configuration va mettre en place une servlet generique qui va se charger du traitement des requetes (URL qui
se terminent par .htm). Pour configurer cette servlet, nous allons creer le fichier WEB-INF/springapp-servlet.xml :
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.spri
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">
<bean name="/hello.htm" class="springapp.web.HelloController" />
</beans>

Ce fichier de configuration Spring va creer un bean et lui donner un nom (/hello.htm). Cest notre premier
controleur. Creez cette classe `a partir du code ci-dessous :

package springapp.web;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import
import
import
import

org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
org.springframework.web.servlet.ModelAndView;
org.springframework.web.servlet.mvc.Controller;

public class HelloController implements Controller {


protected final Log logger = LogFactory.getLog(getClass());
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
logger.info("Returning hello view");
return new ModelAndView("hello.jsp");
}
}

A cette etape, vous devez trouver les traces du serveur la creation de ce controleur.
Essayez dutiliser le controleur hello.htm
Visiblement ce controleur se termine en donnant la main `a une page JSP (hello.jsp) destinee `a fabriquer la
reponse `a envoyer au client. Creons cette page :
<html>
<head><title>Hello :: Spring Application</title></head>
<body>
<h1>Hello - Spring Application</h1>
</body>
</html>

Etudiez
la classe ModelAndView. Cette classe est le coeur du mod`ele MVC de Spring : Elle permet de separer
dune part, les controleur qui travaillent sur la requete et dautres part les vues (pages JSP) qui se chargent du
resultat. Entre les deux, les instances de ModelAndView transportent `a la fois le nom de la vue et les donnees
qui seront affiches par cette vue (cest-`a-dire le mod`
ele).

3.2

Am
eliorer les vues

Toutes les vues dune application partagent souvent de nombreuses declarations. Nous allons les regrouper dans
une page JSP. Preparez la page WEB-INF/jsp/include.jsp suivante :
<%@ page session="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

Nous pouvons maintenant revoir notre page daccueil (fichier index.jsp)) en forcant lutilisateur `a utiliser le
controleur :
3

<%@ include file="/WEB-INF/jsp/include.jsp" %>


<%-- rediriger le contr^
oleur hello --%>
<c:redirect url="/hello.htm"/>

Testez le bon fonctionnement de cette redirection.


Pour linstant la page hello.jsp est accessible au client dans la mesure ou le client connait le nom de cette
page. Dans le mod`ele MVC, les requetes doivent toujours etre traitees par le controleur et non pas, directement,
par une page JSP. Pour eviter que la page hello.jsp ne soit accessible, nous allons la modifier et la deplacer
dans WEB-INF/jsp/hello.jsp :
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<html>
<head><title>Hello :: Spring Application</title></head>
<body>
<h1>Hello - Spring Application</h1>
<p>Greetings, it is now <c:out value="${now}" default="None" /></p>
</body>
</html>

Bien entendu, nous devons modifier le controleur en consequence :


package springapp.web;
import java.io.IOException;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import
import
import
import

org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
org.springframework.web.servlet.ModelAndView;
org.springframework.web.servlet.mvc.Controller;

public class HelloController implements Controller {


protected final Log logger = LogFactory.getLog(getClass());
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String now = (new Date()).toString();
logger.info("Returning hello view with " + now);
return new ModelAndView("WEB-INF/jsp/hello.jsp", "now", now);
}
}

Nous avons profite de cette mise `a jour pour introduire une nouveaut
e : le controleur fabrique maintenant une
donnee et transmet cette donnee `a la page JSP qui se charge de lafficher (la date).
Exercice : preparez une nouvelle donnee (par exemple un message), passez cette donnee `a la page hello.jsp
et assurez sa presentation.
4

3.3

D
ecoupler les vues et les contr
oleurs

Pour linstant, le chemin dacc`es complet `a la vue figure dans le controleur. Pour eviter ce couplage fort, nous
allons creez un service spring qui va se charger de retrouver les vues `a partir dun simple nom. Ajoutez au fichier
WEB-INF/springapp-servlet.xml le code :
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView"></property>
<property name="prefix" value="/WEB-INF/jsp/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

dote de ce service de resolution des noms de vue, le code du controleur va devenir :


package springapp.web;
import java.io.IOException;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import
import
import
import

org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
org.springframework.web.servlet.ModelAndView;
org.springframework.web.servlet.mvc.Controller;

public class HelloController implements Controller {


protected final Log logger = LogFactory.getLog(getClass());
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
String now = (new Date()).toString();
logger.info("Returning hello view with " + now);
return new ModelAndView("hello", "now", now);
}
}

Exercice : A ce stade, vous pouvez faire un classe de test unitaire pour verifier que votre controleur est bien
correct.

Utiliser les annotations

Pour linstant, nous avons fait du MVC Spring `a lancienne (interface, implantation et fichier de configuration
XML). Nous allons revoir ce processus en utilisant les annotations java.
Commencez par ajouter dans le fichier WEB-INF/springapp-servlet.xml la clause ci-dessous (avant la
creation du bean /hello.htm). Elle permet dindiquer `a la servlet quelle doit explorer les classes (des packages
listes) et exploiter les annotations.

<context:component-scan base-package="springapp.web" />


<context:component-scan base-package="springapp.business" />

Nous allons ensuite enrichier la correspondance URL/Controleurs en ajoutant la clause ci-dessous au fichier
WEBINF/web.xml :
<servlet-mapping>
<servlet-name>springapp</servlet-name>
<url-pattern>/actions/*</url-pattern>
</servlet-mapping>

Nous pouvons maintenant definir un nouveau controleur :


package springapp.web;
import java.util.Date;
import
import
import
import
import
import

org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
org.springframework.stereotype.Controller;
org.springframework.web.bind.annotation.RequestMapping;
org.springframework.web.bind.annotation.RequestMethod;
org.springframework.web.servlet.ModelAndView;

@Controller()
@RequestMapping("/tests")
public class HelloAnnoController {
protected final Log logger = LogFactory.getLog(getClass());
@RequestMapping(value = "/welcome", method = RequestMethod.GET)
public ModelAndView sayHello() {
String now = (new Date()).toString();
logger.info("Running " + this);
return new ModelAndView("hello", "now", now);
}
}

Exercices :
Verifiez dans les traces du serveur que ces controleurs sont bien detectes par Spring.
Testez ce controleur avec une URL du type
http://localhost:8080/votre-application/actions/tests/welcome

Lisez la documention de lannotation @Controller.


Un controleur est maintenant une methode qui
- renvoie une instance de ModelAndView ou le nom dune vue (String),
- accepte en argument une instance de HttpServletRequest et/ou HttpServletRequest et/ou
HttpSession et bien dautres choses.
Creez un nouveau controleur qui va stocker un compteur en session, le faire evoluer et assurer son affichage.
Faites en sorte que ce controleur traite egalement un arguement de la requete HTTP.
6

4.1

D
eclarer les param`
etres

Nous pouvons egalement utiliser lannotation @RequestParam pour recuperer, sous la forme dun param`etre de
la methode, les param`etres de la requete HTTP. En voici un exemple :
@RequestMapping(value = "/plus10", method = RequestMethod.GET)
public ModelAndView plus10(
@RequestParam(value = "num", defaultValue = "100") Integer value) {
logger.info("Running plus10 controler with param = " + value);
return new ModelAndView("hello", "now", value + 10);
}

Exercices :
Testez ce controleur en lui fournissant le param`etre attendu. Testez egalement les cas derreur (param`etre
absent ou incorrect).
Ajoutez un nouveau param`etre de type Date et utilisez lannotation @DateTimeFormat pour recuperer ce
param`etre.

4.2

Utiliser de belles adresses

Il est maintenant habituel de placer des param`etres `a linterieur des adresses WEB. cela permet davoir des URL
simples, faciles `a construire et faciles `a memoriser. En voila un exemple :
@RequestMapping(value = "/voir/{param}", method = RequestMethod.GET)
public ModelAndView voir(@PathVariable("param") Integer param) {
logger.info("Running param controler with param=" + param);
return new ModelAndView("hello", "now", param);
}

Exercices :
Testez ce controleur.
Modifiez ce controleur pour avoir plusieurs param`etres dans la meme adresse.
Utilisez le mecanisme des expressions reguli`eres pour traiter une adresse composee (inspirez de la documention sur lannotation @PathVariable).
Terminez en testant lannotation @MatrixVariable pour traiter des URL de la forme (a et b ne sont pas
des param`etres) :
/une/action;a=10;b=20

Le traitement des formulaires

Pour introduire la traitement des formulaires nous allons proceder en trois etapes :
definition dun mod`ele et dune couche metier,
creation du formulaire,
validation des donnees,
7

5.1

D
efinir une couche m
etier

Considerons le POJO (Plain Old java Object) suivant :


package springapp.model;
public class Product {
private
private
private
private
private

Integer number;
String name;
Double price;
String description;
String type;

public Integer getNumber() {


return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

Nous allons lui adjoindre un service metier defini par linterface ci-dessous :

package springapp.business;
import java.util.Collection;
import springapp.model.Product;
public interface ProductManager {
Collection<Product> findAll();
void save(Product p);
Product find(int number);
}

pour laquelle nous definissons une premi`ere implantation :

package springapp.business;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
import springapp.model.Product;
@Service("productManager")
public class InMemoryProductManager implements ProductManager {
final Map<Integer, Product> products;
int maxId = 0;
public InMemoryProductManager() {
this.products = new HashMap<Integer, Product>();
Product p1 = new Product();
p1.setNumber(100);
p1.setName("Car");
p1.setPrice(2000.0);
p1.setDescription("Small car");
products.put(p1.getNumber(), p1);
Product p2 = new Product();
p2.setNumber(200);
p2.setName("Gift");
p2.setPrice(100.0);
p2.setDescription("Big gift");
products.put(p2.getNumber(), p2);
maxId = 300;
}
@Override
public Collection<Product> findAll() {
return products.values();
}
@Override
public void save(Product p) {
if (p.getNumber() == null) {
p.setNumber(maxId++);
}
products.put(p.getNumber(), p);
}
@Override
public Product find(int number) {
Product p = products.get(number);
if (p == null) {
throw new IllegalArgumentException("no product " + number);
}
return p;
}
}

5.2

Lister les produits

Nous pouvons maintenant mettre en place un controleur qui va gerer toutes les actions sur les produits (listage,
creation, modification et suppression).

10

Commencons par lister les produits disponibles :


package springapp.web;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import
import
import
import
import
import
import

org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
org.springframework.beans.factory.annotation.Autowired;
org.springframework.stereotype.Controller;
org.springframework.web.bind.annotation.RequestMapping;
org.springframework.web.bind.annotation.RequestMethod;
org.springframework.web.servlet.ModelAndView;

import springapp.business.ProductManager;
import springapp.model.Product;
@Controller()
@RequestMapping("/product")
public class ProductController {
@Autowired
ProductManager manager;
protected final Log logger = LogFactory.getLog(getClass());
@RequestMapping(value = "/list", method = RequestMethod.GET)
public ModelAndView listProducts() {
logger.info("List of products");
Collection<Product> products = manager.findAll();
return new ModelAndView("productsList", "products", products);
}
}

Ce controleur est accompagne de la vue productsList.jsp :


<%@ include file="/WEB-INF/jsp/include.jsp"%>
<c:url var="edit" value="/actions/product/edit" />
<html>
<head>
<title>Hello :: Spring Application</title>
</head>
<body>
<h1>Products</h1>
<table border=1>
<c:forEach items="${products}" var="prod">
<tr>
<td><a href="${edit}?id=${prod.number}"><c:out value="${prod.name}" /></a></td>
<td><i>$<c:out value="${prod.price}" /></i></td>
</tr>
</c:forEach>
</table>
<p><a href="${edit}">Create new product</a></p>
</body>
</html>

11

Cette vue va construire la liste des produits, avec pour chacun une possibilite dedition. Bien entendu, pour
linstant, la phase dedition ne fonctionne pas.
Note : Dans cet exemple, le controleur ne fait rien dautre que de construire des donnees (la liste des produits) pour
les envoyer `a la vue. La creation des donnees peut etre decouplee des controleurs et placee dans des methodes
annotees par @ModelAttribute. Ces methodes sont systematiquement executees avant les controleurs pour
remplir le mod`ele.
Dans notre exemple, la creation de la liste des produits (nommee products) peut se faire par la methode :
@ModelAttribute("products")
Collection<Product> products() {
logger.info("Making list of products");
return manager.findAll();
}

Le controleur devient :
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String listProducts2() {
logger.info("List of products");
return "productsList";
}

La vue va simplement puiser dans le mod`ele qui est rempli par la methode products.

5.3

Editer
un produit

Definissons maintenant le controleur dacc`es au formulaire dedition :


@RequestMapping(value = "/edit", method = RequestMethod.GET)
public String editProduct(@ModelAttribute Product p) {
return "productForm";
}

accompagne du formulaire productForm.jsp :

12

<%@ include file="/WEB-INF/jsp/include.jsp"%>


<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>
<head>
<style>
.error {
color: #ff0000;
}
.errorblock{
color: #000;
background-color: #ffEEEE;
border: 3px solid #ff0000;
padding:8px;
margin:16px;
}
</style>
</head>
<body>
<h1>Edit Product</h1>
<form:form method="POST" commandName="product">
<form:errors path="*" cssClass="errorblock" element="div"/>
<table>
<tr>
<td>Name : </td>
<td><form:input path="name" /></td>
<td><form:errors path="name" cssClass="error" /></td>
</tr>
<tr>
<td>Description : </td>
<td><form:textarea path="description" /></td>
<td><form:errors path="description" cssClass="error" /></td>
</tr>
<tr>
<td>price : </td>
<td><form:input path="price" /></td>
<td><form:errors path="price" cssClass="error" /></td>
</tr>
<tr>
<td colspan="3"><input type="submit" /></td>
</tr>
</table>
</form:form>
</body>
</html>

Cette vue utilise les balises personnalisees de Spring pour gerer facilement la recuperation des donnees du mod`ele
(attribut commandName de la balise form) et la mise en place des champs (balises form :input, form :select,
etc...). Vous trouverez plus dinformation sur ces balises dans cette documentation9 .
Pour linstant, ce formulaire ne permet pas dediter des produits dej`a existants. Pour ce faire, nous allons ajouter
une methode annotee @ModelAttribute qui va preparer linstance du produit `a editer en fonction du param`etre
de la requete HTTP :
9

http://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/html/view.html#view-jsp-formtaglib

13

@ModelAttribute
public Product newProduct(
@RequestParam(value = "id", required = false) Integer productNumber) {
if (productNumber != null) {
logger.info("find product " + productNumber);
return manager.find(productNumber);
}
Product p = new Product();
p.setNumber(null);
p.setName("");
p.setPrice(0.0);
p.setDescription("");
logger.info("new product = " + p);
return p;
}

En clair : si la requete /edit est accompagnee dun numero de produit (param`etre id optionnel), le produit sera
charge `a partir du manager. Dans le cas contraire, un nouveau produit sera renvoye. Testez ce fonctionnement.
Il nous reste maintenant `a mettre en place le controleur de soumission du formulaire :
@RequestMapping(value = "/edit", method = RequestMethod.POST)
public String saveProduct(@ModelAttribute Product p, BindingResult result) {
if (result.hasErrors()) {
return "productForm";
}
manager.save(p);
return "productsList";
}

A cette etape, la seule erreur possible provient dune erreur de conversion sur le prix. Essayez de donner un prix
incorrect afin de tester ce fonctionnement.
Avertissement : A ce stade, la creation dun nouveau produit (apr`es la soumission) se termine sur laffichage
de la liste des produits (derni`ere ligne du controleur ci-dessus). Ce comportement pose un probl`eme : Si le client
tente un rechargement de la page, cela va provoquer une nouvelle soumission et la creation dun nouveau produit !
Pour regler ce probl`eme, nous allons renvoyer non pas sur la vue productsList, mais sur laction permettant
davoir la liste des produits :
@RequestMapping(value = "/edit", method = RequestMethod.POST)
public String saveProduct(@ModelAttribute Product p, BindingResult result) {
if (result.hasErrors()) {
return "productForm";
}
manager.save(p);
return "redirect:list";
}

5.4

Introduire des donn


ees dans un formulaire

Pour construire un formulaire complexe, nous avons souvent besoin dutiliser des donnees annexes (liste de
references, nom dutilisateur, etc.). Pour ce faire, nous allons de nouveau utiliser lannotation @ModelAttribute.
Mettez en place la methode suivante :

14

@ModelAttribute("productTypes")
public Map<String, String> productTypes() {
Map<String, String> types = new LinkedHashMap<>();
types.put("type1", "Type 1");
types.put("type2", "Type 2");
types.put("type3", "Type 3");
types.put("type4", "Type 4");
types.put("type5", "Type 5");
return types;
}

Elle fabrique et injecte dans le mod`ele une table de correspondance qui va nous etre utile pour ajouter le champ
de typage dans le formulaire. Modifiez notre formulaire en ajoutant :
<tr>
<td>Type : </td>
<td>
<form:select path="type" multiple="false">
<form:option value="" label="--- Select ---" />
<form:options items="${productTypes}" />
</form:select>
</td>
<td><form:errors path="type" cssClass="error" /></td>
</tr>

Nous pouvons maintenant associer un type `a chaque produit.

5.5

Valider les donn


ees

Il manque maintenant la phase de validation des donnees du formulaire. Pour ce faire, nous allons developper une
classe de validation adaptee au produit :

15

package springapp.web;
import
import
import
import

org.springframework.stereotype.Service;
org.springframework.validation.Errors;
org.springframework.validation.ValidationUtils;
org.springframework.validation.Validator;

import springapp.model.Product;
import springapp.model.ProductCode;
@Service
public class ProductValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Product.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Product product = (Product) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name",
"product.name", "Field name is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "description",
"product.description", "Field description is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "type",
"product.type", "Field type is required.");
if (!(product.getPrice() > 0.0)) {
errors.rejectValue("price", "product.price.too.low",
"Price too low");
}
}
}

Pour lutiliser, il suffit de modifier le controleur comme suit :


@Autowired
ProductValidator validator;
@RequestMapping(value = "/edit", method = RequestMethod.POST)
public String saveProduct(@ModelAttribute Product p, BindingResult result) {
validator.validate(p, result);
if (result.hasErrors()) {
return "productForm";
}
manager.save(p);
return "productsList";
}

5.6

Traduire les messages de validation

Pour linstant les messages derreurs sont affiches en anglais. Nous pouvons les traduire automatiquement en
delocalisant ces messages dans des fichiers de ressources.

Commencez par creer dans le repertoire contenant les sources du paquetage springapp.web le fichier product.properties
16

product.name = Name is required!


product.description = Description is required!
product.type = Type is required!
product.price.too.low = Price is too low!

puis le fichier product fr FR.properties :


product.name = Le nom est requis
product.price.too.low = Le prix est trop bas !

Tous les messages sont donnes en anglais. Certains sont en francais.


Pour exploiter ces ressources, nous allons les charger en ajoutant dans le fichier WEB-INF/springapp-servlet.xml
la creation dun nouveau service :
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>/springapp/web/product</value>
</list>
</property>
</bean>

Nous pouvons simplifier notre classe de validation en supprimant les messages. Elle devient :

17

package springapp.web;
import
import
import
import

org.springframework.stereotype.Service;
org.springframework.validation.Errors;
org.springframework.validation.ValidationUtils;
org.springframework.validation.Validator;

import springapp.model.Product;
import springapp.model.ProductCode;
@Service
public class ProductValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Product.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Product product = (Product) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name",
"product.name");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "description",
"product.description");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "type",
"product.type");
if (!(product.getPrice() > 0.0)) {
errors.rejectValue("price", "product.price.too.low");
}
}
}

5.7

Traiter des champs complexes

Nous venons de le voir, Spring MVC traite parfaitement les champs qui correspondent `a un type de base (entier,
flottant, chane, etc.). Nous allons maintenant nous occuper des champs complexes.
Commencons par definir une classe pour representer le numero de serie dun produit (une lettre suivie dun entier
entre 1000 et 9999) :

18

package springapp.model;
public class ProductCode {
String base;
int number;
public String getBase() {
return base;
}
public void setBase(String base) {
this.base = base;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public ProductCode() {
super();
}
public ProductCode(String base, int number) {
super();
this.base = base;
this.number = number;
}
}

et ajoutons ce code `a notre produit :


package springapp.model;
public class Product {
...
private ProductCode code;
...
public ProductCode getCode() {
return code;
}
public void setCode(ProductCode code) {
this.code = code;
}
}

Faites ensuite les modifications suivantes :


dans InMemoryProductManager associez le code A1000 au premier produit et B2000 au deuxi`eme.
19

ajoutez le code suivant `a votre formulaire :


<tr>
<td>Code : </td>
<td>
<form:input path="code.base" />
<form:input path="code.number" />
</td>
<td><form:errors path="code" cssClass="error" /></td>
</tr>

Modifiez le validateur en consequence en ajoutant


ProductCode code = product.getCode();
if (code != null) {
if (!code.getBase().matches("[A-Z]")) {
errors.rejectValue("code", "product.code.base");
}
if (!(code.getNumber() >= 1000 && code.getNumber() <= 9999)) {
errors.rejectValue("code", "product.code.number");
}
}

Ajoutez des messages derreurs pour product.code.base et product.code.number.


Vous devez maintenant etre capable dediter les deux parties du numero de serie.
Une autre solution consiste `a fournir une classe dadaptation (un editeur dans la terminologie des JavaBeans)
qui est capable de transformer un code en chane et vice-versa. En voici un exemple :
package springapp.web;
import java.beans.PropertyEditorSupport;
import springapp.model.ProductCode;
class ProductCodeEditor extends PropertyEditorSupport {
@Override
public String getAsText() {
Object o = this.getValue();
if (o instanceof ProductCode) {
ProductCode c = (ProductCode) o;
return c.getBase() + "" + c.getNumber();
}
return super.getAsText();
}
@Override
public void setAsText(String text) throws IllegalArgumentException {
try {
String base = text.substring(0, 1);
int number = Integer.parseInt(text.substring(1));
ProductCode c = new ProductCode(base, number);
super.setValue(c);
} catch (Exception e) {
throw new IllegalArgumentException("Bad code format");
}
}
}

20

Il suffit maintenant dindiquer au controleur que nous disposons de cette classe. Pour ce faire nous allons lui
adjoindre une methode annotee par InitBinder :
@InitBinder
public void initBinder(WebDataBinder b) {
b.registerCustomEditor(ProductCode.class, new ProductCodeEditor());
}

Le formulaire peut devenir :


<tr>
<td>Code : </td>
<td>
<form:input path="code" />
</td>
<td><form:errors path="code" cssClass="error" /></td>
</tr>

Testez le bon fonctionnement de cette nouvelle version.

Validation des JavaBean dans JEE 6 (et donc 7)

Une nouvelle specification (JSR303) nous permet dexprimer les contraintes sur les proprietes par des annotations
(plus dinformation dans la documentation JEE 610 et dans la JavaDoc11 ).

6.1

Mise en oeuvre

Commencons par enrichir nos POJOs :


10
11

http://docs.oracle.com/javaee/6/tutorial/doc/gircz.html
http ://docs.oracle.com/javaee/6/api/javax/validation/constraints/package-summary.html

21

package springapp.model;
import
import
import
import

javax.validation.Valid;
javax.validation.constraints.Min;
javax.validation.constraints.NotNull;
javax.validation.constraints.Size;

public class Product {


private Integer number;
@NotNull
@Size(min = 1, message = "Le nom est obligatoire")
private String name;
@NotNull
@Min(value = 1, message = "Le prix est trop bas")
private Double price;
@NotNull(message = "La description est obligatoire")
@Size(min = 1, max = 100, message = "Entre 1 et 200 caract`
eres")
private String description;
@NotNull()
@Size(min=1,message="Le type doit ^
etre renseign
e")
private String type;
@Valid
private ProductCode code;
... getters and setters
}

et
package springapp.model;
import
import
import
import

javax.validation.constraints.Max;
javax.validation.constraints.Min;
javax.validation.constraints.NotNull;
javax.validation.constraints.Size;

public class ProductCode {


@NotNull
@Size(min = 1, max = 1)
@Pattern(regexp="[A-Z]", message="Le code doit d
ebuter par une majuscule")
String base;
@Min(value=1000, message="Le num
ero doit ^
etre >= `
a 1000")
@Max(value=9999, message="Le num
ero doit ^
etre <= `
a 9999")
int number;
... getters and setters
}

Nous allons indiquer `a Spring que nous utilisons la validation par lutilisation des annotations (fichier WEB-INF/springapp-s

22

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
....
<!-- support JSR303 annotation if JSR 303 validation present on classpath -->
<mvc:annotation-driven />
....
</beans>

puis nous devons ajouter lannotation @Valid dans le controleur :


...
@RequestMapping(value = "/edit", method = RequestMethod.POST)
public String saveProduct(@ModelAttribute @Valid Product p, BindingResult result) {
validator.validate(p, result);
if (result.hasErrors()) {
return "productForm";
}
manager.save(p);
return "productsList";
}
...

Testez le resultat. Vous devez vous retrouver avec les messages en provenance du validateur plus les nouveaux
messages en provenance des annotations. Vous pouvez maintenant vider votre validateur manuel. La classe de
validation reste utile pour les validations m
etier.

6.2

Cr
eer ses propres contraintes

Le mecanisme de validation peut facilement etre etendu pas ajout de contraintes specifiques. Nous allons creer
une contrainte Bye qui va verifier la presence de ce mot dans un champ. Pour ce faire, commencez par creez
lannotation Bye :

23

package springapp.web;
import
import
import
import
import

java.lang.annotation.Documented;
java.lang.annotation.ElementType;
java.lang.annotation.Retention;
java.lang.annotation.RetentionPolicy;
java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ByeConstraintValidator.class)
@Documented
public @interface Bye {
String message() default "Il manque le bye";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

Puis continuez en creant la classe de validation ByeConstraintValidator :


package springapp.web;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ByeConstraintValidator implements ConstraintValidator<Bye, String> {
@Override
public void initialize(Bye arg0) {
}
@Override
public boolean isValid(String arg0, ConstraintValidatorContext arg1) {
if (arg0.contains("bye"))
return true;
return false;
}
}

Modifez ensuite la classe Product pour utiliser cette annotation :

24

public class Product {


...
@NotNull(message = "La description est obligatoire")
@Size(min = 1, max = 100, message = "Entre 1 et 200 caract`
eres")
@Bye
private String description;
...
}

Verifiez son bon fonctionnement !

Utiliser des donn


ees en session

Il est facile de recuperer des donnees placees en session, mais Spring nous offre le moyen dinjecter directement
dans nos controleurs des donnees de portee session.

Etape
1 : definissez un nouveau bean pour representer lutilisateur courant :
package springapp.web;
import java.io.Serializable;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component()
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

Lannotation Component indique que cest un composant gere par Spring. Lannotation Scope donne la portee
des instances (une par session). La clause ProxyMode permet dindiquer que ce nest pas directement une instance
qui doit etre injectee, mais un proxy qui va selectionner la bonne instance (dans la bonne session) en fonction du
contexte.

Etape
2 : definissez un controleur qui utilise linjection du bean User :

25

package springapp.web;
import
import
import
import
import
import

org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
org.springframework.beans.factory.annotation.Autowired;
org.springframework.stereotype.Controller;
org.springframework.web.bind.annotation.ModelAttribute;
org.springframework.web.bind.annotation.RequestMapping;

@Controller()
@RequestMapping("/user")
public class UserController {
protected final Log logger = LogFactory.getLog(getClass());
@Autowired()
User user;
@ModelAttribute("user")
public User newUser() {
return user;
}
@RequestMapping(value = "/show")
public String show() {
logger.info("show user " + user);
return "user";
}
@RequestMapping(value = "/login")
public String login() {
logger.info("login user " + user);
user.setName("Its me");
return "user";
}
@RequestMapping(value = "/logout")
public String logout() {
logger.info("logout user " + user);
user.setName("Anonymous");
return "user";
}
}

Etape
3 : La vue :

26

<%@ include file="/WEB-INF/jsp/include.jsp"%>


<c:url var="login" value="/actions/user/login" />
<c:url var="logout" value="/actions/user/logout" />
<c:url var="show"
value="/actions/user/show" />
<html>
<body>
<h1>User</h1>
<p>
name : <c:out value="${user.name}" default="no name"/> |
<a href="${show}">Show</a> | <a href="${login}">Login</a> |
<a href="${logout}">Logout</a>
</p>
</body>
</html>

Moralit
e : Le controleur (qui est un singleton execute par plusieurs threads) utilise le proxy pour selectionner
automatiquement linstance du bean User qui correspond `a la requete courante et `a la session courante.
La liaison se fait par le thread. Cest le meme thread qui traite toute la requete (Dispatcher, controleur,
vue). Le thread courant est donc utilise comme une sorte de variable globale qui permet de faire des liaisons
implicites.

Utiliser des EJB

Nous allons maintenant etudier comment mettre en place une couche metier basee sur des EJBs.

8.1

D
efinition des EJBs

Prenons un EJB entite :

27

package springapp.services;
import
import
import
import
import

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

@Entity
@Table(name = "WA_MESSAGE")
public class Message {
@Id
@GeneratedValue
Integer number;
@Column
String text;
public Message() {
super();
}
public Message(String text) {
super();
this.text = text;
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}

et un EJB oriente service en commencant par la specification :


package springapp.services;
import java.util.Collection;
public interface IMessageManager {
void add(String message);
int removeAll();
Collection<Message> findAll();
}

28

puis en continuant par limplantation


package springapp.services;
import java.util.Collection;
import
import
import
import
import
import

javax.annotation.PostConstruct;
javax.ejb.LocalBean;
javax.ejb.Startup;
javax.ejb.Stateless;
javax.persistence.EntityManager;
javax.persistence.PersistenceContext;

@Stateless
@LocalBean()
@Startup
public class MessageManager implements IMessageManager {
@PersistenceContext(unitName = "myData")
EntityManager em;
public MessageManager() {
}
@PostConstruct
public void init() {
System.out.println("INIT EJB = " + this);
}
@Override
public void add(String data) {
Message m = new Message(data);
em.persist(m);
}
@Override
public int removeAll() {
return em.createQuery("Delete From Message").executeUpdate();
}
@Override
public Collection<Message> findAll() {
return em.createQuery("Select m From Message m", Message.class)
.getResultList();
}
}

Definissons lunite de persistance en creant le fichier WebContent/META-INF/persistence.xml :

29

<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">

<persistence-unit name="myData" transaction-type="JTA">


<properties>
<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(Foreign
</properties>
</persistence-unit>
</persistence>

Dans cette version nous utilisons le fournisseur de persistance par defaut de TomEE : OpenJPA12 . Nous utilisons
egalement la DataSource par defaut de TomEE (basee sur HSql).

8.2

Configurer Spring

Vous devez ajouter la creation de lEJB dans le fichier XML de configuration de votre application :
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/jee
http://www.springframework.org/schema/jee/spring-jee.xsd
">
...
<jee:jndi-lookup id="messageManager"
jndi-name="java:global/votre-application/MessageManager" />
...
</beans>

Notez le nouvel espace de nom jee `a ajouter (plus dinformation13 ).


Travail : verifiez dans les traces du serveur TomEE la decouverte, le nom et linitialisation de votre EJB.

8.3

La partie WEB

Commencons par le controleur :


12

http://openjpa.apache.org/
http
://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/html/xsd-config.html#xsd-config-bodyschemas-jee
13

30

package springapp.web;
import java.util.Collection;
import
import
import
import
import
import
import
import

org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
org.springframework.beans.factory.annotation.Autowired;
org.springframework.stereotype.Controller;
org.springframework.web.bind.annotation.ModelAttribute;
org.springframework.web.bind.annotation.RequestMapping;
org.springframework.web.bind.annotation.RequestParam;
org.springframework.web.servlet.ModelAndView;

import springapp.services.IMessageManager;
import springapp.services.Message;
@Controller()
@RequestMapping("/message")
public class MessageController {
@Autowired
IMessageManager messageManger;
protected final Log logger = LogFactory.getLog(getClass());
@RequestMapping(value = "/add")
public ModelAndView add(@RequestParam(required = true) String text) {
messageManger.add(text);
return new ModelAndView("message", "messages", messages());
}
@RequestMapping(value = "/removeAll")
public ModelAndView removeAll() {
int n = messageManger.removeAll();
logger.info(n + " deleted message(s)");
return new ModelAndView("message", "messages", messages());
}
@RequestMapping(value = "/list")
public String list() {
return "message";
}
@ModelAttribute("messages")
public Collection<Message> messages() {
return messageManger.findAll();
}
}

et terminons par la vue :

31

<%@ include file="/WEB-INF/jsp/include.jsp"%>


<c:url var="add"
value="/actions/message/add" />
<c:url var="remove" value="/actions/message/removeAll" />
<c:url var="list"
value="/actions/message/list" />
<html>
<body>
<h1>Messages</h1>
<form action="${add}" method="POST">
<p>
<input name="text" size="10"/>
<input type="submit" value="Add"/> |
<a href="${list}">List</a> |
<a href="${remove}">Remove All</a>
</p>
</form>
<ul>
<c:forEach items="${messages}" var="m">
<li>${m.number} : ${m.text}</li>
</c:forEach>
</ul>
</body>
</html>

Testez le bon fonctionement de votre application.


Question : ou se trouve la base de donnees ?
Remarque : vous trouverez dans cette documentation14 des informations sur lutilisation directe de JPA dans
une application Spring hors dun environnement EJB.

Cest fini

` bientot.
A

14

http ://docs.spring.io/spring/docs/3.2.5.RELEASE/spring-framework-reference/html/orm.html#orm-jpa

32