Map a JSON POST to Multiple Spring MVC Parameters – 将一个JSON POST映射到多个Spring MVC参数上

最后修改: 2022年 9月 19日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

When using Spring’s default support for JSON deserialization, we’re forced to map the incoming JSON to a single request handler parameter. Sometimes, however, we’d prefer a more fine-grained method signature.

当使用Spring对JSON反序列化的默认支持时,我们不得不将传入的JSON映射到一个请求处理程序参数。然而,有时我们更喜欢一个更细化的方法签名。

In this tutorial, we will learn how to use a custom HandlerMethodArgumentResolver to deserialize a JSON POST into multiple strongly-typed parameters.

在本教程中,我们将学习如何使用一个自定义的HandlerMethodArgumentResolver来将JSON POST反序列化为多个强类型的参数。

2. The Problem

2.问题

First, let’s look at the limitations of Spring MVC’s default approach to JSON deserialization.

首先,我们来看看Spring MVC默认的JSON反序列化方法的局限性。

2.1. The Default @RequestBody Behavior

2.1.默认的@RequestBody行为

Let’s start with an example JSON body:

让我们从一个JSON体的例子开始。

{
   "firstName" : "John",
   "lastName"  :"Smith",
   "age" : 10,
   "address" : {
      "streetName" : "Example Street",
      "streetNumber" : "10A",
      "postalCode" : "1QW34",
      "city" : "Timisoara",
      "country" : "Romania"
   }
}

Next, let’s create DTOs that match the JSON input:

接下来,让我们创建与JSON输入相匹配的DTO>。

public class UserDto {
    private String firstName;
    private String lastName;
    private String age;
    private AddressDto address;

    // getters and setters
}
public class AddressDto {

    private String streetName;
    private String streetNumber;
    private String postalCode;
    private String city;
    private String country;

    // getters and setters
}

Finally, we’ll use the standard approach for deserializing our JSON request into a UserDto using the @RequestBody annotation:

最后,我们将使用标准方法,使用@RequestBody注解将我们的JSON请求反序列化为一个UserDto

@Controller
@RequestMapping("/user")
public class UserController {

    @PostMapping("/process")
    public ResponseEntity process(@RequestBody UserDto user) {
        /* business processing */
        return ResponseEntity.ok()
            .body(user.toString());
    }
}

2.2. Limitations

2.2.限制条件

The primary benefit of the standard solution above is that we don’t have to deserialize the JSON POST into a UserDto object manually.

上述标准解决方案的主要好处是,我们不必手动将JSON POST反序列化为一个UserDto对象。

However, the entire JSON POST must be mapped to a single request parameter. This means we have to create a separate POJO for each expected JSON structure, polluting our code base with classes used solely for this purpose.

然而,整个JSON POST必须映射到一个请求参数。这意味着我们必须为每个预期的JSON结构创建一个单独的POJO,用仅用于此目的的类污染我们的代码库。

That consequence is especially evident when we only need a subset of the JSON properties. In our request handler above, we only need the user’s firstName and city properties, but we’re forced to deserialize an entire UserDto.

当我们只需要JSON属性的一个子集时,这种后果就特别明显了。在我们上面的请求处理程序中,我们只需要用户的firstNamecity属性,但我们被迫反序列化整个UserDto

While Spring allows us to use Map or ObjectNode as a parameter rather than a homegrown DTO, both are single-parameter options. As with a DTO, everything is packaged together. Since the Map and ObjectNode contents are String values, we must marshal them into objects ourselves. These options save us from declaring single-use DTOs but create even more complexity.

虽然Spring允许我们使用MapObjectNode作为参数,而不是自制的DTO,但两者都是单参数选项。与DTO一样,所有东西都被打包在一起。由于MapObjectNode的内容是String值,我们必须自己把它们打包成对象。这些选项使我们免于声明一次性使用的DTO,但却创造了更多的复杂性。

3. Custom HandlerMethodArgumentResolver

3.自定义HandlerMethodArgumentResolver

Let’s look at a solution to the limitations above. We can use Spring MVC’s HandlerMethodArgumentResolver to allow us to declare just the desired JSON attributes as parameters in our request handler.

让我们来看看解决上述限制的方法。我们可以使用Spring MVC的HandlerMethodArgumentResolver来允许我们在请求处理程序中只声明所需的JSON属性作为参数。

3.1. Creating the Controller

3.1.创建控制器

First, let’s create a custom annotation we can use to map a request handler parameter to a JSON path:

首先,让我们创建一个自定义注解,我们可以用它来映射请求处理程序参数到JSON路径。

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonArg {
    String value() default "";
}

Next, we’ll create a request handler that uses the annotation to map firstName and city as separate parameters that correlate to properties from our JSON POST body:

接下来,我们将创建一个请求处理程序,使用注解将firstNamecity映射为单独的参数,与我们的JSON POST主体的属性相关。

@Controller
@RequestMapping("/user")
public class UserController {
    @PostMapping("/process/custom")
    public ResponseEntity process(@JsonArg("firstName") String firstName,
      @JsonArg("address.city") String city) {
        /* business processing */
        return ResponseEntity.ok()
            .body(String.format("{\"firstName\": %s, \"city\" : %s}", firstName, city));
    }
}

3.2. Creating the Custom HandlerMethodArgumentResolver

3.2.创建自定义的HandlerMethodArgumentResolver

After Spring MVC has decided which request handler should handle an incoming request, it attempts to resolve the parameters automatically. This includes iterating through all beans in the Spring context that implement the HandlerMethodArgumentResolver interface in case that can resolve any parameters Spring MVC can’t do automatically.

在Spring MVC决定哪个请求处理程序应该处理传入的请求后,它试图自动解决参数。这包括遍历Spring上下文中所有实现HandlerMethodArgumentResolver接口的Bean,以防止Spring MVC不能自动解决的参数。

Let’s define an implementation of HandlerMethodArgumentResolver that will process all request handler parameters annotated with @JsonArg:

让我们定义一个HandlerMethodArgumentResolver的实现,它将处理所有用@JsonArg注释的请求处理器参数。

public class JsonArgumentResolver implements HandlerMethodArgumentResolver {

    private static final String JSON_BODY_ATTRIBUTE = "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 jsonPath = Objects.requireNonNull(
          Objects.requireNonNull(parameter.getParameterAnnotation(JsonArg.class)).value());
        Class<?> parameterType = parameter.getParameterType();
        return JsonPath.parse(body).read(jsonPath, parameterType);
    }

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

Spring uses the supportsParameter() method to check whether this class can resolve a given parameter. Since we want our handler to process any parameter annotated with @JsonArg, we return true if the given parameter has that annotation.

Spring使用supportsParameter() 方法来检查这个类是否可以解决一个给定的参数。因为我们希望我们的处理程序可以处理任何带有@JsonArg注释的参数,如果给定的参数有这个注释,我们就返回true

Next, in the resolveArgument() method, we extract the JSON body and then attach it as an attribute to the request so we can access it directly for subsequent calls. We then grab the JSON path from the @JsonArg annotation and use reflection to get the parameter’s type. With the JSON path and the parameter type information, we can deserialize discrete parts of the JSON body into rich objects.

接下来,在resolveArgument()方法中,我们提取JSON主体,然后将其作为属性附加到请求中,这样我们就可以在后续调用中直接访问它。然后我们从@JsonArg注解中获取JSON路径,并使用反射来获取参数的类型。有了JSON路径和参数类型信息,我们可以将JSON主体的离散部分反序列化为丰富的对象。

3.3. Registering the Custom HandlerMethodArgumentResolver

3.3.注册自定义HandlerMethodArgumentResolver

For Spring MVC to use our JsonArgumentResolver, we need to register it:

为了让Spring MVC使用我们的JsonArgumentResolver,我们需要注册它。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        JsonArgumentResolver jsonArgumentResolver = new JsonArgumentResolver();
        argumentResolvers.add(jsonArgumentResolver);
    }
}

Our JsonArgumentResolver will now process all request handler parameters annotated with @JsonArgs. We’ll need to ensure the @JsonArgs value is a valid JSON path, but that is a lighter process than the @RequestBody approach that requires a separate POJO for every JSON structure.

我们的JsonArgumentResolver现在将处理所有用@JsonArgs注释的请求处理程序参数。我们需要确保@JsonArgs值是一个有效的JSON路径,但这比@RequestBody方法更容易处理,因为该方法需要为每个JSON结构单独制作POJO。

3.4. Using Parameters With Custom Types

3.4.在自定义类型中使用参数

To show that this will work with custom Java classes as well, let’s define a request handler with strongly-typed POJO parameters:

为了说明这对自定义的Java类也有作用,让我们定义一个带有强类型POJO参数的请求处理程序。

@PostMapping("/process/custompojo")
public ResponseEntity process(
  @JsonArg("firstName") String firstName, @JsonArg("lastName") String lastName,
  @JsonArg("address") AddressDto address) {
    /* business processing */
    return ResponseEntity.ok()
      .body(String.format("{\"firstName\": %s, \"lastName\": %s, \"address\" : %s}",
        firstName, lastName, address));
}

We can now map the AddressDto as a separate parameter.

我们现在可以将AddressDto映射为一个单独的参数。

3.5. Testing the Custom JsonArgumentResolver

3.5.测试自定义JsonArgumentResolver

Let’s write a test case to prove that the JsonArgumentResolver works as expected:

让我们写一个测试用例来证明JsonArgumentResolver按预期工作。

@Test
void whenSendingAPostJSON_thenReturnFirstNameAndCity() throws Exception {

    String jsonString = "{\"firstName\":\"John\",\"lastName\":\"Smith\",\"age\":10,\"address\":{\"streetName\":\"Example Street\",\"streetNumber\":\"10A\",\"postalCode\":\"1QW34\",\"city\":\"Timisoara\",\"country\":\"Romania\"}}";
    
    mockMvc.perform(post("/user/process/custom").content(jsonString)
      .contentType(MediaType.APPLICATION_JSON)
      .accept(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andExpect(MockMvcResultMatchers.jsonPath("$.firstName").value("John"))
      .andExpect(MockMvcResultMatchers.jsonPath("$.city").value("Timisoara"));
}

Next, let’s write a test where we call the second endpoint that parses the JSON directly into POJOs:

接下来,让我们写一个测试,我们调用第二个端点,将JSON直接解析成POJOs。

@Test
void whenSendingAPostJSON_thenReturnUserAndAddress() throws Exception {
    String jsonString = "{\"firstName\":\"John\",\"lastName\":\"Smith\",\"address\":{\"streetName\":\"Example Street\",\"streetNumber\":\"10A\",\"postalCode\":\"1QW34\",\"city\":\"Timisoara\",\"country\":\"Romania\"}}";
    ObjectMapper mapper = new ObjectMapper();
    UserDto user = mapper.readValue(jsonString, UserDto.class);
    AddressDto address = user.getAddress();

    String mvcResult = mockMvc.perform(post("/user/process/custompojo").content(jsonString)
      .contentType(MediaType.APPLICATION_JSON)
      .accept(MediaType.APPLICATION_JSON))
      .andExpect(status().isOk())
      .andReturn()
      .getResponse()
      .getContentAsString();

    assertEquals(String.format("{\"firstName\": %s, \"lastName\": %s, \"address\" : %s}",
      user.getFirstName(), user.getLastName(), address), mvcResult);
}

4. Conclusion

4.总结

In this article, we looked at some limitations in Spring MVC’s default deserialization behavior and then learned how to use a custom HandlerMethodArgumentResolver to overcome them.

在这篇文章中,我们研究了Spring MVC默认反序列化行为的一些限制,然后学习了如何使用自定义HandlerMethodArgumentResolver来克服这些限制。

As always, the code for these examples is available over on GitHub.

像往常一样,这些例子的代码可以在GitHub上找到over