提示:本文章由 AI(OpenAI GPT 5.6 Sol)生成。
本站内文章,凡是转载/AI 生成,均会在标题及文字最开头清晰标注。若无标注,则为笔者的原创文章。

OkHttp 是 Java 和 Android 生态中常用的 HTTP 客户端,适合发送:

  • GET、POST、PUT、DELETE 请求
  • 表单数据
  • JSON 数据
  • 文件上传
  • 文件下载
  • 自定义请求头
  • HTTPS 请求
  • 请求重试、超时、缓存和拦截器

本文使用 Java,并统一使用 okhttp3 包名。


1. OkHttp 3 与 OkHttp 4 的关系

OkHttp 3.x 使用的包名是:

okhttp3.OkHttpClient

后来 OkHttp 4.x 虽然主要使用 Kotlin 实现,但 Java 代码仍然使用 okhttp3 包名。

如果项目必须使用 OkHttp 3.x,可以使用 3.x 最后的版本:

Maven

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

Gradle

implementation 'com.squareup.okhttp3:okhttp:3.14.9'

如果要使用日志拦截器,还需要添加:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>logging-interceptor</artifactId>
    <version>3.14.9</version>
</dependency>

2. OkHttp 的基本工作流程

一次 HTTP 请求通常分为以下几步:

创建 OkHttpClient
        ↓
创建 Request
        ↓
通过 client.newCall(request) 创建 Call
        ↓
同步执行 execute()
或异步执行 enqueue()
        ↓
读取 Response
        ↓
关闭 Response

最基本的代码结构如下:

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()) {
        String result = response.body().string();
        System.out.println(result);
    }
}

需要注意:

  1. Response 必须关闭。
  2. ResponseBody.string() 只能读取一次。
  3. HTTP 状态码不是 2xx 时,isSuccessful() 会返回 false
  4. 网络请求可能抛出 IOException

3. 创建 OkHttpClient

3.1 最简单的创建方式

OkHttpClient client = new OkHttpClient();

实际项目中通常使用 Builder 配置超时时间:

import okhttp3.OkHttpClient;

import java.util.concurrent.TimeUnit;

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

这三个超时时间分别表示:

配置含义
connectTimeout与服务器建立连接的超时时间
readTimeout等待服务器返回数据的超时时间
writeTimeout向服务器发送请求数据的超时时间

不要为每个请求都创建一个新的 OkHttpClient。推荐在应用中复用同一个实例:

public final class HttpClientProvider {

    private static final OkHttpClient CLIENT =
            new OkHttpClient.Builder()
                    .connectTimeout(10, TimeUnit.SECONDS)
                    .readTimeout(30, TimeUnit.SECONDS)
                    .writeTimeout(30, TimeUnit.SECONDS)
                    .build();

    private HttpClientProvider() {
    }

    public static OkHttpClient getClient() {
        return CLIENT;
    }
}

复用客户端可以充分利用:

  • TCP 连接池
  • Keep-Alive
  • HTTP/2 多路复用
  • 线程调度器
  • 缓存配置

4. GET 请求

4.1 简单 GET 请求

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

import java.io.IOException;

public class GetExample {

    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("HTTP 错误,状态码:" + response.code());
            }

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

.get() 可以省略,因为没有请求体时,默认就是 GET:

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

4.2 携带查询参数

不要直接通过字符串拼接参数,应该使用 HttpUrl 自动完成 URL 编码:

import okhttp3.HttpUrl;
import okhttp3.Request;

HttpUrl url = HttpUrl.parse("https://httpbin.org/get")
        .newBuilder()
        .addQueryParameter("keyword", "Java 网络请求")
        .addQueryParameter("page", "1")
        .build();

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

最终 URL 大致类似:

https://httpbin.org/get?keyword=Java%20%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82&page=1

addQueryParameter 会自动处理空格、中文和特殊字符。


5. 同步请求与异步请求

5.1 同步请求

同步请求使用:

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

完整示例:

try (Response response = client.newCall(request).execute()) {
    String result = response.body() == null
            ? ""
            : response.body().string();

    System.out.println(result);
}

同步请求会阻塞当前线程,因此:

  • Java 服务端中不要长时间阻塞重要业务线程
  • Android 中不能在主线程执行同步网络请求
  • 适合放在后台线程、线程池或任务调度器中

5.2 异步请求

异步请求使用:

client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        // 网络失败、连接失败、超时等
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        // 请求完成
    }
});

完整示例:

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

import java.io.IOException;

OkHttpClient client = new OkHttpClient();

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

client.newCall(request).enqueue(new Callback() {

    @Override
    public void onFailure(Call call, IOException e) {
        System.err.println("请求失败:" + e.getMessage());
    }

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

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

在 OkHttp 3 中,异步回调通常运行在 OkHttp 的后台线程中。如果是 Android,需要切回主线程更新界面。


6. POST 请求

6.1 POST 表单请求

使用 FormBody

import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;

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

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

发送请求:

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("请求失败,状态码:" + response.code());
    }

    String result = response.body() == null
            ? ""
            : response.body().string();

    System.out.println(result);
}

FormBody 会发送类似下面的数据:

username=zhangsan&password=123456

请求头中的 Content-Type 通常是:

application/x-www-form-urlencoded

6.2 POST JSON 请求

OkHttp 本身只负责发送 HTTP 请求,不负责 JSON 序列化和反序列化。JSON 可以使用 Jackson、Gson 等库处理。

先准备 JSON 字符串:

String json = "{"
        + "\"name\":\"张三\","
        + "\"age\":20"
        + "}";

创建请求体:

import okhttp3.MediaType;
import okhttp3.RequestBody;

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

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

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

执行请求:

try (Response response = client.newCall(request).execute()) {
    String result = response.body() == null
            ? ""
            : response.body().string();

    System.out.println(result);
}

更推荐使用 JSON 库生成 JSON:

ObjectMapper objectMapper = new ObjectMapper();

Map<String, Object> data = new HashMap<>();
data.put("name", "张三");
data.put("age", 20);

String json = objectMapper.writeValueAsString(data);

这样可以避免手工拼接 JSON 时出现引号、转义和类型错误。


7. PUT、DELETE 和 PATCH 请求

7.1 PUT

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

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

7.2 DELETE

没有请求体的 DELETE:

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

带请求体的 DELETE:

RequestBody body = RequestBody.create(
        MediaType.parse("application/json; charset=utf-8"),
        "{\"id\":1001}"
);

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

7.3 PATCH

OkHttp 3 没有专门的 .patch() 方法,可以使用:

Request request = new Request.Builder()
        .url("https://example.com/api/user/1001")
        .method("PATCH", body)
        .build();

8. 设置请求头

使用 .header()

Request request = new Request.Builder()
        .url("https://example.com/api/user")
        .header("Accept", "application/json")
        .header("User-Agent", "MyJavaClient/1.0")
        .build();

header() 会替换同名请求头。

使用 .addHeader()

Request request = new Request.Builder()
        .url("https://example.com/api")
        .addHeader("X-Trace-Id", "trace-001")
        .addHeader("X-Trace-Id", "trace-002")
        .build();

addHeader() 会追加同名请求头。

常见的认证请求头:

Request request = new Request.Builder()
        .url("https://example.com/api/profile")
        .header("Authorization", "Bearer your-access-token")
        .build();

实际项目中不应该把令牌硬编码在源代码中。


9. 使用拦截器统一处理请求

拦截器可以统一处理:

  • 添加 Token
  • 添加公共请求头
  • 打印日志
  • 统计耗时
  • 统一修改 URL
  • 处理公共错误

9.1 添加认证 Token

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

import java.io.IOException;

Interceptor authInterceptor = new Interceptor() {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();

        Request request = original.newBuilder()
                .header("Authorization", "Bearer your-access-token")
                .header("Accept", "application/json")
                .build();

        return chain.proceed(request);
    }
};

配置到客户端:

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

不要在每个业务请求中重复添加相同的公共请求头。


9.2 日志拦截器

import okhttp3.logging.HttpLoggingInterceptor;

HttpLoggingInterceptor loggingInterceptor =
        new HttpLoggingInterceptor();

loggingInterceptor.setLevel(
        HttpLoggingInterceptor.Level.BASIC
);

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

日志级别:

HttpLoggingInterceptor.Level.NONE
HttpLoggingInterceptor.Level.BASIC
HttpLoggingInterceptor.Level.HEADERS
HttpLoggingInterceptor.Level.BODY

开发环境可以使用:

HttpLoggingInterceptor.Level.BODY

生产环境通常建议使用:

HttpLoggingInterceptor.Level.BASIC

或者关闭日志。

注意:BODY 可能输出:

  • 用户密码
  • Token
  • Cookie
  • 身份证号
  • 文件内容
  • 业务敏感数据

生产环境不要无条件开启完整 BODY 日志。


9.3 应用拦截器与网络拦截器

应用拦截器:

.addInterceptor(interceptor)

网络拦截器:

.addNetworkInterceptor(interceptor)

简单理解:

  • 应用拦截器:更适合处理业务层公共逻辑
  • 网络拦截器:更接近底层网络连接,可以观察重定向、缓存和实际网络请求

大多数业务场景使用普通应用拦截器即可。


10. 文件上传

10.1 上传单个文件

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

import java.io.File;

File file = new File("/tmp/avatar.jpg");

RequestBody fileBody = RequestBody.create(
        MediaType.parse("image/jpeg"),
        file
);

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

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

执行请求:

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        throw new IOException("文件上传失败:" + response.code());
    }

    System.out.println("上传成功");
}

10.2 同时上传普通字段

RequestBody multipartBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("userId", "1001")
        .addFormDataPart("description", "用户头像")
        .addFormDataPart(
                "file",
                file.getName(),
                fileBody
        )
        .build();

服务器接收到的字段名称必须与 addFormDataPart() 中的名称一致。


11. 文件下载

下载大文件时,不要使用:

response.body().bytes()

因为这会把整个文件一次性加载到内存中。应该使用流式读取:

import okhttp3.Response;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

File target = new File("/tmp/download.zip");

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

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

    if (response.body() == null) {
        throw new IOException("响应体为空");
    }

    try (InputStream input = response.body().byteStream();
         OutputStream output = new FileOutputStream(target)) {

        byte[] buffer = new byte[8192];
        int length;

        while ((length = input.read(buffer)) != -1) {
            output.write(buffer, 0, length);
        }
    }
}

如果需要显示下载进度,需要自定义 ResponseBody,根据已读取的字节数回调进度。


12. HTTP 状态码和异常处理

一次请求可能出现三类问题。

12.1 网络异常

例如:

  • DNS 解析失败
  • 连接超时
  • 服务器拒绝连接
  • 网络断开
  • TLS 握手失败

通常会进入:

onFailure()

或者同步请求抛出:

IOException

12.2 HTTP 错误

例如:

  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 500 Internal Server Error

这类情况不一定会进入 onFailure(),请求本身可能已经成功完成,只是服务器返回了错误状态码。

因此必须主动检查:

if (!response.isSuccessful()) {
    System.out.println("HTTP 状态码:" + response.code());
}

12.3 业务错误

有些接口即使返回 200,业务上仍然可能失败:

{
  "code": 10001,
  "message": "余额不足",
  "data": null
}

因此实际项目应该分别处理:

网络层是否成功
HTTP 状态码是否成功
业务 code 是否成功

不要只判断 HTTP 状态码。


13. Response 的正确处理方式

推荐使用 try-with-resources:

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        String errorBody = response.body() == null
                ? ""
                : response.body().string();

        throw new IOException(
                "HTTP " + response.code() + ": " + errorBody
        );
    }

    String body = response.body() == null
            ? ""
            : response.body().string();

    System.out.println(body);
}

注意:

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

第二次读取通常会得到空内容,因为响应体是一次性流。

如果需要多次使用内容,应先保存到变量:

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

14. 取消请求

异步请求可以保存 Call 对象:

Call call = client.newCall(request);

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        if (call.isCanceled()) {
            System.out.println("请求已取消");
            return;
        }

        System.out.println("请求失败");
    }

    @Override
    public void onResponse(Call call, Response response)
            throws IOException {
        try (Response closeableResponse = response) {
            System.out.println(closeableResponse.code());
        }
    }
});

取消请求:

call.cancel();

也可以取消客户端中的全部请求:

client.dispatcher().cancelAll();

在 Android 中,页面销毁时通常应该取消与页面绑定的请求,避免请求完成后继续更新已经销毁的界面。


15. Call 不能重复执行

一个 Call 只能执行一次:

Call call = client.newCall(request);

call.execute();
call.execute(); // 错误

如果需要重新执行,应创建新的 Call:

client.newCall(request).execute();

或者复制 Call:

Call anotherCall = call.clone();

16. HTTP 缓存

OkHttp 支持 HTTP 缓存:

import okhttp3.Cache;

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

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

缓存大小为 10 MB。

缓存是否生效还取决于服务器响应头,例如:

Cache-Control: max-age=60
ETag: "abc123"
Last-Modified: ...

客户端强制使用缓存:

Request request = new Request.Builder()
        .url("https://example.com/api/config")
        .cacheControl(CacheControl.FORCE_CACHE)
        .build();

客户端强制访问网络:

Request request = new Request.Builder()
        .url("https://example.com/api/config")
        .cacheControl(CacheControl.FORCE_NETWORK)
        .build();

一般来说,是否缓存应该由服务器通过 HTTP 缓存头控制,而不是客户端随意覆盖。


17. 重试和重定向

OkHttp 默认会处理部分连接失败和重定向。

可以配置自动重试连接:

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

但不要简单地对所有请求进行无限重试,尤其是:

  • 支付请求
  • 创建订单
  • 提交表单
  • 文件上传
  • 修改数据的 POST 请求

因为请求可能已经在服务器端执行成功,但客户端只是没有收到响应。此时盲目重试可能造成重复操作。

如果业务需要重试,应该配合:

  • 请求幂等设计
  • 幂等键
  • 最大重试次数
  • 指数退避
  • 只重试明确可重试的异常

18. HTTPS 和安全配置

正常情况下,OkHttp 会使用系统的证书校验和主机名校验:

OkHttpClient client = new OkHttpClient();

不要为了测试 HTTPS 而使用“信任所有证书”的代码,也不要关闭主机名校验,例如:

hostnameVerifier((hostname, session) -> true)

这种做法会导致中间人攻击风险。

正确做法是:

  • 使用有效的 HTTPS 证书
  • 正确配置服务器证书链
  • 使用系统默认的 TrustManager
  • 需要证书锁定时,使用 CertificatePinner
  • 不在日志中输出 Token 和敏感请求体

证书锁定示例:

CertificatePinner certificatePinner =
        new CertificatePinner.Builder()
                .add(
                        "example.com",
                        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
                )
                .build();

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

证书锁定需要谨慎维护,否则服务器证书更换时,客户端可能全部无法连接。


19. Android 中的额外注意事项

如果是在 Android 项目中使用,还需要在 AndroidManifest.xml 中添加网络权限:

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

不要在主线程执行:

client.newCall(request).execute();

应该使用:

client.newCall(request).enqueue(callback);

或者放入线程池:

ExecutorService executor = Executors.newFixedThreadPool(4);

executor.submit(() -> {
    try (Response response = client.newCall(request).execute()) {
        // 处理请求
    } catch (IOException e) {
        // 处理异常
    }
});

如果是在回调中更新 Android UI,需要切换回主线程。


20. 一个可复用的 GET 工具方法

可以先封装一个简单的同步 GET 方法:

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

import java.io.IOException;

public final class HttpUtils {

    private static final OkHttpClient CLIENT =
            new OkHttpClient.Builder()
                    .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
                    .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
                    .build();

    private HttpUtils() {
    }

    public static String get(String url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();

        try (Response response = CLIENT.newCall(request).execute()) {
            String body = response.body() == null
                    ? ""
                    : response.body().string();

            if (!response.isSuccessful()) {
                throw new IOException(
                        "HTTP 请求失败,状态码:"
                                + response.code()
                                + ",响应内容:"
                                + body
                );
            }

            return body;
        }
    }
}

调用:

try {
    String result = HttpUtils.get("https://httpbin.org/get");
    System.out.println(result);
} catch (IOException e) {
    e.printStackTrace();
}

不过,正式项目通常还需要加入:

  • JSON 反序列化
  • 统一错误对象
  • Token 刷新
  • 请求重试
  • 日志脱敏
  • 请求链路 ID
  • 请求取消
  • 统一线程调度
  • 业务状态码处理

21. 常见错误

错误一:忘记关闭 Response

错误:

Response response = client.newCall(request).execute();
System.out.println(response.body().string());

正确:

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

错误二:只判断 IOException,不判断 HTTP 状态码

错误:

try {
    Response response = client.newCall(request).execute();
    // 直接认为成功
} catch (IOException e) {
}

正确:

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) {
        // 处理 4xx、5xx
    }
}

错误三:每次请求都创建客户端

错误:

OkHttpClient client = new OkHttpClient();

如果这段代码在每次调用中执行,会浪费连接池和线程资源。

正确做法是复用客户端实例。

错误四:把大文件一次性读入内存

错误:

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

下载大文件时应使用:

InputStream input = response.body().byteStream();

错误五:在生产环境开启完整 BODY 日志

完整日志可能泄露密码、Token、Cookie 和个人信息。生产环境应关闭或进行脱敏。

错误六:把同步请求放到 Android 主线程

这会触发:

NetworkOnMainThreadException

应该使用异步请求或后台线程。


22. 推荐的项目实践

一个比较稳妥的 OkHttp 使用方式是:

全局复用一个 OkHttpClient
        ↓
通过拦截器统一添加公共请求头
        ↓
业务层只负责构造具体 Request
        ↓
统一处理网络错误和 HTTP 状态码
        ↓
统一进行 JSON 序列化和反序列化
        ↓
所有 Response 都使用 try-with-resources 关闭

核心原则可以总结为:

  1. OkHttpClient 尽量复用。
  2. Response 一定关闭。
  3. ResponseBody 只能消费一次。
  4. 网络异常和 HTTP 错误分别处理。
  5. 大文件使用流式读写。
  6. 不要信任所有 HTTPS 证书。
  7. 生产环境谨慎记录请求日志。
  8. 不要对非幂等请求进行无条件重试。
  9. 使用 HttpUrl 构造查询参数。
  10. 让 JSON 库负责 JSON 序列化和反序列化。

OkHttp 的核心 API 实际上并不复杂:OkHttpClient 负责配置和执行,Request 描述请求,Call 表示一次请求任务,Response 表示服务器响应。掌握这几个对象之后,再逐步加入拦截器、缓存、上传下载和统一错误处理,就可以满足绝大多数 Java HTTP 客户端需求。

最后修改:2026 年 08 月 28 日
如果觉得我的文章对你有用,请随意赞赏!