Spring MVC: oggetto complesso come GET @RequestParam


193

Supponiamo di avere una pagina che elenca gli oggetti su una tabella e che devo inserire un modulo per filtrare la tabella. Il filtro viene inviato come Ajax GET a un URL del genere: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z

E invece di avere molti parametri sul mio controller come:

@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) { ... }

E supponiamo di avere MyObject come:

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

    //Getters and setters
    ...
}

Voglio fare qualcosa del tipo:

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

È possibile? Come lo posso fare?


1
Prova a utilizzare "@ModelAttribute" in combinazione con "@RequestMapping", in cui MyObject sarà il tuo modello.
michal,

@michal +1. Ecco un paio di tutorial che mostrano come farlo: Spring 3 MVC: Gestione dei moduli in Spring 3.0 MVC , Che cos'è e come utilizzare@ModelAttribute , Spring MVC Form Handling Example . Basta google " Gestione dei moduli Spring MVC " e otterrai tantissimi tutorial / esempi. Ma assicurati di utilizzare il modo moderno di gestione dei moduli, ovvero Spring v2.5 +
informatik01

Risposte:


250

Puoi assolutamente farlo, basta rimuovere l' @RequestParamannotazione, Spring legherà in modo pulito i parametri della tua richiesta all'istanza della classe:

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

8
Biju, puoi fare un esempio di come chiamare questo? Sto effettuando una chiamata HTTP GET di base all'API Rest e non ha moduli elaborati.
bschandramohan,

33
Si noti che Spring per impostazione predefinita richiede getter / setter affinché MyObject lo associ automaticamente. Altrimenti non vincolerà myObject.
aka_sh

21
Come puoi impostare i campi come facoltativi / non opzionali in MyObject? Non sono sicuro di come trovare la documentazione adeguata per questo ..
worldsayshi,

6
@Biju, c'è un modo per controllare i valori predefiniti e richiesti per MyObjectallora, in modo simile possiamo fare con @RequestParam?
Alexey,

7
@BijuKunjummen Come posso aggiornare MyObjectper accettare i parametri della query nel caso Snake e mapparlo a un membro del caso cammello di MyObject. Ad esempio, ?book_id=4dovrebbe essere mappato con il bookIdmembro del MyObject?
Vivek Vardhan,

55

Aggiungerò un breve esempio da parte mia.

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);
    }
}

Richiesta di mappatura all'interno della classe controller:

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

Query:

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

Risultato:

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

Spero possa essere d'aiuto :)

AGGIORNAMENTO / KOTLIN

Perché attualmente sto lavorando molto con Kotlin se qualcuno vuole definire un DTO simile la classe in Kotlin dovrebbe avere la seguente forma:

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

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

Con la dataclasse come questa:

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

Spring (testato in Boot) restituisce il seguente errore per la richiesta menzionata nella risposta:

"Impossibile convertire il valore del tipo 'java.lang.String []' nel tipo richiesto 'java.lang.Long []'; l'eccezione nidificata è java.lang.NumberFormatException: per la stringa di input: \" 353.234 \ ""

La classe di dati funzionerà solo per il seguente modulo di parametri di richiesta:

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

Sii consapevole di questo!


2
è possibile impostare "obbligatorio" su campi dto?
Normale

4
Suggerisco di provare con i validatori Spring MVC. Esempio: codejava.net/frameworks/spring/…
Przemek Nowak,

È molto curioso che ciò non richieda un'annotazione! Mi chiedo, c'è un'annotazione esplicita per questo seppur inutile?
James Watkins,

8

Ho un problema molto simile. In realtà il problema è più profondo come pensavo. Sto usando jquery $.postche utilizza Content-Type:application/x-www-form-urlencoded; charset=UTF-8come predefinito. Sfortunatamente ho basato il mio sistema su questo e quando avevo bisogno di un oggetto complesso come@RequestParam non potevo semplicemente farlo accadere.

Nel mio caso sto cercando di inviare le preferenze dell'utente con qualcosa di simile;

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

Sul lato client sono i dati grezzi effettivi inviati al server;

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

analizzato come;

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

e il lato server è;

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

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

Ho provato @ModelAttribute, aggiunto setter / getter, costruttori con tutte le possibilitàUserPreferences ma nessuna possibilità in quanto ha riconosciuto i dati inviati come 5 parametri ma in realtà il metodo mappato ha solo 2 parametri. Ho anche provato la soluzione di Biju, tuttavia ciò che accade è che spring crea un oggetto UserPreferences con costruttore predefinito e non inserisce i dati.

Ho risolto il problema inviando una stringa JSon delle preferenze dal lato client e gestendola come se fosse una stringa sul lato server;

cliente:

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

server:

@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();
        }
}

in breve, ho eseguito manualmente la conversione all'interno del metodo REST. A mio avviso, il motivo per cui Spring non riconosce i dati inviati è il tipo di contenuto.


1
Ho appena riscontrato lo stesso problema e risolto allo stesso modo. A proposito, hai trovato una soluzione migliore ad oggi?
Shay Elkayam,

1
Sto riscontrando esattamente lo stesso problema. Ho fatto una soluzione più pulita usando@RequestMapping(method = POST, path = "/settings/{projectId}") public void settings(@PathVariable String projectId, @RequestBody ProjectSettings settings)
Petr Újezdský il

4

Poiché la domanda su come impostare i campi obbligatori viene visualizzata sotto ogni post, ho scritto un piccolo esempio su come impostare i campi come richiesto:

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

Mentre le risposte che fanno riferimento a @ModelAttribute, @RequestParam, @PathParame simili sono valide, c'è un piccolo Gotcha mi sono imbattuto in. Il parametro del metodo risultante è un proxy che Spring avvolge il DTO. Pertanto, se si tenta di utilizzarlo in un contesto che richiede il proprio tipo personalizzato, è possibile ottenere risultati imprevisti.

Quanto segue non funzionerà:

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

Nel mio caso, il tentativo di utilizzarlo nel collegamento Jackson ha portato a com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

Sarà necessario creare un nuovo oggetto da dto.



0

La risposta accettata funziona come un incantesimo ma se l'oggetto ha un elenco di oggetti non funzionerà come previsto, quindi ecco la mia soluzione dopo alcuni scavi.

Seguendo questo consiglio di discussione , ecco come ho fatto.

  • Frontend: stringi il tuo oggetto piuttosto che codificalo in base64 per l'invio.
  • Backend: decodifica la stringa base64 quindi converti la stringa json nell'oggetto desiderato.

Non è il migliore per il debug dell'API con Postman ma funziona come previsto per me.

Oggetto originale: {pagina: 1, dimensione: 5, filtri: [{campo: "id", valore: 1, confronto: "EQ"}

Oggetto codificato: 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)
}

E qui SearchFilterDTO e 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
)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.