PAYATHON 2026

RestTestClient 发送 multipart 数据时为何提示缺少 value?

数据小许

在基于 MockMvc 的测试模式下,RestTestClient 对 multipart 的支持取决于 Spring Framework 版本。对于 Spring Framework 7.0.x,示例中的现象符合已知限制:客户端可以生成 multipart 请求体,但 MockMvc 无法像 Servlet 容器那样解析这些字节。可以使用 MockMvc 的 multipart 请求构造器,也可以启动真实服务器后通过 RestTestClient 测试。官方问题 #35569

为什么有请求体,仍然提示缺少 value?

示例中的客户端通过以下方式创建:

RestTestClient.bindToApplicationContext(context).build();

这种绑定方式通过 MockMvc 使用模拟请求和响应,不会启动真实 HTTP 服务器。RestTestClient 官方说明

multipart 请求涉及两个环节:客户端将数据序列化成 multipart 字节,服务端再解析这些字节,生成供 @RequestPart 读取的 part。日志中的 Content-Length: [199] 只能说明存在请求体,不能证明解析已经完成。

在上述支持受限的版本中,MockMvc 缺少 Servlet 容器负责的 multipart 解析过程。因此,即使请求体存在,@RequestPart("value") 仍可能找不到对应的 part。这也能解释为什么启动服务器后,使用 Postman 可以正常调用。官方问题中的原因说明

先确认版本标注

需要确认材料中的 Spring v4.0.4 指的是 Spring Boot 版本还是 Spring Framework 版本。RestTestClient 从 Spring Framework 7.0 才开始提供,因此不能将这段代码直接视为 Spring Framework 4.0.4 的示例。RestTestClient API

应检查项目实际解析到的 spring-test、spring-web 和 spring-webmvc 版本。Spring Framework 7.1 的发布说明明确列出了 RestTestClient 与 MockMvc 的 multipart 请求支持改进。如果项目已经使用包含该改进的版本,还需要检查请求内容及自定义配置,不能仅凭同一个异常认定原因相同。Spring Framework 7.1 发布说明

方案一:使用 MockMvc 显式构造 part

如果目的是验证 Controller 能否接收 value 并返回结果,可以保留 MOCK 环境,使用 multipart() 和 MockPart:

import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class TestMultipartWithMockMvc {

    private MockMvc mockMvc;

    @BeforeEach
    void setup(WebApplicationContext context) {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    void sendMultiPart() throws Exception {
        MockPart valuePart = new MockPart(
                "value", "test".getBytes(StandardCharsets.UTF_8));
        valuePart.getHeaders().setContentType(MediaType.TEXT_PLAIN);

        mockMvc.perform(multipart("/api/multipart").part(valuePart))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.value").value("test"));
    }
}

这个测试直接提供构造好的 part。MockMvc 的 multipart 测试使用 MockMultipartHttpServletRequest,不会实际解析网络传输中的 multipart 请求体,因此适合验证参数绑定和 Controller 行为,不能替代对真实容器解析过程的验证。MockMvc 请求构造说明

方案二:启动真实服务器,继续使用 RestTestClient

如果需要覆盖 multipart 编码、HTTP 传输和 Servlet 容器解析,应将测试环境改成随机端口,并把客户端绑定到服务器。

将测试类上的注解改为:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

在测试类中注入端口,并替换客户端初始化方法:

@LocalServerPort
private int port;

@BeforeEach
void setupApi() {
    client = RestTestClient.bindToServer()
            .baseUrl("http://localhost:" + port)
            .build();
}

其中 LocalServerPort 的完整类名是 org.springframework.boot.test.web.server.LocalServerPort。bindToServer() 会连接实际运行的服务器;如果只修改 webEnvironment,仍使用 bindToApplicationContext(context),客户端还是会走 MockMvc。RestTestClient 绑定方式说明

请求代码可以继续使用 MultiValueMap,同时修正响应断言:

@Test
void sendMultiPart() {
    String value = "test";
    MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
    body.add("value", value);

    client.post()
            .uri("/api/multipart")
            .contentType(MediaType.MULTIPART_FORM_DATA)
            .body(body)
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$.value").isEqualTo(value);
}

示例中还需要修正什么?

  • 日志记录的是 /api/parts,Controller 和测试代码使用的是 /api/multipart。应统一路径,再用同一次请求的日志进行排查。
  • 原来的 JSON 断言字符串缺少结尾的 }。这不会导致缺少 part,但在请求成功后会影响断言。上面的示例改用 JSONPath,避免手动拼接 JSON。
  • 不要通过 @RequestPart(required = false) 隐藏异常。接口需要 value 时,应修复测试请求或解析过程。
  • 如果真实服务器测试也失败,再检查实际请求中是否存在名为 value 的 part,以及应用的 multipart 配置。仅凭 Content-Type 和请求体长度无法判断 part 内容是否正确。

备注:内容仅供参考。