3. 多文件批量上传:List<MultipartBody.Part> 动态构建、请求体组织、服务端接收处理

多文件批量上传是文件上传场景中最常见的需求,如用户一次选择多张图片、批量提交文档等。Retrofit 通过 MultipartBody.Part 列表机制,允许客户端动态构建任意数量的 Parts,而服务端只需按约定解析 Part 列表即可。本章将深入讲解其原理、动态构建流程、请求体组织方式以及服务端接收策略,并通过完整代码示例和常见错误排查帮助开发者掌握高效、稳定的批量上传实现。

核心知识点: Retrofit 使用 @Part List<MultipartBody.Part> 注解声明批量上传接口;客户端通过循环构建 Part 列表;服务端通过 Spring Boot 等框架的 @RequestParam MultipartFile[] 或 List<MultipartFile> 接收。

3.1 原理概述

HTTP 协议的多部分请求(multipart/form-data)包含多个 Part,每个 Part 可以有自己的头部(如 Content-Disposition、Content-Type)和 body。Retrofit 将每个文件封装为一个 MultipartBody.Part 对象,然后通过列表一次性发送。服务端按 Part 的 name 属性(即 form field name)来区分不同文件。

  • 客户端构建: 每个 Part 通过 MultipartBody.Part.createFormData(name, filename, requestBody) 创建,其中 name 是表单字段名,filename 是上传的文件名,requestBody 是文件内容的 RequestBody。
  • 请求体组织: 所有 Part 被添加到同一个 List<MultipartBody.Part> 中,Retrofit 自动将其合并为一个 multipart 请求体,Content-Type 为 multipart/form-data; boundary=...。
  • 服务端接收: 根据 Part 的 name 进行匹配,通常多个文件可以使用同一个 name(如 "files"),服务端用数组或列表接收。
关键注意: 即使只上传一个文件,也可以使用 List<MultipartBody.Part> 方式,这样代码更具通用性。但注意每个 Part 的 name 必须与服务端约定的字段名一致。
多文件批量上传流程图 1. 客户端循环构建 List<Part> 2. 每个文件 → RequestBody 3. MultipartBody.Part.createFormData() 4. parts.add(part) 5. 发送 POST 请求 6. @RequestParam("files") MultipartFile[] 注意:Part 的 name 属性必须和服务端 @RequestParam 的 value 一致,否则服务端无法正确解析。 多文件上传推荐使用统一的 name(如 "files"),服务端使用 MultipartFile[] 接收。

3.2 实现流程

  1. 定义 API 接口: 使用 @Multipart 和 @POST 注解,参数类型为 List<MultipartBody.Part>。
  2. 构建文件列表: 遍历用户选择的每个文件,创建对应的 RequestBody 和 MultipartBody.Part。
  3. 发送请求: 调用 API 的 upload 方法,传入已构建好的 Part 列表。
  4. 服务端接收: Spring Boot 等框架使用 @RequestParam("files") MultipartFile[] files 接收。

3.3 核心代码实现

1. API 接口定义

public interface FileService {
    @Multipart
    @POST("upload/multiple")
    Call<ResponseBody> uploadMultipleFiles(@Part List<MultipartBody.Part> files);
}

2. 客户端构建 Part 列表

List<MultipartBody.Part> parts = new ArrayList<>();
for (File file : selectedFiles) {
    // 创建 RequestBody
    RequestBody requestBody = RequestBody.create(MediaType.parse("image/*"), file);
    // 创建 Part,name 统一为 "files"
    MultipartBody.Part part = MultipartBody.Part.createFormData("files", file.getName(), requestBody);
    parts.add(part);
}

3. 发送请求

Call<ResponseBody> call = fileService.uploadMultipleFiles(parts);
call.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        if (response.isSuccessful()) {
            Log.d("Upload", "批量上传成功");
        }
    }
    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        Log.e("Upload", "上传失败: " + t.getMessage());
    }
});

4. 服务端接收示例(Spring Boot)

@PostMapping("/upload/multiple")
public ResponseEntity<String> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
    for (MultipartFile file : files) {
        if (!file.isEmpty()) {
            // 处理文件保存等逻辑
            String originalFilename = file.getOriginalFilename();
            // 保存到本地或云存储
        }
    }
    return ResponseEntity.ok("上传成功");
}
优化建议: 如果文件数量较多(如超过10个),建议使用 RequestBody.create(MediaType.parse("application/octet-stream"), file) 而不是具体类型(如 "image/*"),避免 MIME 类型匹配失败。

3.4 调试与日志

开启 Retrofit 日志拦截器可查看请求体结构:

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
    .build();
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://example.com/")
    .client(client)
    .build();

日志输出示例(部分):

---> POST /upload/multiple HTTP/1.1
Content-Type: multipart/form-data; boundary=7b5f1b2c-...  
--7b5f1b2c-...
Content-Disposition: form-data; name="files"; filename="photo1.jpg"
Content-Type: image/jpeg
...binary data...
--7b5f1b2c-...
Content-Disposition: form-data; name="files"; filename="doc2.pdf"
Content-Type: application/pdf
...binary data...
---> END POST

3.5 常见错误与解决

错误现象原因解决方案
服务端接收到的文件列表为空Part 的 name 与服务端 @RequestParam 的 value 不匹配统一使用相同的 name,例如 "files"
上传大文件触发 OOM一次性将所有文件读入内存使用 RequestBody 的 File 重载,流式上传
Multipart 请求被截断或 Content-Type 错误OkHttp 版本不兼容或未添加 @Multipart 注解确保接口方法上有 @Multipart 注解,且使用 OkHttp 4.x+
服务端报 "Required request part is missing"没有发送任何 Part 或 Part 列表为空检查文件选择逻辑,确保 parts 非空
避坑指南: 不要将多个文件放在同一个 Part 中(除非服务端特别要求),应每个文件一个 Part。如果使用相同的 name,服务端会按顺序放入数组中。如果 name 不同,则需要分别用 @RequestParam("file1") 等单独接收。

3.6 性能与稳定性优化

  • 流式上传: 使用 RequestBody.create(MediaType, File) 而不是将 File 转为 byte[],避免内存溢出。
  • 并发控制: 若需同时上传多组文件,使用 Retrofit 的 @Headers 或自定义拦截器控制请求队列。
  • 断点续传: 批量上传时,可记录每个文件的上传进度,失败时单独重新上传失败的文件,而无需重传整个批次。
  • 服务端限流: 在 Nginx 或 API 网关层设置 client_max_body_size,防止超大请求导致服务崩溃。
最佳实践: 为每个 Part 设置唯一的 name 后缀(如 "files[0]","files[1]")在某些框架中可辅助定位失败索引,但 Spring Boot 的 MultipartFile[] 已支持顺序,因此简单统一 name 更易维护。