知識庫 / Spring / Spring MVC RSS 訂閱

將 JSON POST 請求映射到多個 Spring MVC 參數

JSON,Spring MVC
HongKong
5
09:49 PM · Dec 05 ,2025

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 屬性的子集時尤為明顯。 在我們上面的請求處理程序中,我們只需要用户的firstNamecity屬性,但我們被迫解序列化整個UserDto

雖然 Spring 允許我們使用MapObjectNode作為參數而不是自制的 DTO,但兩者都是單參數選項。 就像 DTO 一樣,一切都被打包在一起。 由於MapObjectNode的內容是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 "";
}

接下來,我們將創建一個請求處理程序,它使用註解將 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</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> 來解決這些問題。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.