[JAVA] Je veux comprendre le flux des paramètres de demande de traitement Spring

Je me suis parfois demandé ce qui était arrivé au processus de liaison d'un paramètre de requête en utilisant @ ModelAttribute à un objet, je vais donc résumer les résultats. Il n'est souvent pas mentionné dans la documentation et les résultats de la lecture du code source sont résumés.

Vous pouvez également obtenir des paramètres de requête avec @ RequestParam, mais cette fois, c'est hors de portée.

environnement

@ ModelAttribute peut être utilisé

--Méthode

En fait, il n'est pas nécessaire d'ajouter @ ModelAttribute à l'argument de la méthode Handler. Dans le comportement par défaut, si vous spécifiez une classe jugée «false» dans «BeanUtils.isSimpleProperty» dans l'argument de la méthode Handler, le comportement est le même que lorsque «@ ModelAttribute» est ajouté. Plus précisément, à part les types primitifs et leurs wrappers, ʻEnum, String, CharSequence, Number, Date, ʻURI, ʻURL, Locale, Class, arrays, etc. une sorte de. Cependant, il a la priorité la plus basse, donc si un autre HandlerMethodArgumentResolver` peut résoudre l'argument, il y sera résolu.

ModelAttributeMethodProcessor

Dans Spring, HandlerAdapter appelle la méthode Handler. Il existe plusieurs classes d'implémentation, mais si vous utilisez @ RequestMapiing, RequestMappingHandlerAdapter est responsable de l'appel de la méthode Handler. C'est cette classe qui résout la valeur passée à l'argument de la méthode Handler et gère la valeur de retour, et il est agréable de spécifier Model ou RedirectAttributes comme argument de la méthode Handler. Travaille dur.

Donc, c'est le rôle de la classe d'implémentation de HandlerMethodArgumentResolver de résoudre réellement la valeur passée à l'argument, et quand @ ModelAttribute est spécifié, ModelAttributeMethodProcessor est en train de traiter.

Comportement lorsque @ ModelAttribute est ajouté à la méthode

Ce n'était pas le sujet principal, mais je résumerai également le fonctionnement de la méthode avec @ ModelAttribute.

Le processus de stockage de la valeur de retour de la méthode dans Model est effectué avant d'exécuter la méthode Handler. (Strictement parlant, il s'agit de ModelMap dans ModelAndViewContainer, mais cela semble déroutant, donc je l'appellerai Model.) Par exemple, supposons que vous créez le contrôleur suivant.

@Controller
public class HelloController {}
    @ModelAttribute
    public User setUp() {
        return new User();
    }

    @GetMapping("/")
    public String index() {
        return "hello";
    }
}

Le comportement à ce moment-là est le suivant. (À proprement parler, ce n'est qu'une image.)

@Controller
public class HelloController {
    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute(new User());
        return "hello";
    }
}

Lorsque le nom de l'attribut est spécifié dans @ ModelAttriubte comme indiqué ci-dessous,

@ModelAttribute("hoge")
public User setUp() {
    return new User();
}

L'image ajoutée au premier argument de model.addAttribute ().

@GetMapping("/")
public String index(Model model) {
    model.addAttribute("hoge", new User());
    return "index";
}

Au fait, c'est la classe appelée ModelFactory qui appelle la méthode avec @ ModelAttribute.

Comportement lorsque @ ModelAttribute est ajouté à l'argument de la méthode Handler

Le sujet principal est d'ici.

Obtenir et créer des objets

Tout d'abord, récupérez l'objet spécifié à partir de Model. Si «valeur» ou «nom» est spécifié pour «@ ModelAttribute», la clé d'obtention de «Model» peut être spécifiée. S'il est omis, Spring est automatiquement déterminé à partir du nom de la classe.

Il existe plusieurs modèles qui peuvent être obtenus à partir de Model, tels que le stockage dans Model avec la méthode avec @ ModelAttribute mentionnée ci-dessus, en utilisant la portée Flash au moment de la redirection ou en définissant la portée de la session avec @ SessionAttribute. Si vous l'utilisez, il peut être stocké dans Model avant d'exécuter la méthode Handler (ou plutôt avant de résoudre la valeur passée à l'argument).

Si l'objet ne peut pas être obtenu à partir de «Model», un objet est créé. S'il y a un constructeur, il sera utilisé, s'il y a plusieurs constructeurs, le constructeur par défaut sera utilisé, mais s'il n'y a pas de constructeur par défaut à ce moment, il mourra. Lors de l'utilisation d'un constructeur avec des arguments, le processus de liaison des paramètres de demande est inséré, ce qui sera décrit plus loin.

Demander la liaison de paramètres

Liez la valeur du paramètre de demande à l'objet acquis ou généré. Une valeur est liée au champ de l'objet qui correspond au nom du paramètre de demande.

Remplace la valeur de l'objet, mais ne fait rien pour les valeurs non incluses dans les paramètres de la demande.

@Controller
public class HelloController {
    @ModelAttribute
    public User setUp() {
        User user = new User();
        user.setName("Nom");
        user.setEmail("Email");
        return user;
    }

    @GetMapping("/")
    public String index(User user) {
        return "hello";
    }
}

Supposons que ʻUser ait des champs nom et ʻemail. Ensuite, comme indiqué ci-dessous, seul «nom» est ajouté au paramètre de requête et envoyé.

curl http://localhost:8080?name=hogehoge

Le nom de l'objet ʻUser devient hogehoge, et ʻemail devient mail, qui n'est pas écrasé par null.

Lors de la création d'un objet avec un constructeur avec des paramètres

Lors de la création d'un objet à l'aide d'un constructeur avec des paramètres, essayez de lier les données à ce moment. Dans ce cas, liez le paramètre de demande qui correspond à la valeur spécifiée dans le nom du paramètre du constructeur ou «@ ConstructorProperties» au lieu du nom du champ.

public class User {
    private String name;
    private String email;

    public User(String n, String e) {
        this.name = n;
        this.email = e;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

Dans le cas du constructeur ci-dessus, le nom du paramètre de requête doit être «n» ou «e».

public class User {
    private String name;
    private String email;

    @ConstructorProperties({"name", "email"})
    public User(String n, String e) {
        this.name = n;
        this.email = e;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

Si vous utilisez @ ConstructorProperties comme ci-dessus, le nom du paramètre de requête peut être name ou ʻemail. S'il y a @ ConstructorProperties, il sera prioritaire, donc il ne fonctionnera pas avec n ou ʻe.

Cependant, une fois la création de l'objet terminée, le même processus de liaison du paramètre de requête que d'habitude est exécuté, donc s'il y a un setter ou si vous utilisez DataBinder qui permet un accès direct au champ, cela Le résultat de est prioritaire.

Par exemple, si vous créez la classe suivante

public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name + "hoge";
        this.email = email + "fuga";
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

Dans le constructeur, «hoge» et «fuga» sont donnés et conservés dans le champ, mais comme setter est exécuté après cela, «hoge» et «fuga» sont attribués à la valeur pour être finalement liés. Cela n'a pas été le cas.

Supprimer la liaison de paramètre de demande

En changeant binding of @ ModelAttribute en false, il est possible de supprimer la liaison des paramètres de requête.

@Controller
public class HelloController {
    @ModelAttribute
    public User setUp() {
        User user = new User();
        user.setName("Nom");
        user.setEmail("Email");
        return user;
    }

    @GetMapping("/")
    public String index(@ModelAttribute(binding = false) User user) {
        return "hello";
    }
}

Même si la requête suivante est envoyée dans l'état ci-dessus, le "nom" de l'objet de ʻUser reste nom, et ʻemail reste mail.

curl http://localhost:8080?name=hogehoge&email=fugafuga

Validation

Effectuez la validation lorsqu'une validation est requise, par exemple lorsque «@ Validated» est ajouté à l'argument.

À la suite de la validation, s'il y a une erreur et qu'il n'y a pas d'erreurs dans l'argument immédiatement après la méthode Handler, BindingException est levée, et si c'est le cas, le résultat est conservé comme BindingResult. (ʻErrors est l'interface parente de BindingResult`)

Au fait, lorsque vous définissez BindingResult comme argument dans la méthode Handler, vous devez faire attention dans l'ordre à cause de l'implémentation ici.

L'histoire tourne mal, mais si vous définissez BindingResult sur l'argument de la méthode Handler, la classe ʻErrorsMethodArgumentResolverrésout la valeur de l'argument. Cette classe vérifie si le dernier élément stocké dansModel est BindingResult, et si c'est le cas, le définit dans l'argument. Par conséquent, il peut ne pas fonctionner correctement à moins qu'il ne soit exécuté immédiatement après avoir effectué la validation et le stockage de BindingResult dans Model. Par conséquent, il semble que l'argument de la méthode Handler doit avoir BindingResult immédiatement après l'argument avec @ Validated`.

Après avoir étudié cela, j'ai pensé qu'il était possible de réaliser un code qui divise l'objet qui lie le paramètre de requête en deux et obtient le résultat de validation de chacun comme suit. Le résultat de validation de ʻUser est stocké dans result1, et le résultat de validation de ʻOtherObj est stocké dans result2.

@Controller
public class HelloController {
    @GetMapping("/")
    public String index(@Validated User user, BindingResult result1, @Validated OtherObj other, BindingResult result2) {

        return "index";
    }
}

Lors de l'utilisation d'un constructeur avec des paramètres

Lors de l'utilisation d'un constructeur avec des paramètres, la conversion de type est effectuée si nécessaire au moment de la création de l'objet. À ce moment-là, si un champ qui ne peut pas être converti de type est inclus, le résultat est conservé comme «BindingResult», mais le traitement de liaison et la validation ultérieurs ne sont pas effectués. En d'autres termes, le traitement de liaison de paramètre de demande par l'installateur et la vérification de champ par Bean Validation ne sont pas effectués.

public class User {
    @NotEmpty
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    //getter omis
}

@Controller
public class HelloController {
    @GetMapping("/")
    public String index(@Validated User user, BindingResult result) {
        return "index";
    }
}

Lorsque vous créez une classe comme celle ci-dessus, envoyez la demande suivante.

curl http://localhost:8080?age=hogehoge

Le résultat de validation initialement attendu est que «nom» est vide et «âge» ne peut pas être converti en «entier», mais seul ce dernier peut être détecté. Les deux peuvent être détectés en supprimant le constructeur paramétré et en le modifiant pour utiliser le constructeur et le setter par défaut.

Stocker dans le modèle

Stockez l'objet créé par le processus ci-dessus et «BindingResult» dans «Model». Si le nom de l'attribut est spécifié dans @ ModelAttribute, utilisez cette valeur comme clé.

à la fin

Je vois. Lorsque vous utilisez un constructeur avec des paramètres, vous devez être prudent car l'opération peut être légèrement différente.

Recommended Posts

Je veux comprendre le flux des paramètres de demande de traitement Spring
Je veux contrôler le message d'erreur par défaut de Spring Boot
Je veux var_dump le contenu de l'intention
[Spring Boot] J'ai étudié comment implémenter le post-traitement de la demande reçue.
05. J'ai essayé de supprimer la source de Spring Boot
J'ai essayé de réduire la capacité de Spring Boot
Je veux connaître la réponse de l'application Janken
Je souhaite afficher le nom de l'affiche du commentaire
Contrôlez le flux de traitement Spring Batch avec JavaConfig.
Je veux voir le contenu de Request sans dire quatre ou cinq
Je veux retourner la position de défilement de UITableView!
Je souhaite modifier le paramètre de sortie du journal de UtilLoggingJdbcLogger
[Ruby] Je souhaite inverser l'ordre de la table de hachage
L'histoire de Collectors.groupingBy que je veux garder pour la postérité
Je veux limiter l'entrée en réduisant la plage de nombres
Je veux changer la valeur de l'attribut dans Selenium of Ruby
J'ai étudié le traitement interne de Retrofit
Je veux connaître la méthode du contrôleur où l'exception a été levée dans le ExceptionHandler de Spring Boot
Je veux afficher des images avec REST Controller de Java et Spring!
Je veux connaître le JSP du portlet ouvert lors du développement de Liferay
[Administrateur actif] Je souhaite personnaliser le traitement de création et de mise à jour par défaut
[Ruby] Je souhaite extraire uniquement la valeur du hachage et uniquement la clé
Confirmation et refactoring du flux de la requête au contrôleur dans [httpclient]
J'ai examiné le flux de communication TCP avec Spring Integration (édition client)
J'ai examiné le flux de communication TCP avec Spring Integration (édition serveur)
Je veux passer l'argument d'Annotation et l'argument de la méthode d'appel à aspect
Je veux obtenir le nom de champ du champ [Java]. (Vieux ton d'histoire)
[Rails] Articles pour les débutants pour organiser et comprendre le flux de form_with
Je veux que vous utilisiez Enum # name () pour la clé de SharedPreference
L'histoire de la montée de la série Spring Boot 1.5 à la série 2.1
Je veux tronquer après la virgule décimale
Je souhaite effectuer un traitement d'agrégation avec spring-batch
Je veux obtenir la valeur en Ruby
Partie 2: Comprendre (approximativement) le flux de processus de la connexion OAuth 2.0 prise en charge par Spring Security 5
Je veux obtenir la valeur de Cell de manière transparente quel que soit CellType (Apache POI)
03. J'ai envoyé une demande de Spring Boot à l'API de recherche de code postal
Partie 3: Comprendre (en profondeur) le flux de processus de la connexion OAuth 2.0 prise en charge par Spring Security 5
Je veux obtenir récursivement la superclasse et l'interface d'une certaine classe
Je veux appeler une méthode d'une autre classe
[JavaScript] Je souhaite limiter le traitement en vérifiant les éléments d'entrée lorsque le temps spécifié s'écoule lors de la mise au point.
[Java] Je souhaite calculer la différence par rapport à la date
Je veux intégrer n'importe quel TraceId dans le journal
J'ai essayé de vérifier le fonctionnement de la requête http (Put) avec Talented API Tester
Comprenez en lisant l'article! Résumé des contenus que les débutants d'Elasticsearch souhaitent supprimer
Comprendre les caractéristiques de Scala en 5 minutes (Introduction à Scala)
Je veux juger la gamme en utilisant le diplôme mensuel
Je souhaite utiliser le mode sombre avec l'application SWT