Depuis ses débuts, le framework Spring s'est efforcé d'apporter des solutions puissantes, mieux encore, non invasives à des problèmes complexes. Spring 2.0 a introduit les "custom namespaces" comme un moyen de réduire la configuration basée sur XML. Ils ont depuis pris racine au coeur du framework Spring (les namespaces aop, context, jee, jms, lang, tx et util), dans les projets Spring Portfolio (Spring Security) et dans les projets non Spring (CXF).
Spring 2.5 a déployé un ensemble complet d'annotations comme alternative à la configuration basée sur XML. Les annotations peuvent être utilisées pour la découverte automatique des objets gérés par Spring, l'injection des dépendances, le cycle de vie des méthodes, la configuration de la couche Web et les tests unitaires ou d'intégration.
Cet article est la seconde partie d'une série de trois explorant les annotations introduites dans la version 2.5 de Spring. Il couvre le support des annotations dans la couche Web. Le dernier article mettra l'accent sur les fonctionnalités supplémentaires disponibles pour l'intégration et le test.
La première partie de la série a montré comment les annotations Java peuvent être utilisées comme une alternative à XML pour la configuration des objets gérés par Spring et pour l'injection des dépendances. Voici à nouveau un exemple :
@Controller
public class ClinicController {
private final Clinic clinic;
@Autowired
public ClinicController(Clinic clinic) {
this.clinic = clinic;
}
...
@Controller indique que ClinicController est un composant de la couche Web. @Autowired demande l'injection d'une instance de Clinic. Cet exemple ne requiert qu'une petite quantité d'XML pour permettre la prise en compte des 2 annotations et pour limiter le périmètre des composants à scanner :
<context:component-scan base-package="org.springframework.samples.petclinic"/>
C'est une très bonne nouvelle pour la couche Web où la configuration XML de Spring tend à être plus verbeuse, pour une valeur peut-être moindre, que dans les couches inférieures. Les contrôleurs contiennent une multitude de propriétés telles que les noms des vues, les noms des objets de formulaire et les types de validateur, qui concernent davantage des problématiques de configuration que d'injection des dépendances. Ces configurations peuvent être gérées efficacement, par exemple en passant par l'héritage de la définition de bean ou en évitant la configuration de propriétés rarement modifiées. Pourtant, d'après mon expérience, beaucoup de développeurs ne font pas cela et il en résulte plus de code XML que nécessaire. Ainsi, les annotations @Controller et @Autowired peuvent avoir un très bon effet sur la configuration de la couche Web.
Poursuivons cette discussion de la deuxième partie de la série par une revue des annotations de la couche Web de Spring 2.5. Ces annotations sont, de manière informelle, connues sous le nom de @MVC, référence à Spring MVC et à Spring Portlet MVC. En effet, la plupart des fonctionnalités évoquées dans cet article s'appliquent à ces 2 modules.
De Controller à @Controller
Contrairement aux annotations évoquées dans la première partie, @MVC est plus qu'une alternative de configuration. Considérons cette signature bien connue d'un contrôleur Spring MVC :
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse
response) throws Exception;
}
Tous les contrôleurs Spring MVC soit implémentent directement Controller soit étendent l'une des implémentations de base telles que AbstractController, SimpleFormController, MultiActionController ou AbstractWizardFormController. C'est cette interface qui permet à la DispatcherServlet de Spring MVC de traiter ces derniers comme des "handlers" et de les invoquer à l'aide d'un adaptateur appelé SimpleControllerHandlerAdapter.
@MVC modifie ce modèle de programmation de 3 manières significatives :
1. Il ne requiert aucune extension d'interface ou classe de base.
2. Il permet n'importe quel nombre de méthodes de traitement de requête.
3. Il autorise un haut degré de flexibilité dans la signature des méthodes.
Etant donnés ces 3 points, il est juste de dire que @MVC n'est pas simplement une alternative. C'est la prochaine étape dans l'évolution de la technologie contrôleur de Spring MVC.
La DispatcherServlet invoque des contrôleurs annotés à l'aide d'un adaptateur appelé AnnotationMethodHandlerAdapter. C'est cet adaptateur qui est en charge d'analyser les annotations évoquées ci-après. C'est également cet adaptateur qui permet de s'affranchir du besoin d'extension de classes du framework.
Présentation de @RequestMapping
Commençons par un contrôleur qui ressemble à un Controller Spring MVC traditionnel :
@Controller
public class AccountsController {
private AccountRepository accountRepository;
@Autowired
public AccountsController(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@RequestMapping("/accounts/show")
public ModelAndView show(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
ModelAndView mav = new ModelAndView("/WEB-INF/views/accounts/show.jsp");
mav.addObject("account", accountRepository.findAccount(number));
return mav;
}
}
Ce qui diffère ici est que ce contrôleur n'implémente pas l'interface Controller et utilise l'annotation @RequestMapping pour indiquer que show() est une méthode de traitement de requête mappée sur le chemin d'URI "accounts/show". Le reste du code est typique d'un contrôleur Spring MVC.
Nous reviendrons sur @RequestMapping après avoir complètement converti la méthode ci-dessus en @MVC, mais, avant de continuer, notons que l'URI du mapping de requête ci-dessus sera associé à des chemins d'URI pourvus de n'importe quelle extension, comme :
/accounts/show.htm
/accounts/show.xls
/accounts/show.pdf
...
Signatures flexibles des méthodes de traitement de requête
Nous vous avons promis des signatures de méthodes flexibles. Supprimons donc l'objet "response" des paramètres d'entrée et au lieu de retourner un ModelAndView, ajoutons dans ces paramètres une Map représentant notre modèle. De plus, nous retournerons une String pour indiquer le nom de la vue à utiliser lors du rendu de la réponse :
@RequestMapping("/accounts/show")
public String show(HttpServletRequest request, Map<String, Object> model)
throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
model.put("account", accountRepository.findAccount(number));
return "/WEB-INF/views/accounts/show.jsp";
}
Le paramètre d'entrée de type Map est connu sous le nom de modèle "implicite" et est commodément créé pour nous avant l'invocation de la méthode. On peut y ajouter des paires clé-valeur, qui seront disponibles et accessibles dans la vue -- dans ce cas, la page show.jsp.
@MVC permet l'utilisation d'un certain nombre de types de paramètres d'entrée tels que HttpServletRequest/HttpServletResponse, HttpSession, Locale, InputStream, OutputStream, File[] et d'autres. Ils peuvent être fournis dans n'importe quel ordre. @MVC autorise aussi un certain nombre de types de retour tels que ModelAndView, Map, String et void parmi d'autres. Etudiez la JavaDoc de @RequestMapping pour une liste complète des types de paramètre d'entrée et de sortie supportés.
Regardons ce qu'il se passe dans le cas particulier d'une méthode qui ne spécifie pas de vue (ex: le type de retour est void). Dans ce cas, par convention, DispatcherServlet réutilise le chemin de l'URI de la requête en supprimant le premier slash et l'extension. Passons le type de retour à void :
@RequestMapping("/accounts/show")
public void show(HttpServletRequest request, Map<String, Object> model) throws Exception {
String number = ServletRequestUtils.getStringParameter(request, "number");
model.put("account", accountRepository.findAccount(number));
}
Si l'on combine cette méthode de traitement de requête avec le mapping "/accounts/show", alors la DispatcherServlet utilise par défaut le nom de vue "accounts/show", qui, associé à un résolveur de vue approprié, produit le même résultat que précédemment :
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
S'appuyer sur des conventions pour le nommage des vues est fortement recommandé car cela permet d'éliminer leur codage en dur dans les contrôleurs. Si vous avez besoin de personnaliser la manière dont la DispatcherServlet dérive les noms de vue par défaut, alors configurez votre propre implémentation de RequestToViewNameTranslator dans le contexte de servlet en lui donnant l'id "viewNameTranslator".
Extraire et analyser les paramètres avec @RequestParam
Une autre fonctionnalité de @MVC est sa capacité à extraire et analyser les paramètres de la requête. Continuons le refactoring de notre méthode en ajoutant l'annotation @RequestParam :
@RequestMapping("/accounts/show")
public void show(@RequestParam("number") String number, Map<String, Object> model) {
model.put("account", accountRepository.findAccount(number));
}
Ici, l'annotation @RequestParam permet d'extraire un paramètre de type String et de nom "number", et permet de le passer en tant que paramètre d'entrée. @RequestParam supporte la conversion de type ainsi que les paramètres obligatoires ou optionnels. Concernant la conversion de type, tous les types Java de base sont supportés et vous pouvez customiser cette conversion avec des PropertyEditors. Voici quelques exemples de plus incluant des paramètres obligatoires et optionnels :
@RequestParam(value="number", required=false) String number
@RequestParam("id") Long id
@RequestParam("balance") double balance
@RequestParam double amount
Remarquez que la dernière ligne ne fournit pas de nom de paramètre explicite. Un paramètre de nom "amount" sera cependant extrait avec succès, si des instructions de débogage ("debug symbols") complémentaires sont bien spécifiées. Au moment de la compilation, en cas d'absence de ces instructions, une IllegalStateException sera toutefois lancée, le programme manquant alors d'informations pour réaliser l'extraction. Pour cette raison, il est préférable de spécifier le nom du paramètre explicitement.
@RequestMapping, la suite
Il est tout à fait possible de placer @RequestMapping à la fois au niveau de la classe et des méthodes, ce qui permet ainsi d'affiner la configuration par un effet de cascade. Voici quelques exemples :
Niveau classe :
RequestMapping("/accounts/*")
Niveau méthode :
@RequestMapping(value="delete", method=RequestMethod.POST)
@RequestMapping(value="index", method=RequestMethod.GET, params="type=checking")
@RequestMapping
La combinaison du premier mapping de requête niveau méthode avec le mapping niveau classe cible le chemin "/accounts/delete" en méthode HTTP POST. Le deuxième exige qu'un paramètre de nom "type" et de valeur "checking" soit également présent dans la requête. Le troisième ne spécifie pas de chemin du tout, i.e. correspond à toutes les méthodes HTTP, le nom de la méthode de classe étant utilisé si nécessaire. Changeons notre code pour s'appuyer sur la résolution par nom de méthode :
@Controller
@RequestMapping("/accounts/*")
public class AccountsController {
@RequestMapping(method=RequestMethod.GET)
public void show(@RequestParam("number") String number, Map<String, Object> model) {
model.put("account", accountRepository.findAccount(number));
}
...
Cette méthode est appelée pour les requêtes sur l'url "/accounts/show", grâce au @RequestMapping niveau classe pour "/accounts/*" et au nom de méthode pour "show".
Suppression des mappings de requête niveau Classe
Une fréquente objection aux annotations situées dans la couche Web vient du fait que les chemins d'URI sont intégrés directement dans le code source. Nous pouvons facilement y remédier en utilisant une stratégie par configuration XML, faisant correspondre les chemins d'URI aux classes de contrôleurs, et utilisant les annotations @RequestMapping pour le mapping de méthode uniquement.
Nous allons configurer un ControllerClassNameHandlerMapping, qui mappe les chemins d'URI aux contrôleurs en utilisant une convention qui s'appuie sur un nom de classe de contrôleur :
<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
Dorénavant, les requêtes sur "/accounts/*" sont prises en charge par AccountsController. Cela fonctionne bien en combinaison avec les annotations @RequestMapping niveau méthode, qui vont compléter le mapping ci-dessus en ajoutant le nom de la méthode. De plus, puisque notre méthode ne retourne pas un nom de vue, nous utilisons maintenant une convention basée sur le nom de la classe, le nom de la méthode, le chemin d'un URI et le nom de la vue.
Voici à quoi ressemble le @Controller après avoir été complètement converti en @MVC :
@Controller
public class AccountsController {
private AccountRepository accountRepository;
@Autowired
public AccountsController(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@RequestMapping(method=RequestMethod.GET)
public void show(@RequestParam("number") String number, Map<String, Object> model) {
model.put("account", accountRepository.findAccount(number));
}
...
Et le XML associé :
<context:component-scan base-package="com.abc.accounts"/>
<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
Comme vous pouvez le voir, il y a un minimum de configuration XML, pas de chemins d'URI au sein des annotations, pas de noms de vues explicites, la méthode de traitement de requête consiste en une seule ligne, la signature de la méthode correspond précisément à notre besoin et des méthodes supplémentaires de traitement de requête peuvent facilement être ajoutées. Tous ces avantages sont apportés sans le besoin d'une classe de base et sans XML - au moins non directement imputable à ce contrôleur.
Commencez-vous à mesurer l'efficacité de ce modèle de programmation?
Le traitement de formulaire @MVC
Un scénario typique de traitement de formulaire implique la récupération de l'objet à éditer, la présentation en mode édition des données qu'il contient, la soumission du formulaire par l'utilisateur et, finalement, la validation et l'enregistrement des modifications. Pour faciliter tout cela, Spring MVC offre plusieurs fonctionnalités : un mécanisme de data binding pour générer complètement un objet à partir des paramètres de la requête, le support du traitement des erreurs et de la validation, une librairie de tags JSP pour les formulaires, et des classes de base pour les contrôleurs. Avec @MVC rien ne change - si ce n'est que l'on peut se passer des classes à hériter pour les contrôleurs, en faisant usage des annotations suivantes : @ModelAttribute, @InitBinder et @SessionAttributes.
L'annotation @ModelAttribute
Jetez un coup d'oeil à ces signatures de méthode de traitement de requête :
@RequestMapping(method=RequestMethod.GET)
public Account setupForm() {
...
}
@RequestMapping(method=RequestMethod.POST)
public void onSubmit(Account account) {
...
}
Ce sont des signatures de méthode de traitement de requête parfaitement valides. La première méthode traite le GET HTTP initial. Elle prépare les données à éditer et retourne un Account utilisable avec les tags de formulaire Spring MVC. La seconde méthode traite le POST HTTP suivant, lorsque l'utilisateur soumet les modifications. Elle accepte un Account qui est généré automatiquement à partir des paramètres de la requête, grâce au mécanisme de data binding de Spring MVC. C'est un modèle de programmation très simple.
L'objet Account contient les données à éditer. Dans la terminologie Spring MVC l'Account est connu sous le nom de "form model object". Cet objet doit être rendu disponible pour les tags de formulaire (ainsi que pour le mécanisme de data binding), sous un nom donné. Voici un extrait d'une page JSP qui réfère un "form model object" nommé "account" :
<form:form modelAttribute="account" method="post">
Account Number: <form:input path="number"/><form:errors path="number"/>
...
</form>
Ce bout de code JSP fonctionnera très bien avec les signatures des méthodes ci-dessus, bien que nous n'ayons spécifié le nom "account" nulle part. C'est parce que @MVC s'appuie sur le nom du type de l'objet retourné pour choisir un nom par défaut. Ainsi, un objet de type Account donnera lieu par défaut à un "form model object" nommé "account". Si le nom par défaut ne convient pas, nous pouvons utiliser @ModelAttribute pour le modifier :
@RequestMapping(method=RequestMethod.GET)
public @ModelAttribute("account") SpecialAccount setupForm() {
...
}
@RequestMapping(method=RequestMethod.POST)
public void update(@ModelAttribute("account") SpecialAccount account) {
...
}
@ModelAttribute peut aussi être placée au niveau méthode, pour un effet légèrement différent :
@ModelAttribute
public Account setupModelAttribute() {
...
}
Ici, setupModelAttribute() n'est pas une méthode de traitement de requête. C'est en fait une méthode utilisée pour préparer le "form model object" avant que toute autre méthode de traitement de requête ne soit invoquée. Pour ceux qui sont familiers avec Spring MVC, c'est très similaire à la méthode formBackingObject() de SimpleFormController.
Positionner @ModelAttribute au niveau d'une méthode est utile dans un scénario de traitement de formulaire où l'on récupère le "form model object" une première fois pendant le GET initial, et une deuxième fois sur le POST suivant, lorsque l'on souhaite que le data binding applique les modifications de l'utilisateur sur l'objet Account. Bien sûr, une alternative à la double récupération de l'objet aurait été de l'enregistrer dans la session HTTP entre les deux requêtes. C'est ce que nous allons étudier ensuite.
Stocker les attributs avec @SessionAttributes
L'annotation @SessionAttributes peut être utilisée pour spécifier soit les noms soit les types des "form model objects" à garder dans la session entre les requêtes. Voici quelques exemples :
@Controller
@SessionAttributes("account")
public class AccountFormController {
...
}
@Controller
@SessionAttributes(types = Account.class)
public class AccountFormController {
...
}
Avec cette annotation, l'AccountFormController stocke un "form model object" nommé "account" (ou, dans le second cas, tout "form model object" de type Account) dans la session HTTP entre le GET initial et le POST qui suit. L'attribut doit cependant être supprimé de la session quand les modifications sont persistées. Nous pouvons faire cela à l'aide d'une instance de SessionStatus, que @MVC passera si ajoutée à la signature de la méthode onSubmit :
@RequestMapping(method=RequestMethod.POST)
public void onSubmit(Account account, SessionStatus sessionStatus) {
...
sessionStatus.setComplete(); // Clears @SessionAttributes
}
Customiser le DataBinder
Parfois le data binding exige des customisations. Par exemple, on peut avoir besoin de spécifier des champs obligatoires ou spécifier des PropertyEditors customisés pour les dates, les montants, et autres. Cela est facilement faisable avec @MVC :
@InitBinder
public void initDataBinder(WebDataBinder binder) {
binder.setRequiredFields(new String[] {"number", "name"});
}
Une méthode annotée avec @InitBinder peut accéder à l'instance de DataBinder que @MVC utilise pour lier les paramètres de la requête. Cela nous permet de faire les customisations nécessaires pour chaque contrôleur.
Résultats du Data Binding et Validation
Le data binding peut échouer sur des erreurs telles que des échecs de conversion de type ou des champs manquants. Pour toute erreur rencontrée, nous aimerions retourner au formulaire d'édition et permettre à l'utilisateur de faire des corrections. Pour cela nous ajoutons un objet BindingResult à la signature de la méthode, immédiatement après le "form model object". Voici un exemple :
@RequestMapping(method=RequestMethod.POST)
public ModelAndView onSubmit(Account account, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
ModelAndView mav = new ModelAndView();
mav.getModel().putAll(bindingResult.getModel());
return mav;
}
// Save the changes and redirect to the next view...
}
Dans le cas d'erreurs, nous retournons à la vue d'où nous venons en ajoutant les attributs de BindingResult au modèle afin que les champs spécifiques d'erreur puissent être affichés à l'utilisateur. Remarquez que nous ne spécifions pas de nom de vue explicite. Au lieu de cela, nous permettons à la DispatcherServlet de se rabattre sur un nom de vue par défaut qui correspondra au chemin de l'URI d'entrée.
La validation ne requiert qu'une ligne supplémentaire pour invoquer l'objet Validator, en lui passant le BindingResult. Cela permet de regrouper les erreurs de binding et de validation en un seul endroit :
@RequestMapping(method=RequestMethod.POST)
public ModelAndView onSubmit(Account account, BindingResult bindingResult) {
accountValidator.validate(account, bindingResult);
if (bindingResult.hasErrors()) {
ModelAndView mav = new ModelAndView();
mav.getModel().putAll(bindingResult.getModel());
return mav;
}
// Save the changes and redirect to the next view...
}
Cela conclut notre visite guidée des annotations de la couche Web de Spring 2.5, également connue sous le nom de @MVC.
Résumé
Les annotations dans la couche Web se sont révélées très bénéfiques. Pas seulement parce qu'elles réduisent significativement la quantité de configuration XML, mais aussi parce qu'elles permettent un modèle de programmation élégant, flexible, simple, avec un accès complet à la technologie de contrôleur Spring MVC. Il est vivement recommandé d'utiliser une approche par convention plutôt que par configuration, ainsi qu'une stratégie de délégation des requêtes vers les contrôleurs via un mécanisme de mapping centralisé, afin d'éviter d'avoir des chemins d'URI en dur dans le code source ou de définir des références explicites aux noms des vues.
Enfin, bien que non traitée dans cet article, il convient de mentionner une très importante extension de Spring MVC. La récente version 2 de Spring Web Flow ajoute des fonctionnalités telles que l'intégration de vues JSF dans Spring MVC, une librairie JavaScript Spring et une gestion avancée des états et de la navigation, permettant le pilotage des scénarios d'édition les plus poussés.