Contrôleurs dans Spring

Gérer les requêtes à l'aide des contrôleurs

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

L'article consacré au Spring dans l'architecture MVC2 a introduit les notions du contrôleur, de modèle de vue et de vue. Cette troisième partie d'articles sera destiné à les approfondir. Elle montrera plus en détails quelles technologies sont utilisées dans notre application de gestion de la bibliothèque. Elle traitera aussi des problématiques plus liées aux applications web.

Créer un contrôleur sous Spring

Un contrôleur sera défini dans notre application par l'annotation @Controller, placée avant le nom de la classe. Pour l'instant cela a l'air assez simple. Cependant, une petite complication se rajoute au niveau de la configuration du DispatcherServlet. On doit y préciser le package dans lequel se trouvent les contrôleurs, comme ici :

<context:component-scan base-package="library.controller"/>

Ce bout de code XML permet au Spring d'auto-détecter les composants. En occurrence, il s'agit des contrôleurs qui se trouvent dans le package library.controller. Voici l'exemple d'un contrôleur simple, qui ne fait qu'afficher une vue :

package library.controller;

// imports are ommitted

@Controller
public class IndexController extends FrontendController {
    // code ommitted
    @RequestMapping(value = "/access-denied", method = {RequestMethod.POST, RequestMethod.GET})
    public String accessDenied() {
        return "accessDenied";
    }
    // code ommitted
}

Le code est simple. Pour chaque URI "/access-denied", quelque soit sa méthode d'appel (POST ou GET), on invoque la fonction accessDenied() de la classe IndexController. Cette fonction retourne le modèle de vue "accessDenied", défini dans une liste des modèles de vue gérée par Apache Tiles. On abordera l'aspect des vues dans l'article consacré à des vues dans Spring.

Une nouveauté dans le code ci-dessus, pas encore présentée, est l'annotation @RequestMapping. Son rôle principal est de préciser quelle méthode doit être appelée pour une URI donnée. L'URI est exprimée par l'attribut value<. La méthode d'appel est définie dans le paramètre method. Ce dernier peut prendre soit une seule valeur, soit plusieurs valeurs. Maintenant qu'on a abordé cet aspect, regardons quelles peuvent être ses autres formes :

package library.controller;

// imports are ommitted

@RequestMapping("/categories")
@Controller
public class CategoryController {

    // code ommitted

    @RequestMapping(value = "{category}", method = RequestMethod.GET)
    public String listBooksByCategory(@PathVariable String category /* other parameters are ommitted */) {
        // body code is ommitted
        return "listBooksByCategory";
    }
    // code ommitted

Quelles sont les différences avec la définition précédente ? Tout d'abord on remarque la présence d'un @RequestMapping au-dessus de l'annotation @Controller. Grâce à elle on s'assure que toutes les URI commençant par "/categories" seront traités par ce contrôleur. Concernant @RequestMapping au-dessus de la méthode, son valeur sera concaténée au @RequestMapping du contrôleur. Un URI "/categories/drama" va alors être géré par la méthode listBooksByCategory de la classe CategoryController.

Une deuxième différence repose dans la définition de l'URI. Il ne prend pas une valeur statique mais dynamique. "{category}" est une String qui peut aussi bien correspondre aux mots "drama" que "books-from-19-century". Pour savoir qu'il s'agit d'une chaîne de caractères, on se base sur l'annotation @PathVariable, passée dans les paramètres de la méthode. On remarque que son nom (String category) est identique que celui de la variable spécifiée dans l'URI. Si l'on voudrait utiliser la nomination différente, il faudrait la préciser dans le paramètre du @PathVariable comme suit : @PathVariable("category") String categoryNameFromUri.

Réponses d'un contrôleur

D'habitude un contrôleur va retourner le modèle de vue qui sera par la suite transformée et affichée sur l'écran de l'utilisateur. C'est le cas dans les exemples évoqués plus haut. Cependant, il peut retourner d'autres choses qu'une page web.

// code ommitted
    @RequestMapping(value = "/delete/{categoryId}/{langId}/{type}", method = RequestMethod.GET)
    public @ResponseBody Map delete(@PathVariable long categoryId, @PathVariable long langId, @PathVariable String type) {
       // code ommitted
       Map jsonMap = new HashMap();
       return jsonMap;
    }
// code ommitted

En occurrence, la réponse attendue sera une chaîne de caractères en format JSON. Afin que cela soit possible, il faut définir un message converter dans le fichier de configuration du DispatcherServlet :

<mvc:annotation-driven conversion-service="conversionService">
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

Comment Spring sait qu'en occurrence il doit retourner un JSON et non pas un modèle de vue ? La détection du message converter à utiliser s'effectue à travers la valeur de l'en-tête HTTP Content-type. Par défaut, MappingJacksonHttpMessageConverter est invoqué à chaque fois que cette valeur est application/json.

D'autres message converters existent. Pour les types text/xml ou application/xml on peut utiliser MarshallingHttpMessageConverter. En ce qui concerne le type text/plain, on peut faire appel à StringHttpMessageConverter. Cependant, on n'est pas obligé de se limiter à cela. Au moment de configurer les message converters on peut préciser quel type de contenu correspond auquel converter, comme dans l'exemple ci-dessous, non compris dans la projet d'exemple qu'on développe :

<mvc:annotation-driven conversion-service="conversionService">
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
		       <property name="supportedMediaTypes">
		           <list>
		               <value>application/json</value>
		               <value>application/javascript</value>
		           </list>
		       </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

Dans cet exemple chaque requête ayant comme Content-Type application/json ou application/javascript, sera transformée par MappingJacksonHttpMessageConverter.

TODO : aborder la présence du ModelAndView

Redirections sous Spring

Spring permet également de retourner les réponses non sous forme de modèles de vue mais sous forme de redirections. Cette réponse est également sous format d'un String. On peut le constater sur l'exemple de notre méthode qui sert à valider ou pas les modifications apportées au compte d'un client connecté :

// imports are ommitted
@Controller
public class SubscriberController extends FrontendController {
    // code ommitted
    public String modifyAccountDo(@ModelAttribute("subscriber") @Validated({SubscriberAccountCheck.class}) Subscriber subscriber, BindingResult binRes, @LoggedUser AuthenticationFrontendUserDetails user, Model layout, RedirectAttributes redAtt) {
        // body isn't important for now
        return "redirect:/account/modify";
    }
}

Le code exécuté est tout simple. La redirection est signalée par le mot "redirect:". Ensuite il suffit de déterminer l'URI que doit chercher Spring après la redirection.

Le forward se fait de la même manière. A la différence de redirection, on utiliser le mot "forward:" à la place du "redirect:". Aucun des contrôleurs utilisés par l'exemple de l'application Spring ne forwarde pas des requêtes. Malgré cela, il est important de rappeler la différence entre ces deux actions.

Dans le cas d'une redirection, une nouvelle requête est créée. Elle est ensuite envoyée à une nouvelle adresse et analysée par le contrôleur correspondant à cette nouvelle URI. Les paramètres de la requête d'origine ne sont pas automatiquement transmises à la deuxième requête. Donc par exemple si l'on reçoit une requête POST contenant un paramètre "name", on doit l'inclure à nouveau dans la nouvelle requête.

Concernant un forward, la même requête est traitée par un contrôleur différent que celui d'origine. On peut imaginer une situation où l'on envoie une requête qui devrait être analysée par la méthode notifyLogout() du contrôleur UserController. Cependant, pour des utilisateurs enregistrés en tant qu'une entreprise, on veut cette requête soit manipulée par la méthode notifyLogoutCompany() du même contrôleur. On désire de garder la même requête (paramètres, en-tête...) pour cette deuxième fonction. Dans ce cas, on utilisera le forward.

Les annotations personnalisées dans les contrôleurs

Précédemment on a vu que des annotations peuvent faire partie des paramètres passés dans les méthodes des contrôleurs. On a constaté cela avec @PathVariable, utilisé dans notre cas pour récupérer la valeur des paramètres GET transmis dans la requête HTTP. Cependant, on n'est pas obligés à se limiter à des annotations mises à disposition par Spring. On peut en créer d'autres, ce qu'illustre l'exemple ci-dessous :

afficher le code

Comment cela fonctionne ? Grâce à la partie de configuration du DispatcherServlet qui permet de spécifier argument resolvers. La configuration utilisée dans notre application est la suivante (comprend les messages converters connus dans le paragraphe précédent) :
	<mvc:annotation-driven conversion-service="conversionService">
        <mvc:argument-resolvers>
            <bean class="library.resolver.UserDetailsResolver" />
        </mvc:argument-resolvers>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

Pour détecter quel argument resolver utiliser, Spring appele la méthode supportsParameter() de chaque bean< défini dans la partie argument-resolvers. Si la méthode supportsParameter de la classe retourne true, la fonction resolveArgument() est alors appelée. Sinon, il continue à chercher l'argument resolver adéquat.

Une question ? Une remarque ?

*

*

Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil MySQL

Qu'est-ce que signifie l'attribut UNSIGNED ?

Si notre base stocke uniquement les valeurs supérieures à 0, il faut utiliser un attribut qui aggrandit le nombre des chiffres possibles à sauvegarder. Il s'agit de l'attribut UNSIGNED qu'on peut appliquer aux champs numériques. Prenons l'exemple d'un champ type BIGINT. Normalement il occupe 8 octets et stocke les nombres entiers entre -9 223 372 036 854 775 808 à 9 223 372 036 854 775 807. Cependant, cela s'avère très peu utile lorsqu'on est sûr d'avoir uniquement les entiers positifs. En rajoutant l'attribut UNSIGNED à ce champ on double sa capacité de stockage. Alors il est capable de contenir les chiffres de 0 à 18 446 744 073 709 551 615.