3、OkHttp进阶:Post请求(Form表单、Json)、文件上传与下载、拦截器(Interceptor)机制

好,咱们接着聊。上一章我们把 OkHttp 的 Get 请求讲透了,这一章我带你深入 Post 请求的世界。说实话,Post 请求才是我们日常开发中的重头戏。登录、注册、提交订单、上传头像……几乎每个 App 都离不开它。

我个人习惯把 Post 请求分成三类:表单提交、JSON 提交、文件上传。这三类场景,OkHttp 都提供了非常优雅的 API。咱们一个一个来。

3.1 Form表单提交:最传统的 Post 方式

Form 表单,说白了就是 application/x-www-form-urlencoded 这种格式。数据以键值对的形式拼接在请求体里,像这样:username=admin&password=123456

OkHttp 里怎么做?用 FormBody 构建器。我直接上代码:

OkHttpClient client = new OkHttpClient();

FormBody formBody = new FormBody.Builder()
        .add("username", "admin")
        .add("password", "123456")
        .build();

Request request = new Request.Builder()
        .url("https://api.example.com/login")
        .post(formBody)
        .build();

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 网络异常处理
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (response.isSuccessful()) {
            String result = response.body().string();
            // 解析结果
        }
    }
});

嗯,这里要注意一点:FormBody 默认使用 UTF-8 编码。如果你遇到中文乱码,先检查服务端是不是也用的 UTF-8。我在项目中遇到过几次,最后发现是服务端用了 GBK,那叫一个坑。

小技巧: 如果你需要添加多个参数,可以用循环往 FormBody.Builder 里 add。但注意,add 方法不会覆盖同名参数,而是追加。如果你需要覆盖,先 clear 再重新 add。

3.2 JSON 提交:现代 API 的主流选择

现在大部分 RESTful API 都要求用 JSON 格式提交数据。OkHttp 没有内置 JSON 构建器,但我们可以用 RequestBody 配合 MediaType 来实现。

我个人习惯先构建一个 JSON 字符串,然后封装成 RequestBody:

// 构建 JSON 字符串
String json = "{ \"username\": \"admin\", \"password\": \"123456\" }";

// 或者用 Gson/Jackson 序列化对象
// Gson gson = new Gson();
// String json = gson.toJson(user);

MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody requestBody = RequestBody.create(json, JSON);

Request request = new Request.Builder()
        .url("https://api.example.com/login")
        .post(requestBody)
        .build();

你想想看,为什么现在大家都用 JSON 而不是 Form 表单?因为 JSON 支持嵌套结构,表达能力更强。比如你要提交一个订单,里面包含商品列表、收货地址、优惠券信息,用 Form 表单你得拼一堆奇怪的参数名,用 JSON 就清晰多了。

避坑指南: 我曾经在 Content-Type 上栽过跟头。如果你用 RequestBody.create() 时忘了指定 MediaType,OkHttp 默认会使用 application/octet-stream,服务端解析不了,直接返回 415 错误。所以,一定要显式指定 MediaType

3.3 文件上传与下载:Multipart 的实战

文件上传,用的是 MultipartBody。它模拟了 HTML 表单的 multipart/form-data 格式。每个文件作为一个 Part 上传,还可以附带普通字段。

我直接给你看一个上传头像的例子:

File file = new File("/sdcard/avatar.jpg");
RequestBody fileBody = RequestBody.create(file, MediaType.parse("image/jpeg"));

MultipartBody multipartBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("user_id", "12345")
        .addFormDataPart("avatar", "avatar.jpg", fileBody)
        .build();

Request request = new Request.Builder()
        .url("https://api.example.com/upload")
        .post(multipartBody)
        .build();

这里有个细节:addFormDataPart 的第二个参数是文件名,服务端通常用它来保存文件。如果你传 null,服务端可能不知道文件叫什么名字。

至于文件下载,OkHttp 本身不提供断点续传,但我们可以结合 ResponseBody 的流式读取来实现。我习惯这样写:

Request request = new Request.Builder()
        .url("https://example.com/file.zip")
        .build();

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (!response.isSuccessful()) return;

        // 获取输入流
        InputStream inputStream = response.body().byteStream();
        FileOutputStream outputStream = new FileOutputStream("/sdcard/file.zip");

        byte[] buffer = new byte[8192];
        int bytesRead;
        long totalBytes = response.body().contentLength();
        long downloadedBytes = 0;

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            downloadedBytes += bytesRead;
            // 这里可以回调进度,比如用接口通知 UI 更新
        }

        outputStream.flush();
        outputStream.close();
        inputStream.close();
    }
});
注意: 文件下载一定要在子线程执行,不能在主线程读写文件。另外,记得在 AndroidManifest 里声明 WRITE_EXTERNAL_STORAGE 权限(Android 10 以上要用分区存储)。

3.4 拦截器(Interceptor)机制:OkHttp 的灵魂

拦截器,说白了就是请求和响应的「中间人」。它可以在请求发出前修改请求头、添加日志、重试失败请求,也可以在响应返回后修改响应体、缓存数据。

OkHttp 的拦截器分两种:

  • Application Interceptor:在应用层拦截,可以修改原始请求和最终响应。
  • Network Interceptor:在网络层拦截,可以看到重定向和重试后的请求。

我平时用得最多的是日志拦截器。自己写一个也不难:

class LoggingInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        // 记录请求信息
        long startTime = System.nanoTime();
        System.out.println("请求 URL: " + request.url());
        System.out.println("请求方法: " + request.method());
        System.out.println("请求头: " + request.headers());

        // 执行请求
        Response response = chain.proceed(request);

        // 记录响应信息
        long endTime = System.nanoTime();
        System.out.println("响应码: " + response.code());
        System.out.println("耗时: " + (endTime - startTime) / 1e6 + " ms");

        return response;
    }
}

// 使用
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new LoggingInterceptor())
        .build();

你想想看,有了拦截器,我们可以在不修改业务代码的情况下,统一给所有请求加上 Token、统一处理错误码、统一打印日志。这就是 AOP(面向切面编程)的思想。

实战经验: 我曾经用拦截器做过一个「自动重试」功能。当请求返回 502 或 503 时,自动重试 3 次,每次间隔 1 秒。代码很简单,就是在 intercept 方法里加个循环。但要注意,重试次数不能太多,否则会雪崩。

还有一个常见的场景:添加公共请求头。比如每个请求都要带上 AuthorizationApp-Version

class HeaderInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        Request newRequest = originalRequest.newBuilder()
                .header("Authorization", "Bearer " + token)
                .header("App-Version", "1.0.0")
                .build();
        return chain.proceed(newRequest);
    }
}

嗯,这里要注意:header() 方法会覆盖同名头,而 addHeader() 是追加。如果你需要保留原有的头,用 addHeader

3.5 总结一下

这一章我们讲了 Post 请求的三种形式、文件上传下载、以及拦截器机制。说白了,OkHttp 的核心就是 Request + OkHttpClient + Callback 这个铁三角。掌握了这些,你已经能应对 90% 的网络请求场景了。

下一章,我会带你看看 OkHttp 的缓存策略和连接池管理。这些东西平时用不到,但一旦遇到性能问题,它们就是救命稻草。

课后练习: 试着用拦截器实现一个「网络状态检测」功能。当手机没有网络时,直接返回一个自定义错误,而不是等到请求超时。提示:可以用 ConnectivityManager 判断网络状态。