Spring MVC: objet complexe comme GET @RequestParam


193

Supposons que j'ai une page qui répertorie les objets sur une table et que j'ai besoin de mettre un formulaire pour filtrer la table. Le filtre est envoyé en tant que GET Ajax à une URL comme celle-ci: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z

Et au lieu d'avoir beaucoup de paramètres sur mon contrôleur comme:

@RequestMapping(value = "/action")
public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    @RequestParam(value = "prop1", required = false) String prop1,
    @RequestParam(value = "prop2", required = false) String prop2,
    @RequestParam(value = "prop3", required = false) String prop3) { ... }

Et en supposant que j'ai MyObject comme:

public class MyObject {
    private String prop1;
    private String prop2;
    private String prop3;

    //Getters and setters
    ...
}

Je veux faire quelque chose comme:

@RequestMapping(value = "/action")
public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    @RequestParam(value = "myObject", required = false) MyObject myObject,) { ... }

C'est possible? Comment puis je faire ça?


1
Essayez d'utiliser '@ModelAttribute' combiné avec '@RequestMapping', où MyObject sera votre modèle.
michal

@michal +1. Voici quelques didacticiels montrant comment faire cela: Spring 3 MVC: Gestion des formulaires dans Spring 3.0 MVC , Qu'est-ce que c'est et comment utiliser@ModelAttribute , Exemple de gestion de formulaire Spring MVC . Il suffit de google " Gestion des formulaires Spring MVC " et vous obtiendrez une tonne de tutoriels / exemples. Mais assurez-vous d'utiliser un moyen moderne de gestion des formulaires, c'est-à-dire Spring v2.5 +
informatik01

Réponses:


250

Vous pouvez absolument le faire, supprimez simplement l' @RequestParamannotation, Spring liera proprement vos paramètres de requête à votre instance de classe:

public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    MyObject myObject)

8
Biju, pouvez-vous donner un exemple de comment appeler cela? Je fais un appel http GET de base à l'API Rest et il n'a pas de formes sophistiquées.
bschandramohan

33
Notez que Spring demande par défaut aux getters / setters que MyObject le lie automatiquement. Sinon, il ne liera pas le myObject.
aka_sh

21
Comment pouvez-vous définir des champs facultatifs / non facultatifs dans MyObject? Je ne sais pas comment trouver la documentation appropriée pour cela ..
worldsayshi

6
@Biju, existe-t-il un moyen de contrôler les valeurs par défaut et requises pour cela MyObject, de la même manière que nous pouvons le faire avec @RequestParam?
Alexey

7
@BijuKunjummen Comment puis-je mettre MyObjectà jour le pour accepter les paramètres de requête dans le cas Snake et le mapper à un membre de cas de chameau MyObject. Par exemple, ?book_id=4doit être mappé avec le bookIdmembre du MyObject?
Vivek Vardhan

55

J'ajouterai un petit exemple de ma part.

La classe DTO:

public class SearchDTO {
    private Long id[];

    public Long[] getId() {
        return id;
    }

    public void setId(Long[] id) {
        this.id = id;
    }
    // reflection toString from apache commons
    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

Demande de mappage à l'intérieur de la classe de contrôleur:

@RequestMapping(value="/handle", method=RequestMethod.GET)
@ResponseBody
public String handleRequest(SearchDTO search) {
    LOG.info("criteria: {}", search);
    return "OK";
}

Requete:

http://localhost:8080/app/handle?id=353,234

Résultat:

[http-apr-8080-exec-7] INFO  c.g.g.r.f.w.ExampleController.handleRequest:59 - criteria: SearchDTO[id={353,234}]

J'espère que cela aide :)

MISE À JOUR / KOTLIN

Parce qu'actuellement, je travaille beaucoup avec Kotlin si quelqu'un veut définir un DTO similaire, la classe dans Kotlin devrait avoir la forme suivante:

class SearchDTO {
    var id: Array<Long>? = arrayOf()

    override fun toString(): String {
        // to string implementation
    }
}

Avec la dataclasse comme celle-ci:

data class SearchDTO(var id: Array<Long> = arrayOf())

le Spring (testé dans Boot) renvoie l'erreur suivante pour la requête mentionnée dans la réponse:

"Impossible de convertir la valeur de type 'java.lang.String []' en type requis 'java.lang.Long []'; l'exception imbriquée est java.lang.NumberFormatException: Pour la chaîne d'entrée: \" 353,234 \ ""

La classe de données fonctionnera uniquement pour le formulaire de paramètres de requête suivant:

http://localhost:8080/handle?id=353&id=234

Soyez conscient de cela!


2
est-il possible de définir "obligatoire" pour les champs dto?
Normal

4
Je suggère d'essayer avec les validateurs Spring MVC. Exemple: codejava.net/frameworks/spring/…
Przemek Nowak

C'est très curieux que cela ne nécessite pas d'annotation! Je me demande, y a-t-il une annotation explicite pour cela, bien que inutile?
James Watkins

8

J'ai un problème très similaire. En fait, le problème est plus profond que je le pensais. J'utilise jquery $.postqui utilise Content-Type:application/x-www-form-urlencoded; charset=UTF-8par défaut. Malheureusement, j'ai basé mon système sur cela et quand j'avais besoin d'un objet complexe, @RequestParamje ne pouvais pas simplement le réaliser.

Dans mon cas, j'essaye d'envoyer les préférences de l'utilisateur avec quelque chose comme;

 $.post("/updatePreferences",  
    {id: 'pr', preferences: p}, 
    function (response) {
 ...

Du côté client, les données brutes réelles envoyées au serveur sont;

...
id=pr&preferences%5BuserId%5D=1005012365&preferences%5Baudio%5D=false&preferences%5Btooltip%5D=true&preferences%5Blanguage%5D=en
...

analysé comme;

id:pr
preferences[userId]:1005012365
preferences[audio]:false
preferences[tooltip]:true
preferences[language]:en

et le côté serveur est;

@RequestMapping(value = "/updatePreferences")
public
@ResponseBody
Object updatePreferences(@RequestParam("id") String id, @RequestParam("preferences") UserPreferences preferences) {

    ...
        return someService.call(preferences);
    ...
}

J'ai essayé @ModelAttribute, ajouté des setter / getters, des constructeurs avec toutes les possibilités UserPreferencesmais aucune chance car il reconnaissait les données envoyées comme 5 paramètres mais en fait la méthode mappée n'a que 2 paramètres. J'ai également essayé la solution de Biju, mais ce qui se passe, c'est que Spring crée un objet UserPreferences avec le constructeur par défaut et ne remplit pas les données.

J'ai résolu le problème en envoyant la chaîne JSon des préférences du côté client et je la gère comme si c'était une chaîne côté serveur;

client:

 $.post("/updatePreferences",  
    {id: 'pr', preferences: JSON.stringify(p)}, 
    function (response) {
 ...

serveur:

@RequestMapping(value = "/updatePreferences")
public
@ResponseBody
Object updatePreferences(@RequestParam("id") String id, @RequestParam("preferences") String preferencesJSon) {


        String ret = null;
        ObjectMapper mapper = new ObjectMapper();
        try {
            UserPreferences userPreferences = mapper.readValue(preferencesJSon, UserPreferences.class);
            return someService.call(userPreferences);
        } catch (IOException e) {
            e.printStackTrace();
        }
}

en bref, j'ai fait la conversion manuellement dans la méthode REST. À mon avis, la raison pour laquelle Spring ne reconnaît pas les données envoyées est le type de contenu.


1
Je viens de rencontrer le même problème et je l'ai résolu de la même manière. Au fait, avez-vous trouvé une meilleure solution dès aujourd'hui?
Shay Elkayam

1
J'ai exactement le même problème aussi. J'ai fait une solution de contournement plus propre en utilisant@RequestMapping(method = POST, path = "/settings/{projectId}") public void settings(@PathVariable String projectId, @RequestBody ProjectSettings settings)
Petr Újezdský

4

Étant donné que la question sur la façon de définir les champs obligatoires apparaît sous chaque message, j'ai écrit un petit exemple sur la façon de définir les champs comme requis:

public class ExampleDTO {
    @NotNull
    private String mandatoryParam;

    private String optionalParam;

    @DateTimeFormat(iso = ISO.DATE) //accept Dates only in YYYY-MM-DD
    @NotNull
    private LocalDate testDate;

    public String getMandatoryParam() {
        return mandatoryParam;
    }
    public void setMandatoryParam(String mandatoryParam) {
        this.mandatoryParam = mandatoryParam;
    }
    public String getOptionalParam() {
        return optionalParam;
    }
    public void setOptionalParam(String optionalParam) {
        this.optionalParam = optionalParam;
    }
    public LocalDate getTestDate() {
        return testDate;
    }
    public void setTestDate(LocalDate testDate) {
        this.testDate = testDate;
    }
}

@RequestMapping(value = "/test", method = RequestMethod.GET)
public String testComplexObject (@Valid ExampleDTO e){
    System.out.println(e.getMandatoryParam() + " " + e.getTestDate());
    return "Does this work?";
}

0

Alors que les réponses qui font référence à @ModelAttribute, @RequestParam, @PathParamet les goûts sont valides, il y a un petit Gotcha je suis tombé. Le paramètre de méthode résultant est un proxy que Spring enveloppe votre DTO. Ainsi, si vous essayez de l'utiliser dans un contexte qui nécessite votre propre type personnalisé, vous pouvez obtenir des résultats inattendus.

Les éléments suivants ne fonctionneront pas:

@GetMapping(produces = APPLICATION_JSON_VALUE)
public ResponseEntity<CustomDto> request(@ModelAttribute CustomDto dto) {
    return ResponseEntity.ok(dto);
}

Dans mon cas, essayer de l'utiliser dans la liaison Jackson a abouti à un com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

Vous devrez créer un nouvel objet à partir du dto.



0

La réponse acceptée fonctionne comme un charme, mais si l'objet a une liste d'objets, cela ne fonctionnera pas comme prévu, voici donc ma solution après quelques recherches.

En suivant ces conseils de fil , voici comment j'ai fait.

  • Frontend: stringify votre objet que de l'encoder en base64 pour soumission.
  • Backend: décodez la chaîne base64 puis convertissez la chaîne json en objet souhaité.

Ce n'est pas le meilleur pour déboguer votre API avec postman, mais cela fonctionne comme prévu pour moi.

Objet d'origine: {page: 1, taille: 5, filtres: [{champ: "id", valeur: 1, comparaison: "EQ"}

Objet codé: eyJwYWdlIjoxLCJzaXplIjo1LCJmaWx0ZXJzIjpbeyJmaWVsZCI6ImlkUGFyZW50IiwiY29tcGFyaXNvbiI6Ik5VTEwifV19

@GetMapping
fun list(@RequestParam search: String?): ResponseEntity<ListDTO> {
    val filter: SearchFilterDTO = decodeSearchFieldDTO(search)
    ...
}

private fun decodeSearchFieldDTO(search: String?): SearchFilterDTO {
    if (search.isNullOrEmpty()) return SearchFilterDTO()
    return Gson().fromJson(String(Base64.getDecoder().decode(search)), SearchFilterDTO::class.java)
}

Et ici les SearchFilterDTO et FilterDTO

class SearchFilterDTO(
    var currentPage: Int = 1,
    var pageSize: Int = 10,
    var sort: Sort? = null,
    var column: String? = null,
    var filters: List<FilterDTO> = ArrayList<FilterDTO>(),
    var paged: Boolean = true
)

class FilterDTO(
    var field: String,
    var value: Any,
    var comparison: Comparison
)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.