第2章:单文件上传核心实现

2.1 原理概述

单文件上传的核心是将本地文件以 multipart/form-data 格式封装到 HTTP 请求体中,通过 Retrofit 的声明式注解驱动,自动完成 MIME 类型分界、流分段写入和传输。Retrofit 依赖 OkHttp 底层的 MultipartBody 构建器,开发者只需关注三个关键元素:

  • @Multipart — 标记该请求为多部分表单请求,启用 OkHttp 的 multipart 编码器。
  • @Part — 声明请求中的一个 part,可携带任意类型的数据或文件二进制流。
  • MultipartBody.Part — OkHttp 提供的请求 part 容器,封装文件名、媒体类型和请求体内容。

服务端接收时,需要按照 multipart/form-data 标准解析字段名为对应文件参数。以下章节将按实现流程逐步拆解每一部分的编写方式与调试要点。

单文件上传核心流程 Retrofit 接口 @Multipart @Part MultipartBody.Part OkHttp MultipartBody 流式写入请求体 HTTP 传输 multipart/form-data 服务端接口 @RequestParam / file 构建 发送 解析 关键: @Multipart 必须与 @Part 配合使用,且方法参数中不能出现普通 @Field。 一个请求可包含多个 @Part,其中仅文件部分使用 MultipartBody.Part。

2.2 实现流程

  1. 定义 Retrofit 接口:使用 @Multipart 标注方法,文件参数用 @Part MultipartBody.Part 声明。
  2. 构造文件 Part:通过 MultipartBody.Part.createFormData(...) 将 File 或 RequestBody 包装为 Part。
  3. 发起请求:调用 Retrofit 接口方法,订阅结果。
  4. 服务端解析:根据指定的 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
@PostMapping("/upload/single")
public String uploadSingle(
    @RequestParam("file") MultipartFile file,
    @RequestParam("description") String desc) {
    // 保存文件逻辑
    return "上传成功: " + file.getOriginalFilename();
}
Ktor (Kotlin)
post("/upload/single") {
    val multipart = call.receiveMultipart()
    var fileName = ""
    multipart.forEachPart { part ->
        when (part) {
            is PartData.FileItem -> {
                fileName = part.originalFileName ?: "unknown"
                // 将流保存到磁盘
            }
            else -> {}
        }
        part.dispose()
    }
    call.respondText("上传成功: $fileName")
}
提示:客户端 @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_TAGMultipartBody.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 结构,加深对协议的理解。