Spring MVC: objeto complexo como GET @RequestParam

191

Suponha que eu tenha uma página que lista os objetos em uma tabela e preciso colocar um formulário para filtrar a tabela. O filtro é enviado como um Ajax GET para um URL assim: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z

E em vez de ter muitos parâmetros no meu controlador, como:

@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 supondo que eu tenha o MyObject como:

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

    //Getters and setters
    ...
}

Eu quero fazer algo como:

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

É possível? Como eu posso fazer isso?

renanleandrof
fonte
1
Tente usar '@ModelAttribute' combinado com '@RequestMapping', onde MyObject será seu modelo.
Michal
@michal +1. Aqui estão alguns tutoriais mostrando como fazer isso: Spring 3 MVC: Manipulando Formulários no Spring 3.0 MVC , O que é e como usar@ModelAttribute , Spring MVC Form Handling Example . Basta pesquisar no " Spring MVC form handling " e você terá vários tutoriais / exemplos. Mas não se esqueça de usar a maneira moderna de manipulação de formulários, ou seja, Primavera v2.5 +
informatik01
Também é útil: O que há @ModelAttributeno Spring MVC
informatik01

Respostas:

247

Você pode fazer isso absolutamente, basta remover a @RequestParamanotação, o Spring vinculará claramente os parâmetros de solicitação à sua instância de classe:

public @ResponseBody List<MyObject> myAction(
    @RequestParam(value = "page", required = false) int page,
    MyObject myObject)
Biju Kunjummen
fonte
8
Biju, você pode dar um exemplo de como chamar isso? Estou fazendo uma chamada HTTP GET básica para a API Rest e ela não possui formas sofisticadas.
bschandramohan
32
Observe que o Spring, por padrão, solicita getters / setters para o MyObject para vinculá-lo automaticamente. Caso contrário, ele não ligará o myObject.
31414
20
Como você pode definir os campos como opcionais / não opcionais no MyObject? Não tenho certeza como encontrar a documentação adequada para este ..
worldsayshi
6
@Biju, Existe uma maneira de controlar os valores padrão e exigidos para MyObjectisso, da mesma forma que podemos fazer com @RequestParam?
Alexey19
7
@BijuKunjummen Como atualizar os MyObjectparâmetros de consulta para aceitar no caso Snake e mapeá-lo para um membro do caso camel MyObject. Por exemplo, ?book_id=4deve ser mapeado com o bookIdmembro do MyObject?
Vivek Vardhan
55

Vou adicionar um pequeno exemplo de mim.

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

Solicitar mapeamento dentro da classe do controlador:

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

Inquerir:

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

Resultado:

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

Espero que ajude :)

UPDATE / KOTLIN

Como atualmente estou trabalhando muito com o Kotlin, se alguém quiser definir um DTO semelhante, a classe no Kotlin deve ter o seguinte formato:

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

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

Com a dataclasse como esta:

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

o Spring (testado na inicialização) retorna o seguinte erro para solicitação mencionada na resposta:

"Falha ao converter o valor do tipo 'java.lang.String []' para o tipo obrigatório 'java.lang.Long []'; a exceção aninhada é java.lang.NumberFormatException: para a sequência de entrada: \" 353,234 \ ""

A classe de dados funcionará apenas no seguinte formulário de parâmetros de solicitação:

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

Esteja ciente disso!

Przemek Nowak
fonte
1
é possível definir "obrigatório" para campos dto?
normal
4
Sugiro tentar com os validadores do Spring MVC. Exemplo: codejava.net/frameworks/spring/…
Przemek Nowak
É muito curioso que isso não exija uma anotação! Gostaria de saber, existe uma anotação explícita para isso, embora desnecessária?
James Watkins
8

Eu tenho um problema muito similar. Na verdade, o problema é mais profundo como eu pensava. Estou usando jquery, $.postque usa Content-Type:application/x-www-form-urlencoded; charset=UTF-8como padrão. Infelizmente, baseei meu sistema nisso e quando precisei de um objeto complexo, @RequestParamnão consegui fazer isso acontecer.

No meu caso, estou tentando enviar preferências do usuário com algo como;

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

No lado do cliente, os dados brutos reais enviados ao servidor são;

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

analisado como;

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

e o lado do servidor é;

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

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

Tentei @ModelAttribute, adicionei setter / getters, construtores com todas as possibilidades, UserPreferencesmas sem chance, pois reconheciam os dados enviados como 5 parâmetros, mas, na verdade, o método mapeado possui apenas 2 parâmetros. Eu também tentei a solução de Biju, no entanto, o que acontece é que, o spring cria um objeto UserPreferences com o construtor padrão e não preenche os dados.

Resolvi o problema enviando uma string JSon das preferências do lado do cliente e lida com ele como se fosse uma String no lado do servidor;

cliente:

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

servidor:

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

Para resumir, fiz a conversão manualmente dentro do método REST. Na minha opinião, a razão pela qual a primavera não reconhece os dados enviados é o tipo de conteúdo.

hevi
fonte
1
Acabei de experimentar o mesmo problema e o resolvi da mesma maneira. A propósito, você encontrou uma solução melhor a partir de hoje?
Shay Elkayam
1
Estou tendo exatamente o mesmo problema também. Eu fiz uma solução alternativa mais limpa usando@RequestMapping(method = POST, path = "/settings/{projectId}") public void settings(@PathVariable String projectId, @RequestBody ProjectSettings settings)
Petr Újezdský
4

Como a pergunta sobre como definir campos obrigatórios aparece em cada postagem, escrevi um pequeno exemplo sobre como definir campos conforme necessário:

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?";
}
FluffyDestroyerOfCode
fonte
0

Embora as respostas que se referem a @ModelAttribute, @RequestParam, @PathParame os gostos são válidas, há uma pequena pegadinha eu corri para. O parâmetro do método resultante é um proxy que o Spring envolve em seu DTO. Portanto, se você tentar usá-lo em um contexto que exija seu próprio tipo personalizado, poderá obter alguns resultados inesperados.

O seguinte não funcionará:

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

No meu caso, tentar usá-lo na ligação de Jackson resultou em um com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

Você precisará criar um novo objeto a partir do dto.

DKO
fonte
0

Sim, você pode fazer isso de uma maneira simples. Veja abaixo o código das linhas.

URL - http: // localhost: 8080 / get / request / multiple / param / by / map? Name = 'abc' & id = '123'

 @GetMapping(path = "/get/request/header/by/map")
    public ResponseEntity<String> getRequestParamInMap(@RequestParam Map<String,String> map){
        // Do your business here 
        return new ResponseEntity<String>(map.toString(),HttpStatus.OK);
    } 
vkstream
fonte
0

A resposta aceita funciona como um encanto, mas se o objeto tiver uma lista de objetos, não funcionará conforme o esperado, então aqui está a minha solução após algumas escavações.

Seguindo este conselho de discussão , aqui está como eu fiz.

  • Frontend: especifique seu objeto e codifique-o na base64 para envio.
  • Back-end: decodifique a string base64 e converta a string json no objeto desejado.

Não é o melhor para depurar sua API com o carteiro, mas está funcionando conforme o esperado para mim.

Objeto original: {página: 1, tamanho: 5, filtros: [{field: "id", valor: 1, comparação: "EQ"}

Objeto codificado: 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 aqui o 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
)
Gabriel Brito
fonte