1. 概述
當使用 Spring 默認的 JSON 反序列化支持時,我們必須將傳入的 JSON 映射到一個單一的請求處理程序參數。然而,有時我們更傾向於使用更精細的方法簽名。
在本教程中,我們將學習如何使用自定義的 <em >HandlerMethodArgumentResolver</em> 將 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. 自定義 <em >HandlerMethodArgumentResolver
讓我們來看如何解決上述限制。我們可以使用 Spring MVC 的 <em >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</em/>
Spring MVC 在確定應處理傳入請求的請求處理程序後,會嘗試自動解析參數。 這包括迭代 Spring 容器中所有實現 HandlerMethodArgumentResolver</em/> 接口的 Bean,以確定 Spring MVC 無法自動解析的參數。
讓我們定義一個實現 HandlerMethodArgumentResolver</em/> 的實現,該實現將處理所有帶有 @JsonArg</em/> 註解的請求處理程序參數:
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 的 @RequestBody 方法相比,這是一種更輕量級的處理方式。
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> 來解決這些問題。