1. 概述
在使用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
}
最后,我们使用标准方法将JSON请求反序列化为UserDto
,使用@RequestBody
注解:
@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属性的子集时,这种后果尤为明显。在上面的处理程序中,我们只需要用户的firstName
和city
属性,但被迫反序列化整个UserDto
。
虽然Spring允许我们使用Map
或ObjectNode
作为参数,而不是自定义的DTO,但两者都是单参数选项。与DTO一样,所有内容都打包在一起。由于Map
和ObjectNode
的内容是String
值,我们必须自己将它们转换为对象。这些选项可以避免声明一次性使用的DTO,但也增加了复杂性。
3. 自定义HandlerMethodArgumentResolver
让我们来看看解决上述限制的方法。我们可以利用Spring MVC的HandlerMethodArgumentResolver
,以便在请求处理器中声明我们想要的JSON属性作为参数。
3.1. 创建控制器
首先,创建一个我们可以用来将请求处理器参数映射到JSON路径的自定义注解:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonArg {
String value() default "";
}
然后,创建一个使用注解将firstName
和city
作为独立参数的请求处理器,这些参数与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路径,但这比为每个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
来克服这些问题。
如往常一样,这些示例的代码可以在GitHub上找到。