You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用MockMvc发送含对象数组的POST请求适配@ModelAttribute控制器

MockMvc测试POST接口传递对象数组参数报400错误排查与解决

问题现象

测试时抛出断言错误:

java.lang.AssertionError: Status expected:<201> but was:<400>

MockHttpServletResponse未返回错误信息,推测问题出在**对象数组参数lecture_values**的传递上,该参数在请求模型中定义为List<Map<String, Object>>。


预期请求体

{
  "study_class_id": 1,
  "lecture_values": [
    {
      "topic": 1,
      "content": "Lorem ipsum"
    },
    {
      "topic": 2,
      "content": "Lorem ipsum"
    }
  ]
}

控制器代码

@PostMapping(value = "/lecture")
public ResponseEntity<LecturesResource> addLecture(
    @Valid @ModelAttribute("Lecture") LectureAddRequestParam param,
    BindingResult bindingResult, Principal principal, HttpServletRequest request,
    HttpServletResponse response, HttpSession session) {

    return processResult;
}

请求参数模型

public class LectureAddRequestParam {

  @NotNull
  @Min(1)
  private int study_class_id;

  @NotNull
  private List<Map<String, Object>> lecture_values;
 
  // Getter Setter..
}

当前测试代码(报错版本)

// Prepare lecture values param
JSONObject lecture1 = new JSONObject();
lecture1.put("topic", 1);
lecture1.put("content", "Lorem ipsum");

JSONObject lecture2 = new JSONObject();
lecture2.put("topic", 2);
lecture2.put("content", "Lorem ipsum");

JSONArray lectureValues = new JSONArray();
lectureValues.add(lecture1);
lectureValues.add(lecture2);

var responseContent = mockMvc
    .perform(
        post("/lecture")
            .with(csrf())
            .param("study_class_id", Integer.toString(studyClassId))
            .param("lecture_values", lectureValues.toString()))
            .andDo(log())
            .andExpect(status().isCreated())
            .andReturn()
            .getResponse()
            .getContentAsString();

前端可成功调用的代码

$.ajax({
  url: '/lecture',
  type: 'POST',
  timeout: this.TIMEOUT_LIMIT,
  headers: requestHeader,
    data: {
      study_class_id: study_class_id,
      lecture_values: lecture_values.toJSON()
    }
});

注:lecture_values是Backbone.js集合,调用toJSON()转换为数组格式。

其他无效尝试

  • 创建包含study_class_id和lecture_values数组的lectureJSONObject,调用flashAttr("lecture", lectureJSONObject)
  • 创建包含study_class_id和转换为List<Map<String, Object>>的lecture_values的lectureObject,调用flashAttr("lecture", lectureObject)

问题原因与解决方案

原因分析

当前测试用.param()传递数组参数时,是把JSON数组转成字符串传递,Spring无法自动将这个字符串解析为List<Map<String, Object>>;而前端jQuery的ajax会自动将数组参数按照表单数组格式(如lecture_values[0].topic=1&lecture_values[0].content=xxx)编码,Spring的@ModelAttribute可以正确解析这种格式。

解决方案

方案1:按表单数组格式构造参数

直接模拟前端的参数传递方式,逐个设置数组元素的属性:

var responseContent = mockMvc
    .perform(
        post("/lecture")
            .with(csrf())
            .param("study_class_id", Integer.toString(studyClassId))
            // 第一个数组元素
            .param("lecture_values[0].topic", "1")
            .param("lecture_values[0].content", "Lorem ipsum")
            // 第二个数组元素
            .param("lecture_values[1].topic", "2")
            .param("lecture_values[1].content", "Lorem ipsum"))
            .andDo(log())
            .andExpect(status().isCreated())
            .andReturn()
            .getResponse()
            .getContentAsString();

方案2:使用RequestBuilder构造完整表单数据

如果数组元素较多,可以用LinkedMultiValueMap构造参数:

MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("study_class_id", Integer.toString(studyClassId));
// 添加第一个元素
params.add("lecture_values[0].topic", "1");
params.add("lecture_values[0].content", "Lorem ipsum");
// 添加第二个元素
params.add("lecture_values[1].topic", "2");
params.add("lecture_values[1].content", "Lorem ipsum");

var responseContent = mockMvc
    .perform(
        post("/lecture")
            .with(csrf())
            .params(params))
            .andDo(log())
            .andExpect(status().isCreated())
            .andReturn()
            .getResponse()
            .getContentAsString();

方案3:改用@RequestBody接收JSON(可选)

如果允许修改控制器,可以将@ModelAttribute改为@RequestBody,测试时直接发送JSON请求体:

// 修改控制器
@PostMapping(value = "/lecture")
public ResponseEntity<LecturesResource> addLecture(
    @Valid @RequestBody LectureAddRequestParam param, // 改为@RequestBody
    BindingResult bindingResult, Principal principal, HttpServletRequest request,
    HttpServletResponse response, HttpSession session) {

    return processResult;
}

// 测试代码
String requestBody = "{\"study_class_id\":1,\"lecture_values\":[{\"topic\":1,\"content\":\"Lorem ipsum\"},{\"topic\":2,\"content\":\"Lorem ipsum\"}]}";

var responseContent = mockMvc
    .perform(
        post("/lecture")
            .with(csrf())
            .contentType(MediaType.APPLICATION_JSON)
            .content(requestBody))
            .andDo(log())
            .andExpect(status().isCreated())
            .andReturn()
            .getResponse()
            .getContentAsString();

内容的提问来源于stack exchange,提问作者MMT Yuma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 00:50:14