Pasando múltiples variables en @RequestBody a un controlador Spring MVC usando Ajax

113

¿Es necesario envolver en un objeto de respaldo? Quiero hacer esto:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody String str1, @RequestBody String str2) {}

Y usa un JSON como este:

{
    "str1": "test one",
    "str2": "two test"
}

Pero en cambio tengo que usar:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody Holder holder) {}

Y luego usa este JSON:

{
    "holder": {
        "str1": "test one",
        "str2": "two test"
    }
}

¿Es eso correcto? Mi otra opción sería la de cambiar el RequestMethodde GETy el uso @RequestParamde la cadena de consulta o uso @PathVariablecon cualquiera RequestMethod.

NimChimpsky
fuente

Respuestas:

92

Tiene razón, se espera que el parámetro anotado @RequestBody contenga todo el cuerpo de la solicitud y se vincule a un objeto, por lo que esencialmente tendrá que ir con sus opciones.

Sin embargo, si desea absolutamente su enfoque, existe una implementación personalizada que puede hacer:

Di que este es tu json:

{
    "str1": "test one",
    "str2": "two test"
}

y desea vincularlo a los dos parámetros aquí:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
public boolean getTest(String str1, String str2)

Primero defina una anotación personalizada, digamos @JsonArg, con la ruta JSON como ruta a la información que desea:

public boolean getTest(@JsonArg("/str1") String str1, @JsonArg("/str2") String str2)

Ahora escriba un HandlerMethodArgumentResolver personalizado que use el JsonPath definido anteriormente para resolver el argumento real:

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.jayway.jsonpath.JsonPath;

public class JsonPathArgumentResolver implements HandlerMethodArgumentResolver{

    private static final String JSONBODYATTRIBUTE = "JSON_REQUEST_BODY";
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(JsonArg.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String body = getRequestBody(webRequest);
        String val = JsonPath.read(body, parameter.getMethodAnnotation(JsonArg.class).value());
        return val;
    }

    private String getRequestBody(NativeWebRequest webRequest){
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        String jsonBody = (String) servletRequest.getAttribute(JSONBODYATTRIBUTE);
        if (jsonBody==null){
            try {
                String body = IOUtils.toString(servletRequest.getInputStream());
                servletRequest.setAttribute(JSONBODYATTRIBUTE, body);
                return body;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return "";

    }
}

Ahora solo registre esto con Spring MVC. Un poco complicado, pero esto debería funcionar limpiamente.

Biju Kunjummen
fuente
2
¿Cómo creo una anotación personalizada, diga @JsonArg, por favor?
Surendra Jnawali
¿Por qué es esto? ahora tengo que crear muchas clases contenedoras diferentes en el backend. Estoy migrando una aplicación Struts2 a Springboot y hubo muchos casos en los que los objetos JSON enviados usando ajax son en realidad dos o más objetos del modelo: por ejemplo, un usuario y una actividad
Jose Ospina
este enlace le muestra "cómo registrar esto con Spring MVC" geekabyte.blogspot.sg/2014/08/…
Bodil
3
Todavía es interesante por qué esta opción no se agrega a la primavera. parece una opción lógica cuando tiene como 2 longs y no suele crear un objeto de envoltura para él
tibi
@SurendraJnawali puedes hacerlo así@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface JsonArg { String value() default ""; }
Epono
88

Si bien es cierto que @RequestBodydebe asignarse a un solo objeto, ese objeto puede ser un Map, por lo que esto le brinda una buena forma de lo que está intentando lograr (no es necesario escribir un objeto de respaldo único):

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody Map<String, String> json) {
   //json.get("str1") == "test one"
}

También puede enlazar al ObjectNode de Jackson si desea un árbol JSON completo:

public boolean getTest(@RequestBody ObjectNode json) {
   //json.get("str1").asText() == "test one"
Kong
fuente
@JoseOspina por qué no puede hacerlo. Cualquier riesgo asociado con Map <String, Object> con requestBody
Ben Cheng
@Ben Quiero decir que puedes usar UN solo Mapobjeto para almacenar cualquier cantidad de objetos dentro de él, pero el objeto de nivel superior debe ser solo uno, no puede haber dos objetos de nivel superior.
Jose Ospina
1
Creo que la desventaja de un enfoque dinámico como Map<String, String>es: las bibliotecas de documentación de API (swagger / springfox, etc.) probablemente no podrán analizar su esquema de solicitud / respuesta desde su código fuente.
stratovarius
10

Puede mezclar el argumento de publicación utilizando la variable body y path para tipos de datos más simples:

@RequestMapping(value = "new-trade/portfolio/{portfolioId}", method = RequestMethod.POST)
    public ResponseEntity<List<String>> newTrade(@RequestBody Trade trade, @PathVariable long portfolioId) {
...
}
alcaudón
fuente
10

Para pasar varios objetos, parámetros, variables, etc. Puede hacerlo dinámicamente usando ObjectNode de la biblioteca jackson como su parámetro. Puedes hacerlo así:

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody ObjectNode objectNode) {
   // And then you can call parameters from objectNode
   String strOne = objectNode.get("str1").asText();
   String strTwo = objectNode.get("str2").asText();

   // When you using ObjectNode, you can pas other data such as:
   // instance object, array list, nested object, etc.
}

Espero esta ayuda.

azwar_akbar
fuente
2

@RequestParames el parámetro HTTP GETo POSTenviado por el cliente, el mapeo de solicitud es un segmento de URL cuya variable:

http:/host/form_edit?param1=val1&param2=val2

var1y var2son parámetros de solicitud.

http:/host/form/{params}

{params}es un mapeo de solicitudes. puede llamar a su servicio como: http:/host/form/usero http:/host/form/firm donde se utilizan firmas y usuarios como Pathvariable.

psisodia
fuente
esto no responde a la pregunta y es incorrecto, no usa una cadena de consulta con solicitudes POST
NimChimpsky
1
@NimChimpsky: seguro que puedes. Una solicitud POST aún puede incluir parámetros en la URL.
Martijn Pieters
2

La solución fácil es crear una clase de carga útil que tenga str1 y str2 como atributos:

@Getter
@Setter
public class ObjHolder{

String str1;
String str2;

}

Y después de que puedas pasar

@RequestMapping(value = "/Test", method = RequestMethod.POST)
@ResponseBody
public boolean getTest(@RequestBody ObjHolder Str) {}

y el cuerpo de su solicitud es:

{
    "str1": "test one",
    "str2": "two test"
}
Nbenz
fuente
1
¿Cuál es el paquete de estas anotaciones? Autoimport ofrecía solo importar jdk.nashorn.internal.objects.annotations.Setter; EDITAR. Supongo que es Lombok projectlombok.org/features/GetterSetter . Corríjame si me equivoco
Gleichmut
@Gleichmut puede usar getters y setter simples para sus variables. Funcionará como esperas.
Gimnath
1

En lugar de usar json, puede hacer cosas simples.

$.post("${pageContext.servletContext.contextPath}/Test",
                {
                "str1": "test one",
                "str2": "two test",

                        <other form data>
                },
                function(j)
                {
                        <j is the string you will return from the controller function.>
                });

Ahora, en el controlador, debe mapear la solicitud ajax de la siguiente manera:

 @RequestMapping(value="/Test", method=RequestMethod.POST)
    @ResponseBody
    public String calculateTestData(@RequestParam("str1") String str1, @RequestParam("str2") String str2, HttpServletRequest request, HttpServletResponse response){
            <perform the task here and return the String result.>

            return "xyz";
}

Espero que esto te ayude.

Japón Trivedi
fuente
1
Eso es json y no funciona. Está especificando requestparam en el método, pero definiendo equestbody con json en la solicitud de publicación de ajax.
NimChimpsky
Vea que no he usado el formato JSON en la llamada ajax. Simplemente he usado dos parámetros de solicitud y en el controlador podemos obtener esos parámetros con la anotación @RequestParam. Está funcionando. Yo uso esto. Pruébalo.
Japan Trivedi
Lo he intentado, es el punto de la pregunta. No funciona así.
NimChimpsky
Especifique exactamente qué ha probado. Muestre eso en su pregunta. Creo que tienes un requisito diferente al que yo he entendido.
Japan Trivedi
1
Funcionó para mí en el primer intento. ¡Gracias!
Humppakäräjät
1

He adaptado la solución de Biju:

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.io.IOUtils;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;


public class JsonPathArgumentResolver implements HandlerMethodArgumentResolver{

    private static final String JSONBODYATTRIBUTE = "JSON_REQUEST_BODY";

    private ObjectMapper om = new ObjectMapper();

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(JsonArg.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String jsonBody = getRequestBody(webRequest);

        JsonNode rootNode = om.readTree(jsonBody);
        JsonNode node = rootNode.path(parameter.getParameterName());    

        return om.readValue(node.toString(), parameter.getParameterType());
    }


    private String getRequestBody(NativeWebRequest webRequest){
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);

        String jsonBody = (String) webRequest.getAttribute(JSONBODYATTRIBUTE, NativeWebRequest.SCOPE_REQUEST);
        if (jsonBody==null){
            try {
                jsonBody = IOUtils.toString(servletRequest.getInputStream());
                webRequest.setAttribute(JSONBODYATTRIBUTE, jsonBody, NativeWebRequest.SCOPE_REQUEST);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return jsonBody;

    }

}

¿Cuál es la diferencia?

  • Estoy usando Jackson para convertir json
  • No necesito un valor en la anotación, puede leer el nombre del parámetro fuera del MethodParameter
  • También leí el tipo de parámetro del Methodparameter => por lo que la solución debería ser genérica (la probé con cadenas y DTO)

BR

usuario3227576
fuente
0

El parámetro de solicitud existe tanto para GET como para POST, para Get se agregará como cadena de consulta a la URL, pero para POST está dentro del Cuerpo de la solicitud

Kaleem
fuente
0

No estoy seguro de dónde agrega el json, pero si lo hago así con angular, funciona sin el requestBody: angluar:

    const params: HttpParams = new HttpParams().set('str1','val1').set('str2', ;val2;);
    return this.http.post<any>( this.urlMatch,  params , { observe: 'response' } );

Java:

@PostMapping(URL_MATCH)
public ResponseEntity<Void> match(Long str1, Long str2) {
  log.debug("found: {} and {}", str1, str2);
}
tibi
fuente
0

Bueno. Sugiero crear un objeto de valor (Vo) que contenga los campos que necesita. El código es más sencillo, no cambiamos el funcionamiento de Jackson y es aún más fácil de entender. ¡Saludos!

Matias Zamorano
fuente
0

Puedes lograr lo que quieras usando @RequestParam. Para ello debes hacer lo siguiente:

  1. Declare los parámetros RequestParams que representan sus objetos y establezca la requiredopción en falso si desea poder enviar un valor nulo.
  2. En la interfaz, codifique los objetos que desea enviar e inclúyalos como parámetros de solicitud.
  3. En el backend, convierta las cadenas JSON en los objetos que representan usando Jackson ObjectMapper o algo así, ¡y listo!

Lo sé, es un truco pero funciona. ;)

Maurice
fuente
0

también puede @RequestBody Map<String, String> paramsusarlo, luego usarlo params.get("key")para obtener el valor del parámetro

Será
fuente
0

También puede usar un mapa de MultiValue para mantener el requestBody adentro. Aquí está el ejemplo.

    foosId -> pathVariable
    user -> extracted from the Map of request Body 

a diferencia de la anotación @RequestBody cuando usamos un mapa para contener el cuerpo de la solicitud, necesitamos anotar con @RequestParam

y enviar al usuario en Json RequestBody

  @RequestMapping(value = "v1/test/foos/{foosId}", method = RequestMethod.POST, headers = "Accept=application"
            + "/json",
            consumes = MediaType.APPLICATION_JSON_UTF8_VALUE ,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public String postFoos(@PathVariable final Map<String, String> pathParam,
            @RequestParam final MultiValueMap<String, String> requestBody) {
        return "Post some Foos " + pathParam.get("foosId") + " " + requestBody.get("user");
    }
anayagam
fuente