提示:本文章由 AI(OpenAI GPT 5.6 Sol)生成。

OkHttp 4 入门教程

面向第一次接触 OkHttp 的开发者。本文以 OkHttp 4.x 为背景,示例以 Java 为主,并在关键位置给出 Kotlin 写法。

1. OkHttp 是什么

OkHttp 是一个 HTTP 客户端库。它负责帮你完成这些工作:

  • 创建 HTTP 请求
  • 建立和复用 TCP/TLS 连接
  • 发送请求头和请求体
  • 接收响应状态码、响应头和响应体
  • 处理连接复用、HTTP/2、缓存、重定向、超时和取消
  • 通过拦截器统一添加日志、认证信息或错误处理

你通常不需要手动操作 Socket,也不需要自己拼接完整的 HTTP 报文。你只需要描述:

  1. 使用哪个 OkHttpClient。
  2. 创建什么样的 Request。
  3. 通过 client.newCall(request) 创建一个 Call。
  4. 同步执行或异步执行这个 Call。
  5. 从 Response 中读取结果。

最基本的调用链是:

OkHttpClient
    |
    | newCall(request)
    v
Call
    |
    | execute() 或 enqueue(...)
    v
Response
    |
    +-- code() / isSuccessful()
    +-- headers()
    +-- body()

2. 添加依赖

2.1 Gradle

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

如果是 Kotlin DSL:

dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
}

本文使用 4.12.0 作为示例版本。实际项目中可以统一升级到团队批准的 4.x 版本。

2.2 Maven

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>

2.3 Android 额外配置

Android 应用需要网络权限:

<uses-permission android:name="android.permission.INTERNET" />

同步请求不能在 Android 主线程执行,否则可能触发 NetworkOnMainThreadException。Android 项目通常在协程、线程池或其他后台任务中执行网络请求。

3. 先理解五个核心对象

3.1 OkHttpClient:网络客户端和配置中心

OkHttpClient 表示一套网络通信配置,内部还管理连接池、线程调度、缓存、代理、Cookie 等资源。

OkHttpClient client = new OkHttpClient();

它通常应该在应用范围内复用,而不是每次请求都 new 一个:

public final class HttpClients {
    private HttpClients() {
    }

    public static final OkHttpClient CLIENT = new OkHttpClient();
}

反复创建客户端会导致连接池和线程资源无法充分复用,也可能增加延迟。

客户端负责“怎么通信”,但不负责描述某一次请求的 URL 和请求体。那些内容属于 Request。

3.2 Request:一次 HTTP 请求的完整描述

Request 包含一次请求需要的信息,例如:

  • URL
  • HTTP 方法,例如 GET、POST、PUT、DELETE
  • 请求头
  • 请求体

一个已经 build 完成的 Request 可以看作不可变对象。创建它时使用的是 Request.Builder。

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

3.3 Call:一次请求的可执行任务

Call call = client.newCall(request);

Call 可以理解为“已经准备好执行的请求任务”。它还没有真正发出网络请求;真正执行发生在:

  • execute():同步执行,当前线程等待结果
  • enqueue(callback):异步执行,OkHttp 在后台线程执行并回调结果

同一个 Call 只能执行一次。需要再次发送相同请求时,重新调用 client.newCall(request) 创建新的 Call。

3.4 Response:服务器返回的结果

Response 包含:

  • 请求 URL
  • HTTP 状态码
  • 状态消息
  • 响应头
  • 响应体
  • 可能存在的缓存响应或网络响应信息

例如:

try (Response response = client.newCall(request).execute()) {
    System.out.println(response.code());
    System.out.println(response.headers());
}

Response 以及其中的响应体都需要及时关闭。最简单的做法是 Java 的 try-with-resources,或者 Kotlin 的 use。

3.5 ResponseBody:响应体内容

响应体可能是文本、JSON、图片、压缩包或其他二进制内容。常用读取方法有:

String text = response.body().string();
byte[] bytes = response.body().bytes();

这些方法会把内容一次性读入内存,并且响应体是一次性的。通常不要对同一个响应体连续调用两次 string()。

对于大文件,应使用 response.body().source() 或 byteStream() 分块读取,而不是一次性调用 bytes()。

4. 第一个完整 GET 示例

下面的例子访问一个地址,检查状态码,然后读取文本:

import java.io.IOException;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class BasicGet {
    public static void main(String[] args) {
        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
                .url("https://httpbin.org/get")
                .get()
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected HTTP code: " + response.code());
            }

            ResponseBody body = response.body();
            if (body == null) {
                throw new IOException("Response body is empty");
            }

            String responseText = body.string();
            System.out.println(responseText);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

逐行理解:

OkHttpClient client = new OkHttpClient();

创建客户端。真实应用一般将它作为单例或应用级对象复用。

Request request = new Request.Builder()
        .url("https://httpbin.org/get")
        .get()
        .build();

创建请求。这里的每一个链式调用都在修改 Builder,最后 build() 把 Builder 的当前内容转换成一个完整的 Request。

client.newCall(request)

把请求交给客户端,得到一个 Call。

.execute()

同步执行。代码会停在这里,直到请求成功返回、发生网络错误或被取消。

try (Response response = ...)

请求完成后得到 Response。try-with-resources 会在代码离开代码块时自动关闭响应,释放底层资源。

response.isSuccessful()

判断状态码是否在 200 到 299 范围内。注意:HTTP 404、500 等状态通常不会自动抛出 IOException,它们仍然是一个正常收到的 HTTP 响应,需要你自己根据业务处理。

body.string()

读取响应体中的全部文本。调用后响应体的流已经被消费,不能指望再次读取出相同内容。

对应的 Kotlin 写法:

val client = OkHttpClient()
val request = Request.Builder()
    .url("https://httpbin.org/get")
    .get()
    .build()

client.newCall(request).execute().use { response ->
    if (!response.isSuccessful) {
        error("Unexpected HTTP code: " + response.code)
    }

    println(response.body?.string())
}

5. 重点:彻底理解 Request.Builder

很多 OkHttp 示例都写成一串链式调用:

Request request = new Request.Builder()
        .url("https://example.com/users")
        .addHeader("Accept", "application/json")
        .get()
        .build();

初学者容易把它看成一个整体。实际上它等价于下面这种逐步写法:

Request.Builder builder = new Request.Builder();

builder.url("https://example.com/users");
builder.addHeader("Accept", "application/json");
builder.get();

Request request = builder.build();

链式调用的前提是:url()、addHeader()、get() 等方法都会返回当前的 Request.Builder,因此下一个方法可以紧接着调用。

5.1 new Request.Builder()

Request.Builder builder = new Request.Builder();

这一步只是创建一个“请求构造器”。此时还没有最终的 URL,也没有真正的网络请求。

Builder 允许你逐步设置请求属性:

builder.url("https://example.com");
builder.get();
Request request = builder.build();

build() 通常是最后一步。没有设置必要属性时,构建阶段可能抛出异常,例如没有 URL,或者 HTTP 方法与请求体组合不合法。

5.2 url(...):设置请求地址

最常见的 Java 写法:

.url("https://example.com/users")

参数是一个完整 URL 字符串。推荐使用 HTTPS:

.url("https://api.example.com/v1/users")

URL 可以包含:

  • 协议:https
  • 主机:api.example.com
  • 端口:8443
  • 路径:/v1/users
  • 查询参数:?page=1&size=20
  • 片段:#section,片段通常不会发送给服务器

例如:

.url("https://api.example.com:8443/v1/users?page=1&size=20")

这里的 ?page=1&size=20 是查询参数,不是请求体。

为什么不建议手动拼接查询参数

下面的写法在参数包含空格、中文、& 或 ? 时容易出错:

String url = "https://api.example.com/search?keyword=" + keyword;

更稳妥的方式是使用 HttpUrl:

import okhttp3.HttpUrl;

HttpUrl url = HttpUrl.get("https://api.example.com/search")
        .newBuilder()
        .addQueryParameter("keyword", "Java 网络编程")
        .addQueryParameter("page", "1")
        .build();

Request request = new Request.Builder()
        .url(url)
        .get()
        .build();

addQueryParameter() 会处理查询参数的编码。例如,参数值中的空格和特殊字符不会直接破坏 URL 结构。

addQueryParameter() 与 addEncodedQueryParameter() 的区别:

  • addQueryParameter() 接受未编码的普通字符串,由 OkHttp 负责编码。
  • addEncodedQueryParameter() 认为你传入的值已经编码,只有确认自己在处理编码时才使用它。

URL 中的路径和参数

HttpUrl url = HttpUrl.get("https://api.example.com")
        .newBuilder()
        .addPathSegment("users")
        .addPathSegment("A/B")
        .addQueryParameter("redirect", "https://example.com/a/b")
        .build();

这里第二个路径段 A/B 会被当作一个段处理,而查询参数 redirect 的 URL 也会被正确编码。

5.3 addHeader(name, value):添加请求头

.addHeader("Accept", "application/json")

HTTP 请求头是发送给服务器的元信息。上面这行表达的是:

  • 请求头名称是 Accept
  • 请求头值是 application/json
  • 告诉服务器:客户端希望收到 JSON

常见请求头:

.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer your-token")
.addHeader("User-Agent", "my-app/1.0")
.addHeader("X-Request-Id", "request-123")

不要把密码、Token 或 Cookie 打印到日志中。示例中的 your-token 只是占位符,真实项目应从安全的配置或认证流程中获取。

addHeader 和 header 的区别

builder.addHeader("Cache-Control", "no-cache");
builder.addHeader("Cache-Control", "no-store");

这会添加两个同名请求头。某些 HTTP 头允许多个值,OkHttp 会保留它们。

builder.header("Authorization", "Bearer old-token");
builder.header("Authorization", "Bearer new-token");

header(name, value) 会移除同名旧值,再设置新值,因此最后只保留 new-token。

删除某个请求头:

builder.removeHeader("Authorization");

可以这样记:

方法行为
addHeader在已有同名值后面继续添加
header替换同名值
removeHeader删除同名值

哪些头不应该手动设置

请求体相关的 Content-Length、传输编码等通常由 OkHttp 根据 RequestBody 计算和设置。除非你非常清楚服务器的特殊要求,否则不要手动修改这些头。

Content-Type 通常也应该由请求体的 MediaType 表达:

MediaType jsonType = MediaType.get("application/json; charset=utf-8");
RequestBody body = RequestBody.create("{\"name\":\"Ada\"}", jsonType);

5.4 get():设置 HTTP 方法为 GET

.get()

这表示 HTTP 方法是 GET,通常用于获取资源。GET 一般没有请求体,因此常见写法是:

Request request = new Request.Builder()
        .url("https://api.example.com/users")
        .get()
        .build();

如果不调用 get(),对于没有请求体的 Builder,OkHttp 默认也会使用 GET。但显式写出 get() 更容易阅读,尤其是教程和复杂请求中。

5.5 post(body):设置 POST 方法和请求体

.post(body)

这一步同时做两件事:

  1. 把 HTTP 方法设置为 POST。
  2. 把 body 作为请求体发送。

例如:

MediaType textType = MediaType.get("text/plain; charset=utf-8");
RequestBody body = RequestBody.create("hello", textType);

Request request = new Request.Builder()
        .url("https://httpbin.org/post")
        .post(body)
        .build();

这里 body 不是一个普通字符串,而是一个 RequestBody 对象。它同时描述内容和内容类型。

常用方法还有:

.put(body)
.patch(body)
.delete(body)

无请求体的 DELETE 可以写为:

Request request = new Request.Builder()
        .url("https://api.example.com/users/42")
        .delete()
        .build();

5.6 method(method, body):完全自定义方法

当服务器使用特殊 HTTP 方法时,可以使用:

.method("REPORT", body)

对于普通的 GET、POST、PUT、PATCH、DELETE,优先使用对应的专用方法,代码意图更清晰。

5.7 build():生成最终的 Request

Request request = builder.build();

build() 会检查当前 Builder 的状态并生成 Request。它不会执行网络请求。

只有下面这类代码才真正执行请求:

Response response = client.newCall(request).execute();

可以把整个过程记成:

Builder:正在填写表单
build():生成一份完整表单
Request:已经生成的请求描述
newCall():把请求放入待执行任务
execute/enqueue:真正发送请求

5.8 常见 Builder 写法的拆解

代码:

Request request = new Request.Builder()
        .url("https://api.example.com/v1/users")
        .addHeader("Accept", "application/json")
        .addHeader("Authorization", "Bearer token")
        .get()
        .build();

等价于:

Request.Builder builder = new Request.Builder();
builder.url("https://api.example.com/v1/users");
builder.addHeader("Accept", "application/json");
builder.addHeader("Authorization", "Bearer token");
builder.get();
Request request = builder.build();

每个方法的作用:

代码含义
new Request.Builder()创建请求构造器
url(...)设置目标地址
addHeader(...)添加请求头
get()设置 HTTP 方法为 GET
build()生成不可变的 Request

6. 查询参数、路径参数和请求体的区别

这三类数据经常被初学者混淆。

6.1 查询参数:URL 的问号后面

/users?page=1&size=20

适合分页、筛选、排序等:

HttpUrl url = HttpUrl.get("https://api.example.com/users")
        .newBuilder()
        .addQueryParameter("page", "1")
        .addQueryParameter("size", "20")
        .addQueryParameter("keyword", "Ada Lovelace")
        .build();

Request request = new Request.Builder()
        .url(url)
        .get()
        .build();

6.2 路径参数:URL 路径中的资源标识

/users/42

可以这样构建:

HttpUrl url = HttpUrl.get("https://api.example.com")
        .newBuilder()
        .addPathSegment("users")
        .addPathSegment("42")
        .build();

如果 ID 来自用户输入,使用 addPathSegment() 比直接字符串拼接更安全。

6.3 请求体:HTTP 报文中承载的数据

请求体常用于 POST、PUT、PATCH:

RequestBody body = RequestBody.create(
        "{\"name\":\"Ada\"}",
        MediaType.get("application/json; charset=utf-8")
);

可以这样区分:

URL 查询参数:告诉服务器“如何查”
路径参数:告诉服务器“操作哪个资源”
请求体:告诉服务器“提交什么内容”

7. POST JSON 请求

OkHttp 本身不会把 Java 对象自动转换为 JSON。你可以临时手写 JSON,但正式项目通常使用 Jackson、Gson、Moshi 或 kotlinx.serialization。

7.1 手写 JSON 的简单示例

import java.io.IOException;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class JsonPost {
    private static final MediaType JSON =
            MediaType.get("application/json; charset=utf-8");

    public static void main(String[] args) throws IOException {
        OkHttpClient client = new OkHttpClient();

        String json = "{\"name\":\"Ada\",\"age\":36}";
        RequestBody body = RequestBody.create(json, JSON);

        Request request = new Request.Builder()
                .url("https://httpbin.org/post")
                .header("Accept", "application/json")
                .post(body)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected HTTP code: " + response.code());
            }

            ResponseBody responseBody = response.body();
            if (responseBody == null) {
                throw new IOException("Response body is empty");
            }

            System.out.println(responseBody.string());
        }
    }
}

这里需要分清三个东西:

String json = "...";

这是 Java 字符串,内容碰巧是 JSON 格式。

MediaType JSON = MediaType.get("application/json; charset=utf-8");

这是媒体类型,告诉服务器请求体按照 JSON 文本理解,并使用 UTF-8 字符集。

RequestBody body = RequestBody.create(json, JSON);

这是 OkHttp 的请求体对象,将数据内容和媒体类型组合起来。

7.2 使用 JSON 序列化库

例如,使用 Gson:

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
    implementation 'com.google.code.gson:gson:2.11.0'
}
Gson gson = new Gson();
User user = new User("Ada", 36);
String json = gson.toJson(user);

RequestBody body = RequestBody.create(json, JSON);

这样可以避免手动处理引号、转义字符和嵌套对象。

8. 表单提交 FormBody

如果服务器要求 application/x-www-form-urlencoded,可以使用 FormBody:

RequestBody formBody = new FormBody.Builder()
        .add("username", "ada")
        .add("password", "secret")
        .build();

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

FormBody.Builder 的 add(name, value) 会将字段作为表单字段提交,并负责进行必要的 URL 编码。

不要把登录密码直接写入普通日志。真实项目还应根据认证方案使用 HTTPS,并避免在异常信息中输出密码。

9. Multipart 文件上传

上传文件和普通 JSON 不同,需要 MultipartBody:

import java.io.File;

import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;

File file = new File("avatar.png");
RequestBody fileBody = RequestBody.create(
        file,
        MediaType.get("image/png")
);

RequestBody multipartBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("userId", "42")
        .addFormDataPart("avatar", file.getName(), fileBody)
        .build();

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

拆开看:

  • setType(MultipartBody.FORM):声明这是表单式 multipart 请求。
  • addFormDataPart("userId", "42"):添加普通文本字段。
  • addFormDataPart("avatar", file.getName(), fileBody):添加文件字段,avatar 是服务器约定的字段名。
  • fileBody:描述文件内容和文件 MIME 类型。

10. 同步执行:execute()

同步执行的特点是:调用线程会等待结果。

try (Response response = client.newCall(request).execute()) {
    ResponseBody body = response.body();
    if (body == null) {
        return;
    }

    String text = body.string();
    System.out.println(text);
}

同步执行适合:

  • 命令行程序
  • 后台线程中的串行任务
  • 测试代码

不适合 Android 主线程或其他不能阻塞的事件线程。

同步执行期间可能出现 IOException,例如:

  • DNS 解析失败
  • 无法连接服务器
  • TLS 握手失败
  • 连接超时
  • 读取过程中连接断开
  • Call 被取消

注意,IOException 和 HTTP 错误状态是两类不同问题:

IOException:网络层或 I/O 层没有正常完成
HTTP 404/500:服务器成功返回了一个 HTTP 响应,但响应表示业务失败

11. 异步执行:enqueue()

异步执行不会阻塞当前线程。OkHttp 会在后台线程执行请求,然后调用回调:

Call call = client.newCall(request);

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        System.err.println("Network failure: " + e.getMessage());
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        try (Response ignored = response) {
            if (!response.isSuccessful()) {
                System.err.println("HTTP error: " + response.code());
                return;
            }

            ResponseBody body = response.body();
            if (body == null) {
                System.err.println("Response body is empty");
                return;
            }

            String text = body.string();
            System.out.println(text);
        }
    }
});

11.1 onFailure 和 onResponse 什么时候调用

  • onFailure:请求没有拿到可用的 HTTP 响应,例如连接失败、超时、取消。
  • onResponse:已经收到了 HTTP 响应,即使状态码是 404 或 500。

因此不能只在 onFailure 中处理所有错误。onResponse 里仍然要检查 response.isSuccessful() 或具体状态码。

11.2 异步回调中的线程

OkHttp 的回调默认在它的调度线程中执行,不要假设它运行在 Android 主线程。如果要更新 UI,应切换回 UI 线程。

Kotlin 示例:

client.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        println("Network failure: " + e.message)
    }

    override fun onResponse(call: Call, response: Response) {
        response.use {
            println("HTTP code: " + it.code)
            println(it.body?.string())
        }
    }
})

12. Response 该如何读取

12.1 状态码

int code = response.code();

常见状态码:

状态码常见含义
200请求成功并返回内容
201创建资源成功
204成功,但没有响应内容
301/302重定向
400请求参数错误
401未认证或 Token 无效
403已识别身份但没有权限
404资源不存在
408请求超时
429请求过多
500服务器内部错误
502/503/504网关或服务暂时不可用

12.2 isSuccessful()

if (response.isSuccessful()) {
    // 200 <= code < 300
}

它只是一个范围判断,不会解析业务 JSON,也不会说明“业务是否成功”。例如某些接口会返回 HTTP 200,但 JSON 中有 success: false,那需要由业务层继续判断。

12.3 响应头

读取单个响应头:

String contentType = response.header("Content-Type");
String requestId = response.header("X-Request-Id");

读取带默认值的响应头:

String cacheControl = response.header("Cache-Control", "");

读取全部响应头:

for (String name : response.headers().names()) {
    System.out.println(name + ": " + response.header(name));
}

如果同一个头有多个值,可以使用:

List<String> values = response.headers("Set-Cookie");

12.4 响应体文本

String text = response.body().string();

string() 会读取完整内容,并根据响应头或默认规则解码为字符串。适合小型 JSON 或文本响应。

12.5 响应体字节

byte[] data = response.body().bytes();

适合小型图片或小文件。文件较大时会消耗大量内存。

12.6 流式读取大文件

import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

Path target = Path.of("download.bin");

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("Download failed: " + response.code());
    }

    ResponseBody body = response.body();
    if (body == null) {
        throw new IOException("Response body is empty");
    }

    try (InputStream input = body.byteStream();
         OutputStream output = Files.newOutputStream(target)) {
        byte[] buffer = new byte[8192];
        int count;
        while ((count = input.read(buffer)) != -1) {
            output.write(buffer, 0, count);
        }
    }
}

这里的核心是分块复制:每次只读取 8 KB,避免把整个文件一次性放入内存。

13. 正确关闭 Response

推荐写法:

try (Response response = client.newCall(request).execute()) {
    String result = response.body().string();
}

异步回调也要关闭:

@Override
public void onResponse(Call call, Response response) throws IOException {
    try (Response ignored = response) {
        // 读取 response
    }
}

Kotlin:

client.newCall(request).execute().use { response ->
    val result = response.body?.string()
}

不关闭响应可能导致连接无法回收到连接池,长时间运行后出现连接耗尽、请求变慢或资源泄漏。

14. 超时配置

OkHttp 有多种超时,它们解决的问题不同:

import java.time.Duration;

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(Duration.ofSeconds(10))
        .readTimeout(Duration.ofSeconds(30))
        .writeTimeout(Duration.ofSeconds(30))
        .callTimeout(Duration.ofSeconds(60))
        .build();

含义:

  • connectTimeout:建立连接允许等待多久。
  • readTimeout:等待读取数据允许等待多久。
  • writeTimeout:发送请求数据允许等待多久。
  • callTimeout:整个 Call 从开始到结束的总时间上限,包含 DNS、连接、重定向、读取等过程。

不同版本和 Java/Kotlin 平台也可以使用 TimeUnit 重载:

OkHttpClient client = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .callTimeout(60, TimeUnit.SECONDS)
        .build();

不要简单地把所有超时设置成很大的数。超时应该与接口类型、网络环境和用户可接受等待时间相匹配。

15. 取消请求

调用 cancel() 可以取消同步或异步 Call:

Call call = client.newCall(request);

// 另一个线程或取消按钮的事件中调用
call.cancel();

判断是否已取消:

if (call.isCanceled()) {
    System.out.println("Call was canceled");
}

典型场景:

  • 用户离开页面,取消页面不再需要的请求
  • 搜索框输入变化,取消旧关键词的搜索
  • 超过业务截止时间,取消仍未完成的请求

取消后,异步回调通常会走 onFailure,异常可能是 IOException。不要把用户主动取消误报成服务器故障。

16. 创建和复用客户端

基础客户端:

OkHttpClient client = new OkHttpClient();

带配置的客户端:

OkHttpClient client = new OkHttpClient.Builder()
        .retryOnConnectionFailure(true)
        .followRedirects(true)
        .build();

常见选项:

  • retryOnConnectionFailure(true):在部分连接失败场景下尝试恢复。它不等于无条件重试所有业务请求。
  • followRedirects(true):自动跟随 HTTP 重定向。
  • followSslRedirects(true):允许 HTTP 和 HTTPS 之间的安全重定向规则。
  • proxy(...):指定代理。
  • cache(...):配置 HTTP 缓存。
  • cookieJar(...):配置 Cookie 管理。
  • authenticator(...):处理某些认证挑战。

16.1 修改已有客户端

如果只需要在已有客户端基础上增加少量配置,可以使用 newBuilder():

OkHttpClient baseClient = new OkHttpClient();

OkHttpClient shortTimeoutClient = baseClient.newBuilder()
        .callTimeout(5, TimeUnit.SECONDS)
        .build();

这样可以保留基础客户端的连接池、调度器等共享资源,同时得到一套不同配置的客户端。

17. 拦截器 Interceptor

拦截器可以在请求发送前和响应返回后执行统一逻辑。结构大致是:

请求 -> 拦截器1 -> 拦截器2 -> 网络
响应 <- 拦截器1 <- 拦截器2 <- 网络

最小拦截器:

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(chain -> {
            Request oldRequest = chain.request();
            Request newRequest = oldRequest.newBuilder()
                    .header("X-App-Version", "1.0.0")
                    .build();

            return chain.proceed(newRequest);
        })
        .build();

逐步拆开:

Request oldRequest = chain.request();

取出当前请求。

Request newRequest = oldRequest.newBuilder()
        .header("X-App-Version", "1.0.0")
        .build();

以原请求为基础复制一个 Builder,加入或替换请求头。不要直接修改已经构建好的 Request。

return chain.proceed(newRequest);

把新请求交给拦截器链的下一环。通常每个拦截器必须调用一次 proceed(),并返回下游的 Response。

17.1 Application interceptor 和 Network interceptor

添加方式:

new OkHttpClient.Builder()
        .addInterceptor(applicationInterceptor)
        .addNetworkInterceptor(networkInterceptor)
        .build();

简单区别:

  • Application interceptor:观察整个 Call,更适合统一添加业务请求头、重试策略和业务日志;重定向等内部过程通常不会分别暴露给它。
  • Network interceptor:更接近实际网络请求,可以观察每次网络交互;重定向、缓存等场景下的行为更细粒度。

普通业务代码优先从 application interceptor 开始。

17.2 认证 Token 拦截器

String token = "your-token";

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(chain -> {
            Request requestWithAuth = chain.request()
                    .newBuilder()
                    .header("Authorization", "Bearer " + token)
                    .build();
            return chain.proceed(requestWithAuth);
        })
        .build();

如果 Token 会过期,不要在拦截器里无条件递归重试。应限制重试次数,并明确区分 401、网络失败和业务错误。

18. HTTP 日志拦截器

日志拦截器是单独的依赖:

dependencies {
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
}

使用:

import okhttp3.logging.HttpLoggingInterceptor;

HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BASIC);

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(logging)
        .build();

常用级别:

  • NONE:不记录。
  • BASIC:请求方法、URL、响应状态和耗时。
  • HEADERS:额外记录请求头和响应头。
  • BODY:额外记录请求体和响应体。

开发环境可以使用 BODY,生产环境要谨慎。请求体和响应体可能包含 Token、Cookie、密码、身份证号或其他隐私数据。正式项目通常还会对敏感头和敏感字段做脱敏。

19. HTTP 缓存

OkHttp 支持基于 HTTP 缓存语义的缓存。基本配置:

import java.io.File;
import okhttp3.Cache;

File cacheDirectory = new File("http-cache");
long cacheSize = 10L * 1024L * 1024L;
Cache cache = new Cache(cacheDirectory, cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
        .cache(cache)
        .build();

缓存是否生效主要取决于服务器返回的 Cache-Control、ETag、Last-Modified 等响应头。仅仅配置 Cache 并不意味着所有响应都会被缓存。

不要把包含用户私密信息的响应随意配置成公共缓存。缓存目录也需要按照应用的安全要求管理。

20. Cookie

默认情况下,OkHttp 不会像浏览器那样自动持久化 Cookie。需要自己提供 CookieJar 实现,或使用项目已有的 Cookie 管理方案。

接口示意:

CookieJar cookieJar = new CookieJar() {
    @Override
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        // 保存 cookies
    }

    @Override
    public List<Cookie> loadForRequest(HttpUrl url) {
        // 返回当前 URL 应发送的 cookies
        return Collections.emptyList();
    }
};

OkHttpClient client = new OkHttpClient.Builder()
        .cookieJar(cookieJar)
        .build();

Cookie 的保存位置、过期时间、域名匹配和加密方式属于应用层责任。不要把敏感 Cookie 持久化到明文日志或不安全存储中。

21. 错误处理的正确分层

一个实用的处理模板如下:

try (Response response = client.newCall(request).execute()) {
    int code = response.code();

    if (code == 401) {
        // 登录状态失效
    } else if (code == 404) {
        // 资源不存在
    } else if (code == 429) {
        // 请求过多,根据 Retry-After 等信息处理
    } else if (!response.isSuccessful()) {
        // 其他 HTTP 错误
    } else {
        // 2xx 成功响应
        ResponseBody body = response.body();
        if (body != null) {
            String text = body.string();
        }
    }
} catch (IOException e) {
    // DNS、连接、TLS、读写、超时、取消等网络层异常
}

建议把错误分成三层:

  1. 网络层错误:没有收到可用响应,通常是 IOException。
  2. HTTP 层错误:收到了响应,但状态码是 4xx 或 5xx。
  3. 业务层错误:HTTP 是 2xx,但 JSON 中的业务状态表示失败。

这样可以避免把服务器返回的 404 错误错误地当成“网络断开”。

22. 响应 JSON 的解析

OkHttp 只负责把响应体读成文本或字节,不负责把 JSON 自动变成 Java 对象。

String json = response.body().string();
User user = gson.fromJson(json, User.class);

注意顺序:

try (Response response = ...) {
    ResponseBody body = response.body();
    if (body == null) {
        throw new IOException("Response body is empty");
    }

    String json = body.string();
    User user = gson.fromJson(json, User.class);
}

不要先调用一次 string() 打日志,再调用第二次交给 Gson,因为第二次通常已经没有内容了。调试时应该先保存到变量:

String json = response.body().string();
System.out.println(json);
User user = gson.fromJson(json, User.class);

23. 一个可复用的 Java 请求方法

下面是一个小型封装,展示如何集中处理状态码和响应体:

import java.io.IOException;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public final class SimpleHttp {
    private final OkHttpClient client;

    public SimpleHttp(OkHttpClient client) {
        this.client = client;
    }

    public String get(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .header("Accept", "application/json")
                .get()
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException(
                        "GET " + url + " failed with HTTP " + response.code()
                );
            }

            ResponseBody body = response.body();
            if (body == null) {
                return "";
            }

            return body.string();
        }
    }
}

这个封装的职责很有限:发送 GET 并返回文本。认证刷新、重试、JSON 解析、业务错误映射等逻辑应根据项目规模放在更合适的层中,不要把所有逻辑都塞进一个“万能 HTTP 工具类”。

24. 测试:使用 MockWebServer

测试网络代码时,不建议依赖真实线上服务。OkHttp 官方生态提供 MockWebServer,可以在测试中启动本地 HTTP 服务,预设响应并检查请求。

依赖:

testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'

Java 示例:

import static org.junit.jupiter.api.Assertions.assertEquals;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.Test;

class HttpTest {
    @Test
    void sendsRequestAndReadsResponse() throws Exception {
        try (MockWebServer server = new MockWebServer()) {
            server.enqueue(new MockResponse()
                    .setResponseCode(200)
                    .setHeader("Content-Type", "text/plain")
                    .setBody("hello"));
            server.start();

            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    .url(server.url("/hello"))
                    .get()
                    .build();

            try (Response response = client.newCall(request).execute()) {
                assertEquals(200, response.code());
                assertEquals("hello", response.body().string());
            }

            RecordedRequest recorded = server.takeRequest();
            assertEquals("/hello", recorded.getPath());
            assertEquals("GET", recorded.getMethod());
        }
    }
}

测试应覆盖:

  • 2xx 成功响应
  • 4xx 和 5xx 响应
  • 空响应体
  • 非法 JSON
  • 超时和取消
  • 请求头、查询参数、请求体是否正确

25. Java 与 Kotlin 的 API 写法差异

OkHttp 4 使用 Kotlin 编写,但仍提供 Java API。IDEA 可以将 Java 转换为 Kotlin,不过转换后最好人工检查:

  • Kotlin 的属性访问,例如 response.code 对应 Java 的 response.code()。
  • Kotlin 的空安全,例如 response.body?.string()。
  • Kotlin 的扩展函数和默认参数。
  • use {} 对资源关闭的表达方式。

同一个请求的对照:

Java:

MediaType JSON = MediaType.get("application/json; charset=utf-8");
RequestBody body = RequestBody.create("{\"name\":\"Ada\"}", JSON);

Request request = new Request.Builder()
        .url("https://api.example.com/users")
        .header("Accept", "application/json")
        .post(body)
        .build();

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("HTTP " + response.code());
    }

    ResponseBody responseBody = response.body();
    if (responseBody != null) {
        String text = responseBody.string();
    }
}

Kotlin:

val json = "{\"name\":\"Ada\"}"
val body = json.toRequestBody("application/json; charset=utf-8".toMediaType())

val request = Request.Builder()
    .url("https://api.example.com/users")
    .header("Accept", "application/json")
    .post(body)
    .build()

client.newCall(request).execute().use { response ->
    check(response.isSuccessful) { "HTTP " + response.code }
    val text = response.body?.string().orEmpty()
}

Kotlin 中的 toMediaType() 和 toRequestBody() 是 OkHttp 提供的扩展函数,需要导入:

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody

26. 常见误区

26.1 每次请求都创建 OkHttpClient

问题:连接池、线程和缓存无法充分复用。

建议:复用应用级客户端;需要不同超时时间时使用已有客户端的 newBuilder()。

26.2 认为 404 会自动进入 onFailure

问题:onFailure 主要表示没有成功拿到 HTTP 响应。404 通常会进入 onResponse。

建议:在 onResponse 中检查 isSuccessful() 和具体状态码。

26.3 忘记关闭 Response

问题:响应体和底层连接资源无法及时释放。

建议:同步用 try-with-resources,Kotlin 用 use,异步回调也要关闭。

26.4 读取响应体两次

问题:ResponseBody 是一次性的,第二次可能得到空内容或异常。

建议:只读取一次并保存到变量。

26.5 用字符串拼接 URL

问题:特殊字符可能破坏 URL 或产生错误编码。

建议:用 HttpUrl.Builder 的 addQueryParameter() 和 addPathSegment()。

26.6 用 bytes() 下载大文件

问题:整个文件会进入内存,容易造成内存压力。

建议:使用 byteStream() 分块写入文件。

26.7 在生产环境打开 BODY 日志

问题:可能泄露 Token、Cookie、密码和个人信息。

建议:生产环境使用 NONE 或 BASIC,并对敏感内容脱敏。

26.8 直接信任服务器返回的 Content-Type

问题:服务器配置错误或恶意响应可能导致错误解析。

建议:根据接口约定、状态码和内容类型校验;处理文件时还要考虑文件大小限制和保存路径安全。

26.9 无限制地自动重试 POST

问题:POST 可能已经在服务器执行成功,只是客户端没有收到响应,再次重试可能造成重复创建或重复扣款。

建议:只对明确幂等的请求设计重试;限制次数、使用退避,并在支付等场景使用幂等键。

27. 从一个请求到网络完成的完整过程

下面是一次同步请求的大致过程:

  1. 你创建 Request.Builder。
  2. 通过 url()、header()、get() 或 post() 填写请求。
  3. build() 生成不可变的 Request。
  4. client.newCall(request) 创建一次性的 Call。
  5. execute() 或 enqueue() 触发执行。
  6. OkHttp 解析 URL,查找或建立连接。
  7. 发送请求行、请求头和请求体。
  8. 接收服务器的状态码、响应头和响应体。
  9. 生成 Response 并返回或回调给你。
  10. 你读取响应体并根据状态码、响应头和业务内容处理结果。
  11. 关闭 Response,让连接能够复用或释放。

这个流程也解释了各对象的职责:

Client 负责通信配置和资源管理
Request 负责描述“要发送什么”
Call 负责表示“一次可执行任务”
Response 负责描述“服务器返回了什么”
ResponseBody 负责提供响应内容

28. 推荐的学习顺序

建议按下面顺序练习:

  1. 用 OkHttpClient 和 Request.Builder 完成 GET。
  2. 学会检查 response.code()、isSuccessful() 和 body().string()。
  3. 学会用 try-with-resources 关闭 Response。
  4. 用 HttpUrl.Builder 添加查询参数。
  5. 分别练习 JSON、表单和 multipart 请求体。
  6. 对比 execute() 和 enqueue()。
  7. 增加超时和取消。
  8. 使用拦截器统一添加请求头和日志。
  9. 用 MockWebServer 编写请求测试。
  10. 最后再加入缓存、Cookie、Token 刷新和有限重试。

29. 一份可记忆的最小模板

Java:

OkHttpClient client = new OkHttpClient();

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

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("HTTP " + response.code());
    }

    ResponseBody body = response.body();
    if (body != null) {
        String result = body.string();
        System.out.println(result);
    }
}

记忆口诀可以简化成:

Client 配置通信
Request 描述请求
Call 执行一次
Response 检查结果
Body 读取内容
Close 释放资源

当你能熟练解释下面这段代码的每一行时,就已经掌握了 OkHttp 的核心使用方式:

Request request = new Request.Builder()
        .url("https://api.example.com/users")
        .addHeader("Accept", "application/json")
        .get()
        .build();

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("HTTP " + response.code());
    }

    ResponseBody body = response.body();
    if (body != null) {
        String json = body.string();
    }
}
最后修改:2026 年 10 月 07 日
如果觉得我的文章对你有用,请随意赞赏!