將 JSON POST 映射到多個 Spring MVC 參數
一、概述
當使用 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 屬性的一個子集時,這種結果尤其明顯。在上面的請求處理程序中,我們只需要用戶的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 路徑,但這比@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
來克服這些限制。
與往常一樣,這些示例的代碼可在 GitHub 上找到。