Spring MVC: objeto complejo como GET @RequestParam


193

Supongamos que tengo una página que enumera los objetos en una tabla y necesito poner un formulario para filtrar la tabla. El filtro se envía como Ajax GET a una URL como esa: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z

Y en lugar de tener muchos parámetros en mi 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) { ... }

Y suponiendo que tengo MyObject como:

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

    //Getters and setters
    ...
}

Quiero hacer algo como:

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

¿Es posible? ¿Cómo puedo hacer eso?


1
Intente usar '@ModelAttribute' combinado con '@RequestMapping', donde MyObject será su modelo.
michal

@michal +1. Aquí hay un par de tutoriales que muestran cómo hacerlo: Spring 3 MVC: Manejo de formularios en Spring 3.0 MVC , Qué es y cómo usar@ModelAttribute , Ejemplo de manejo de formularios Spring MVC . Simplemente busque en Google " Spring MVC form handle " y obtendrá un montón de tutoriales / ejemplos. Pero asegúrese de utilizar una forma moderna de manejo de formularios, es decir, Spring v2.5 +
informatik01

Respuestas:


250

Puede hacerlo absolutamente, solo elimine la @RequestParamanotación, Spring vinculará limpiamente sus parámetros de solicitud a su instancia de clase:

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

8
Biju, ¿puedes dar un ejemplo de cómo llamar a esto? Estoy haciendo una llamada HTTP GET básica a la API Rest y no tiene formas sofisticadas.
bschandramohan

33
Tenga en cuenta que Spring por defecto requiere getters / setters para que MyObject lo enlace automáticamente. De lo contrario, no se unirá al myObject.
aka_sh

21
¿Cómo puede establecer que los campos sean opcionales / no opcionales en MyObject? No está seguro de cómo encontrar la documentación apropiada para este ..
worldsayshi

66
@Biju, ¿Hay alguna manera de controlar los valores predeterminados y necesarios para MyObjectentonces, de una manera similar que podemos hacer con @RequestParam?
Alexey

77
@BijuKunjummen ¿Cómo puedo actualizar MyObjectpara aceptar los parámetros de consulta en el caso de Snake y asignarlo a un miembro de caso de camello MyObject? Por ejemplo, ¿ ?book_id=4debería mapearse con un bookIdmiembro de MyObject?
Vivek Vardhan

55

Agregaré un pequeño ejemplo de mí.

La clase 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 mapeo dentro de la clase de controlador:

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

Consulta:

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 ayude :)

ACTUALIZACIÓN / KOTLIN

Porque actualmente estoy trabajando mucho con Kotlin si alguien quiere definir un DTO similar, la clase en Kotlin debería tener la siguiente forma:

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

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

Con la dataclase como esta:

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

Spring (probado en el arranque) devuelve el siguiente error para la solicitud mencionada en la respuesta:

"Error al convertir el valor del tipo 'java.lang.String []' al tipo requerido 'java.lang.Long []'; la excepción anidada es java.lang.NumberFormatException: Para la cadena de entrada: \" 353,234 \ ""

La clase de datos solo funcionará para el siguiente formulario de parámetros de solicitud:

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

¡Sé consciente de esto!


2
¿es posible establecer "requerido" en los campos dto?
Normal

44
Sugiero probar con los validadores Spring MVC. Ejemplo: codejava.net/frameworks/spring/…
Przemek Nowak

¡Es muy curioso que esto no requiera una anotación! Me pregunto, ¿hay alguna anotación explícita para esto, aunque innecesaria?
James Watkins el

8

Tengo un problema muy similar. En realidad, el problema es más profundo como pensaba. Estoy usando jquery $.postque usa Content-Type:application/x-www-form-urlencoded; charset=UTF-8por defecto. Desafortunadamente, basé mi sistema en eso y cuando necesitaba un objeto complejo como @RequestParamno podía simplemente hacerlo realidad.

En mi caso, estoy tratando de enviar preferencias de usuario con algo como;

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

En el lado del cliente, los datos en bruto reales enviados al servidor son;

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

analizado como;

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

y el lado del servidor es;

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

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

Intenté @ModelAttribute, agregué setter / getters, constructores con todas las posibilidades, UserPreferencespero sin posibilidad, ya que reconoció los datos enviados como 5 parámetros, pero de hecho, el método mapeado tiene solo 2 parámetros. También probé la solución de Biju, sin embargo, lo que sucede es que spring crea un objeto UserPreferences con el constructor predeterminado y no completa los datos.

Resolví el problema enviando una cadena JSon de las preferencias desde el lado del cliente y manejarlo como si fuera una cadena en el lado del 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, hice la conversión manualmente dentro del método REST. En mi opinión, la razón por la cual Spring no reconoce los datos enviados es el tipo de contenido.


1
Acabo de experimentar el mismo problema y lo resolví de la misma manera. Por cierto, ¿encontraste una solución mejor a partir de hoy?
Shay Elkayam

1
Estoy teniendo exactamente el mismo problema también. Hice una solución más limpia usando@RequestMapping(method = POST, path = "/settings/{projectId}") public void settings(@PathVariable String projectId, @RequestBody ProjectSettings settings)
Petr Újezdský

4

Dado que la pregunta sobre cómo configurar los campos obligatorios aparece debajo de cada publicación, escribí un pequeño ejemplo sobre cómo configurar los campos según sea necesario:

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

Mientras que las respuestas que se refieren a @ModelAttribute, @RequestParam, @PathParamy los gustos son válidos, hay un pequeño Gotcha me encontré. El parámetro del método resultante es un proxy que Spring envuelve su DTO. Por lo tanto, si intenta usarlo en un contexto que requiere su propio tipo personalizado, puede obtener algunos resultados inesperados.

Lo siguiente no funcionará:

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

En mi caso, intentar usarlo en la unión de Jackson resultó en a com.fasterxml.jackson.databind.exc.InvalidDefinitionException.

Deberá crear un nuevo objeto desde el dto.



0

La respuesta aceptada funciona a las mil maravillas, pero si el objeto tiene una lista de objetos, no funcionará como se esperaba, así que aquí está mi solución después de excavar.

Siguiendo este consejo de hilo , así es como lo he hecho.

  • Frontend: stringifique su objeto que lo codifique en base64 para enviarlo.
  • Backend: decodifica la cadena base64 y luego convierte la cadena json en el objeto deseado.

No es lo mejor para depurar su API con cartero, pero funciona como se esperaba para mí.

Objeto original: {página: 1, tamaño: 5, filtros: [{campo: "id", valor: 1, comparación: "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)
}

Y aquí el SearchFilterDTO y el 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
)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.