第2章:单文件上传核心实现
2.1 原理概述
单文件上传的核心是将本地文件以 multipart/form-data 格式封装到 HTTP 请求体中,通过 Retrofit 的声明式注解驱动,自动完成 MIME 类型分界、流分段写入和传输。Retrofit 依赖 OkHttp 底层的 MultipartBody 构建器,开发者只需关注三个关键元素:
- @Multipart — 标记该请求为多部分表单请求,启用 OkHttp 的 multipart 编码器。
- @Part — 声明请求中的一个 part,可携带任意类型的数据或文件二进制流。
- MultipartBody.Part — OkHttp 提供的请求 part 容器,封装文件名、媒体类型和请求体内容。
服务端接收时,需要按照 multipart/form-data 标准解析字段名为对应文件参数。以下章节将按实现流程逐步拆解每一部分的编写方式与调试要点。
2.2 实现流程
- 定义 Retrofit 接口:使用 @Multipart 标注方法,文件参数用 @Part MultipartBody.Part 声明。
- 构造文件 Part:通过
MultipartBody.Part.createFormData(...)将File或RequestBody包装为 Part。 - 发起请求:调用 Retrofit 接口方法,订阅结果。
- 服务端解析:根据指定的 part name 获取上传文件流并保存。
2.3 代码与API详解
2.3.1 定义 Retrofit 接口
public interface FileUploadApi {
@Multipart
@POST("upload/single")
Call<ResponseBody> uploadFile(
@Part MultipartBody.Part filePart,
@Part("description") RequestBody description
);
}
关键点:
- 方法必须标注
@Multipart,否则 Retrofit 不会启用 multipart 编码器。 - 所有参数必须是
@Part或@PartMap;不能用@Field或@Body。 - 额外普通字段(如 description)需要包装为
RequestBody或直接传入String(Retrofit 会自动转换)。
2.3.2 构造 MultipartBody.Part
// 步骤1:将文件转换为 RequestBody
File file = new File("/path/to/image.jpg");
RequestBody requestFile = RequestBody.create(
MediaType.parse("image/jpeg"), file
);
// 步骤2:创建 MultipartBody.Part
MultipartBody.Part filePart = MultipartBody.Part.createFormData(
"file", // 服务端接收时的字段名
file.getName(), // 文件名(可选,服务端可获取)
requestFile // 文件内容
);
// 步骤3:构造普通字段的 RequestBody
RequestBody description = RequestBody.create(
MediaType.parse("text/plain"), "用户头像上传"
);
2.3.3 调用接口
FileUploadApi api = retrofit.create(FileUploadApi.class);
Call<ResponseBody> call = api.uploadFile(filePart, description);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
// 上传成功
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
// 网络异常或请求构建错误
}
});
2.4 服务端接口定义示例
以下展示两种常见后端框架的接收方式:
| 框架 | 代码示例 |
|---|---|
| Spring Boot |
|
| Ktor (Kotlin) |
|
提示:客户端
@Part("file") 中的字段名 "file" 必须与服务端 @RequestParam("file") 一致,否则服务端无法匹配 part。
2.5 调试方法
- 启用 OkHttp 日志:在 Retrofit 构建时添加
HttpLoggingInterceptor,级别设为BODY,可打印完整的 multipart 请求体结构。 - 抓包工具:使用 Charles 或 Wireshark 观察请求头中的
Content-Type: multipart/form-data; boundary=...以及每个 part 的分界标识。 - 服务端日志:在 Spring Boot 中打印
file.getSize()、file.getContentType()验证文件到达情况。
// 添加日志拦截器
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com/")
.client(client)
.build();
2.6 常见错误与解决
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
| java.lang.IllegalArgumentException: @Field parameters can only be used with form encoding. | 在 @Multipart 方法中混用了 @Field | 移除 @Field,改用 @Part + RequestBody |
| 服务端收到空文件 | MultipartBody.Part 的 fieldName 与服务端不匹配 | 检查 createFormData 的第一个参数是否与服务端 @RequestParam 名称一致 |
| 413 Request Entity Too Large | 文件大小超过服务端限制 | 调整服务端 max-file-size 配置(Spring Boot: spring.servlet.multipart.max-file-size) |
| org.xmlpull.v1.XmlPullParserException: expected: END_TAG | MultipartBody.Part 被重复添加或未正确构建 | 确保每个 Part 只添加一次,且未提前 close |
2.7 性能与稳定性优化
- 大文件分段上传:单文件上传不适合超大文件(>100MB),应使用分片上传框架(第15章)。
- 超时设置:根据文件大小调整 OkHttp 连接、读、写超时。
- 避免内存泄漏:在 Activity/Fragment 销毁时取消 Call。
- 压缩与缩略图:上传前对图片进行压缩(BitmapFactory 或 libvips),减少网络传输时间。
// 设置较大的超时(以秒为单位)
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build();
本章实验建议:在本地搭建 Spring Boot 服务器,用 Postman 先验证上传接口正常,再通过 Retrofit 客户端调用。观察抓包日志中 multipart 的 boundary 与 part 结构,加深对协议的理解。