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 必须与服务端约定的字段名一致。
3.2 实现流程
- 定义 API 接口: 使用
@Multipart和@POST注解,参数类型为List<MultipartBody.Part>。 - 构建文件列表: 遍历用户选择的每个文件,创建对应的
RequestBody和MultipartBody.Part。 - 发送请求: 调用 API 的 upload 方法,传入已构建好的 Part 列表。
- 服务端接收: 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 更易维护。