Passando várias variáveis ​​em @RequestBody para um controlador Spring MVC usando Ajax

112

É necessário embrulhar em um objeto de apoio? Eu quero fazer isso:

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

E use um JSON como este:

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

Mas, em vez disso, preciso usar:

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

E então use este JSON:

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

Isso está correto? Minha outra opção seria mudar RequestMethodpara GETe usar @RequestParamna string de consulta ou usar @PathVariablecom qualquer um deles RequestMethod.

NimChimpsky
fonte

Respostas:

92

Você está correto, o parâmetro anotado @RequestBody deve conter todo o corpo da solicitação e vincular-se a um objeto, portanto, você essencialmente terá que seguir suas opções.

Se você realmente deseja sua abordagem, há uma implementação personalizada que você pode fazer:

Diga que este é o seu json:

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

e você deseja vinculá-lo aos dois parâmetros aqui:

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

Primeiro defina uma anotação personalizada, digamos @JsonArg, com o caminho JSON como o caminho para as informações que você deseja:

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

Agora escreva um HandlerMethodArgumentResolver personalizado que usa o JsonPath definido acima para resolver o 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 "";

    }
}

Agora, basta registrar isso com Spring MVC. Um pouco envolvido, mas isso deve funcionar de forma limpa.

Biju Kunjummen
fonte
2
Como faço para criar uma anotação personalizada, diga @JsonArg, por favor?
Surendra Jnawali
Por que é isso? agora tenho que criar várias classes de wrapper diferentes no back-end. Estou migrando um aplicativo Struts2 para Springboot e havia muitos casos em que objetos JSON enviados usando ajax são na verdade dois ou mais objetos do modelo: por exemplo, um usuário e uma atividade
Jose Ospina
este link mostra "como registrar isso no Spring MVC" geekabyte.blogspot.sg/2014/08/…
Bodil
3
ainda interessante porque essa opção não foi adicionada à primavera. parece uma opção lógica quando você tem 2 longos e não costuma criar um objeto wrapper para ele
tibi
@SurendraJnawali você pode fazer assim@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface JsonArg { String value() default ""; }
Epono
87

Embora seja verdade que @RequestBodydeve ser mapeado para um único objeto, esse objeto pode ser um Map, então isso fornece a você um bom caminho para o que você está tentando alcançar (não há necessidade de escrever um objeto de apoio único):

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

Você também pode vincular ao ObjectNode de Jackson se quiser uma árvore JSON completa:

public boolean getTest(@RequestBody ObjectNode json) {
   //json.get("str1").asText() == "test one"
Kong
fonte
@JoseOspina porque não pode fazer isso. Qualquer risco associado a Map <String, Object> com requestBody
Ben Cheng
@Ben, quero dizer que você pode usar UM único Mapobjeto para armazenar qualquer número de objetos dentro dele, mas o objeto de nível superior ainda deve ser apenas um, não pode haver dois objetos de nível superior.
Jose Ospina
1
Acho que a desvantagem de uma abordagem dinâmica como Map<String, String>é: bibliotecas de documentação de API (swagger / springfox etc) provavelmente não serão capazes de analisar seu esquema de solicitação / resposta de seu código-fonte.
stratovarius
10

Você pode misturar o argumento post usando body e path variable para tipos de dados mais simples:

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

Para passar vários objetos, parâmetros, variáveis ​​e assim por diante. Você pode fazer isso dinamicamente usando ObjectNode da biblioteca jackson como seu parâmetro. Você pode fazer desta forma:

@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 ajuda.

azwar_akbar
fonte
2

@RequestParamé o parâmetro HTTP GETou POSTenviado pelo cliente, o mapeamento da solicitação é um segmento da URL cuja variável:

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

var1e var2são parâmetros de solicitação.

http:/host/form/{params}

{params}é um mapeamento de solicitação. você pode chamar seu serviço como: http:/host/form/userou http:/host/form/firm onde firma e usuário são usados ​​como Pathvariable.

psisódia
fonte
isso não responde à pergunta e está errado, você não usa uma string de consulta com solicitações POST
NimChimpsky
1
@NimChimpsky: claro que você pode. Uma solicitação POST ainda pode incluir parâmetros no URL.
Martijn Pieters
2

A solução fácil é criar uma classe de carga útil que tenha str1 e str2 como atributos:

@Getter
@Setter
public class ObjHolder{

String str1;
String str2;

}

E depois você pode passar

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

e o corpo da sua solicitação é:

{
    "str1": "test one",
    "str2": "two test"
}
Nbenz
fonte
1
Qual é o pacote dessas anotações? A importação automática oferecida apenas importa jdk.nashorn.internal.objects.annotations.Setter; EDITAR. Presumo que seja o Lombok projectlombok.org/features/GetterSetter . Corrija-me se eu estiver errado
Gleichmut
@Gleichmut você pode usar getters e setter simples para suas variáveis. Vai funcionar como você espera.
Gimnath
1

Em vez de usar json, você pode fazer coisas 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.>
                });

Agora, no controlador, você precisa mapear a solicitação ajax conforme abaixo:

 @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 isso ajude você.

Japan Trivedi
fonte
1
Isso é json e não funciona. Você está especificando requestparam no método, mas definindo equestbody com json na solicitação de postagem ajax.
NimChimpsky de
Veja, eu não usei o formato JSON na chamada ajax. Eu simplesmente usei dois parâmetros de solicitação e no controlador podemos obter esses parâmetros com a anotação @RequestParam. Está funcionando. Eu uso isso. Apenas tente.
Japan Trivedi,
Eu tentei isso, é o ponto da pergunta. Não funciona assim.
NimChimpsky,
Especifique exatamente o que você tentou. Mostre isso em sua pergunta. Eu acho que você tem requisitos diferentes do que eu entendi.
Japan Trivedi
1
Funcionou para mim na primeira tentativa. Obrigado!
Humppakäräjät
1

Eu adaptei a solução do 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;

    }

}

Qual é a diferença:

  • Estou usando Jackson para converter json
  • Eu não preciso de um valor na anotação, você pode ler o nome do parâmetro fora do MethodParameter
  • Eu também li o tipo do parâmetro do parâmetro Method => então a solução deve ser genérica (eu testei com string e DTOs)

BR

user3227576
fonte
0

o parâmetro de solicitação existe para GET e POST, para Get será anexado como string de consulta ao URL, mas para POST está dentro do corpo da solicitação

Kaleem
fonte
0

Não tenho certeza de onde você adiciona o json, mas se eu fizer assim com o angular, ele funcionará sem o 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
fonte
0

Boa. Eu sugiro criar um Value Object (Vo) que contenha os campos de que você precisa. O código é mais simples, não alteramos o funcionamento do Jackson e fica ainda mais fácil de entender. Saudações!

Matias Zamorano
fonte
0

Você pode conseguir o que deseja usando @RequestParam. Para isso, você deve fazer o seguinte:

  1. Declare os parâmetros RequestParams que representam seus objetos e defina o required opção como false se desejar enviar um valor nulo.
  2. No frontend, crie uma string dos objetos que deseja enviar e inclua-os como parâmetros de solicitação.
  3. No backend, transforme as strings JSON de volta nos objetos que representam usando Jackson ObjectMapper ou algo parecido, e voila!

Eu sei, é um pouco hack, mas funciona! ;)

Maurice
fonte
0

você também pode @RequestBody Map<String, String> paramsusar params.get("key")para obter o valor do parâmetro

Vai
fonte
0

Você também pode usar um Mapa MultiValue para conter o requestBody. Aqui está o exemplo para ele.

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

ao contrário da anotação @RequestBody ao usar um mapa para conter o corpo da solicitação, precisamos anotar com @RequestParam

e enviar ao usuário no 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
fonte