拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 将JSON POST 映射到多个Spring MVC 参数

将JSON POST 映射到多个Spring MVC 参数

白鹭 - 2022-09-24 2151 0 2

一、概述

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

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

2.问题

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

2.1。默认@RequestBody行为

让我们从一个示例JSON 正文开始:

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

接下来,让我们创建与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
 }

最后,我们将使用标准方法使用@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.限制

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

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

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

虽然Spring 允许我们使用MapObjectNode作为参数而不是本地DTO,但两者都是单参数选项。与DTO 一样,所有内容都打包在一起。由于MapObjectNode的内容是String值,我们必须自己将它们编组为对象。这些选项使我们不必声明一次性DTO,但会带来更多的复杂性。

3.自定义HandlerMethodArgumentResolver

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

3.1。创建控制器

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

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

接下来,我们将创建一个请求处理程序,该处理程序使用注释将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.创建自定义HandlerMethodArgumentResolver

在Spring MVC 决定哪个请求处理程序应该处理传入请求后,它会尝试自动解析参数。这包括遍历Spring 上下文中实现HandlerMethodArgumentResolver接口的所有bean,以防Spring MVC 无法自动解析任何参数。

让我们定义一个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 使用supportsParameter()方法来检查这个类是否可以解析给定的参数。由于我们希望我们的处理程序处理使用@JsonArg注释的任何参数,因此如果给定参数具有该注释,我们将返回true

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

3.3.注册自定义HandlerMethodArgumentResolver

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

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

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

3.4.使用自定义类型的参数

为了证明这也适用于自定义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));
 }

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

3.5.测试自定义JsonArgumentResolver

让我们编写一个测试用例来证明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"));
 }

接下来,让我们编写一个测试,我们调用将JSON 直接解析为POJO 的第二个端点:

@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。结论

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


标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *