Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

Java 中的 OkHttp:入门指南

OkHttp 是大多数 JVM 和 Android 项目最终都会采用的 HTTP 客户端,它有两项设计决策常会让新手感到困惑:该客户端设计为共享使用,且必须关闭响应正文。 只要正确处理好这两点,其余部分就都很简单明了。如果处理不当,就会导致连接泄漏,直到系统崩溃。 本指南涵盖了基础知识、拦截器、超时和代理——此外还附有一条关于项目当前托管位置的说明,该位置已发生变更。

我们的立场:我们是 Geonode,主要销售代理服务,而 OkHttp 的代理配置确实非常特殊——代理认证是通过 proxyAuthenticator 进行的,而不是通过请求头,如果配置错误,会返回 407 错误,这看起来像是凭据问题,但实际上是配置问题。相关内容在文章末尾部分。 该部分之前的所有内容在完全不使用代理的情况下也能正常工作,如果您正在学习该库,请先在自己的网络连接环境下进行学习。

OkHttp 当前的托管位置

这一点值得在开头说明,因为旧链接已失效。

OkHttp 的代码库已迁移。github.com/square/okhttp

现重定向至 github.com/lysine-dev/okhttp

,而原先位于 square.github.io/okhttp

的文档站点返回 404 错误——当前站点地址为 lysine.dev/okhttp

。 该项目正在积极维护中,并采用 Apache-2.0 许可证,这一点已于 2026 年 9 月得到验证。

Maven 坐标未发生变化。 它仍以 com.squareup.okhttp3:okhttp

的形式发布:

implementation("com.squareup.okhttp3:okhttp:5.5.0")

此外,还有一份物料清单(BOM)用于保持相关构建产物的同步:

implementation(platform("com.squareup.okhttp3:okhttp-bom:5.5.0"))

系统要求: Android 5.0 及以上(API 级别 21 及以上)和 Java 8 及以上。OkHttp 依赖于 Okio 进行 I/O 操作,并依赖于 Kotlin 标准库——项目方将这两者描述为“具有强大向后兼容性的小型库”。 在 Android 平台上,它使用 AndroidX Startup;若在清单文件中禁用初始化器,应用必须在 ``Application.onCreate`

中调用 ``OkHttp.initialize(applicationContext)

`。

旧版 ``3.12.x`

` 分支支持 Android 2.3 及以上版本和 Java 7,项目方直言不讳地指出这些平台“不支持 TLS 1.2,不应使用”。

您的第一个请求

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

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

这个七行代码的示例中有三点值得注意。

**try

-with-resources 并非可选。** Response

实现了 Closeable

接口,而未关闭的响应会占用其连接。如果连接泄漏得足够多,连接池就会耗尽,此时请求会挂起而非失败——这种现象看似网络问题,实则并非如此。

**body().string()

只能调用一次。** 它会消耗该流。调用两次会抛出异常,在响应关闭后调用也会抛出异常。如果需要多次使用响应正文,请将字符串存储起来。

**execute()

是同步的。** 对于异步操作,enqueue()

接受一个 Callback

参数,并在 OkHttp 的调度器线程池中运行。

POST 请求的结构与 POST 请求类似,但包含请求体:

public static final MediaType JSON = MediaType.get("application/json");

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(json, JSON);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

共享客户端

这是人们最常犯错的设计决策。

OkHttpClient

包含一个连接池和一个线程池。如果为每个请求都创建一个新的客户端,不仅会浪费本可重用的所有连接,还会创建随后被弃用的线程。这种做法虽然能工作,但速度很慢,且在高负载下会耗尽资源。

为您的应用程序创建一个客户端并共享它。 这种设计天生就是线程安全的。

当代码的某一部分需要不同配置时,不要从头构建第二个客户端——克隆现有的客户端,这样连接池和调度器就能被共享:

OkHttpClient shortTimeout = client.newBuilder()
    .readTimeout(5, TimeUnit.SECONDS)
    .build();

newBuilder()

生成的客户端会与其父客户端共享连接池和调度器,这正是你所需要的。

对于关闭操作,特别是在短寿命进程中,请显式释放资源:

client.dispatcher().executorService().shutdown();
client.connectionPool().evictAll();

如果不这样做,JVM 可能会在退出前因连接池的保持活动时间而挂起,这在 CLI 工具中调试起来会让人摸不着头脑。

OkHttp 会向您的请求中添加什么

该库会重写请求,了解它会添加什么内容,可以避免一类常见的困惑。

文档中明确指出:“OkHttp 可能会添加原始请求中不存在的请求头,包括 Content-Length、Transfer-Encoding、User-Agent、Host、Connection 以及 Content-Type。除非该请求头已存在,否则它会添加 Accept-Encoding 请求头以实现透明的响应压缩。如果请求包含 Cookie,OkHttp 会添加包含这些 Cookie 的 Cookie 请求头。”

这会带来两个后果。

透明压缩是自动进行的,且会自动为你进行反压缩。 OkHttp 会请求压缩,对响应进行解压缩,然后“会移除相应的响应头 Content-Encoding 和 Content-Length,因为它们不适用于解压缩后的响应正文”。 因此,响应中缺少 Content-Length 是正常现象,而非错误——除非你自己设置了 Accept-Encoding,在这种情况下,解压操作由你负责。

启用缓存时,条件请求会自动发生。 OkHttp 会添加 If-Modified-Since 和 If-None-Match 请求,以重新验证过期的缓存条目。

它还默认跟随重定向,并在遇到授权挑战时,“会请求 Authenticator(如果已配置)来满足该挑战”,并使用提供的凭据进行重试。

该库将自身描述为“遵循原则且避免过度可配置,特别是当此类配置旨在绕过存在缺陷的服务器、测试无效场景或与相关 RFC 相矛盾时”。 它坦诚地指出了自身的局限性——它“不允许带请求体的 GET 请求”,且缓存“并非具有替代实现的接口”。如果您需要发送故意无效的请求,该库并不适合此用途,而这是一种设计选择,而非疏忽。

拦截器

这是主要的扩展点,也是将决定您如何使用该库的关键特性。

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();
    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));
    return response;
  }
}

文档中特别强调:“调用 chain.proceed(request) 是每个拦截器实现的关键部分。这个看似简单的方法正是所有 HTTP 操作发生的地方。” 还有一条值得重视的警告:“如果 chain.proceed(request) 被调用多次,则必须关闭之前的响应正文。”

拦截器分为两种类型,正确选择至关重要。 请使用 addInterceptor() 或 addNetworkInterceptor() 进行注册。文档中对两者的区别进行了精确说明。

应用程序拦截器:

  • “无需担心重定向和重试等中间响应。”
  • “始终仅被调用一次,即使 HTTP 响应来自缓存。”
  • “遵循应用程序的原始意图。不涉及 OkHttp 注入的标头,例如 If-None-Match。”
  • “允许短路处理,无需调用 Chain.proceed()。”
  • “允许重试并多次调用 Chain.proceed()。”
  • “可通过 withConnectTimeout、withReadTimeout、withWriteTimeout 调整调用超时。”

网络拦截器:

  • “能够处理重定向和重试等中间响应。”
  • “对于绕过网络请求的缓存响应,不会被调用。”
  • “观察数据在网络传输时的原始状态。”
  • “可访问承载请求的Connection。”

实用准则:对于与请求意图相关的任何操作,请使用应用程序拦截器——例如添加授权头、用户代理或进行应用层日志记录。 对于实际通过网络传输的内容,请使用网络拦截器——检查压缩的请求体、查看每个重定向跳转、分析连接状态。

最常见的错误是将授权拦截器注册为网络拦截器,这会导致该拦截器在每次重定向跳转时触发一次,从而可能将您的凭据泄露给您未预期的主机。

超时

OkHttp 提供了合理的默认值和四项独立设置,了解是哪一项触发了超时,就能确定问题出在哪里。

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

**connectTimeout

** 涉及建立 TCP 和 TLS 连接。 **readTimeout

** 适用于单次读取操作之间,而非整个响应——因此,下载速度虽慢但仍在进行中的情况绝不会触发该超时。 **writeTimeout

** 对上传操作起同样的作用。 **callTimeout

** 对整个调用(包括重定向、重试和正文传输)进行限制。默认值为零,表示没有总体限制。

最后那个才是需要设置的参数。 如果没有它,一个持续以微量数据传输的调用可能会无限期运行,因为读取超时会在接收到每个字节时重置。callTimeout

是使任务结束的上限,它相当于 curl 中的 --max-time

—— 我们在 使用 curl 设置超时 中详细讨论过这一区别。

可以通过应用程序拦截器的 ``withReadTimeout`

` 及其相关方法实现单次调用的超时覆盖,这样你就可以为某个响应较慢的端点提供更多时间,而无需放宽所有请求的默认超时设置。

代理

这是 OkHttp 与大多数客户端的不同之处,也是人们容易在此耗费时间的地方。

Proxy proxy = new Proxy(Proxy.Type.HTTP,
    new InetSocketAddress("proxy.example.com", 9000));

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

对于 SOCKS,请使用 Proxy.Type.SOCKS,格式保持不变。

身份验证是让人们感到意外的部分。 不要设置 Proxy-Authorization 头部。 你需要提供一个 proxyAuthenticator,当代理发出 407 挑战时,OkHttp 会调用该方法:

Authenticator proxyAuth = (route, response) -> {
  if (response.request().header("Proxy-Authorization") != null) {
    return null;   // already tried these credentials; give up
  }
  String credential = Credentials.basic("user", "pass");
  return response.request().newBuilder()
      .header("Proxy-Authorization", credential)
      .build();
};

OkHttpClient client = new OkHttpClient.Builder()
    .proxy(proxy)
    .proxyAuthenticator(proxyAuth)
    .build();

返回 null 至关重要。如果没有它,密码错误会导致无限重试循环而非直接失败——OkHttp 会向身份验证器请求,收到相同的错误凭据,再收到另一个 407 响应,然后再次请求。 检查是否已发送 Proxy-Authorization 头部,即可打破这一循环。

另有三点说明。

407 状态码与 401 状态码不同。 407 表示代理拒绝了请求,且请求从未到达目标服务器;而 401 表示代理处理成功,目标服务器要求提供凭据,这完全是另一种 authenticator() 配置。

proxySelector() 允许你按请求而非按客户端选择代理,这样就能在不构建多个客户端的情况下,对不同主机进行差异化路由。

验证配置是否生效。 分别在配置和未配置代理的情况下,向一个会回显你地址的服务发送请求。 此处的配置错误通常不会产生明显提示——请求看似成功且直接通过——因此确认出口地址是唯一能确保配置正确的方法。这种“静默成功”的模式正是我们在为何测试代理至关重要一文中讨论过的。

正确处理响应

以下几种模式决定了代码是仅能正常运行,还是能在高负载下依然稳定运行。

**检查isSuccessful()

,而不仅仅是检查是否抛出异常。** OkHttp 仅在发生网络故障时抛出IOException

异常,而非因 HTTP 状态码错误。404 或 500 错误会以普通的Response

形式返回:

try (Response response = client.newCall(request).execute()) {
  if (!response.isSuccessful()) {
    String body = response.body() != null ? response.body().string() : "";
    throw new IOException("HTTP " + response.code() + " from " + request.url()
                          + ": " + body.substring(0, Math.min(200, body.length())));
  }
  return response.body().string();
}

包含错误正文的前半部分,可以将一个晦涩的状态码转化为明确指明问题的消息——大多数 API 都会在 400 状态码的正文中解释问题原因。

请勿将大型响应缓冲到 String 中。 body().string()

会将所有内容读入内存。对于大型下载,请使用流处理:

try (Response response = client.newCall(request).execute();
     BufferedSource source = response.body().source();
     BufferedSink sink = Okio.buffer(Okio.sink(new File("out.bin")))) {
  sink.writeAll(source);
}

异步调用仍需关闭请求主体,且回调在后台线程中运行:

client.newCall(request).enqueue(new Callback() {
  @Override public void onFailure(Call call, IOException e) {
    logger.warn("request failed", e);
  }
  @Override public void onResponse(Call call, Response response) throws IOException {
    try (ResponseBody body = response.body()) {
      handle(body.string());
    }
  }
});

请注意,onFailure

仅在出现网络问题时触发。HTTP 500 状态码会出现在 onResponse

中,这会让那些期望该名称字面意思的人感到意外。

重试需要谨慎处理。 OkHttp 会自动重试某些连接层级的故障,这由 retryOnConnectionFailure()

控制,该选项默认开启。它不会重试 HTTP 错误状态码,而且也不应该重试——重试一个可能已经成功的 POST 请求可能会导致写入操作重复。 若需添加自定义重试逻辑,请在应用程序拦截器中实现,设定重试次数上限,并在重试之间设置退避机制,并仅将其限制在幂等方法上(除非 API 提供了幂等键)。

它还能为您带来什么

简而言之,以下就是选择它的理由。

HTTP/2,其“支持功能可让所有发往同一主机的请求共享一个套接字”。 连接池,可“降低请求延迟(若无法使用 HTTP/2)”。透明 GZIP。响应缓存,可“让重复请求完全绕过网络”。

**高可用性。**它“能自动从常见的连接问题中恢复”,当服务拥有多个地址时,“若首次连接失败,将尝试其他地址”——该项目指出,“这对 IPv4+IPv6 以及托管在冗余数据中心中的服务而言是必要的”。

现代 TLS,包括 TLS 1.3、ALPN 和证书固定,采用该平台的实现。在 JVM 上,它还支持 Conscrypt,该库将 BoringSSL 与 Java 集成,若其为首个安全提供程序,则会自动启用。

**标准符合性。**该项目列出了其遵循的规范:RFC 9110(HTTP语义)、RFC 9111(缓存)、RFC 9112(HTTP/1.1)、 RFC 9113(HTTP/2)、RFC 6455(WebSockets)以及 WHATWG 制定的服务器发送事件规范。当规范存在歧义时,该项目“遵循现代用户代理,例如流行的浏览器或常见的 HTTP 库”。

大家还问

OkHttp 还在维护吗?

是的。代码仓库已从 square/okhttp 迁移至 lysine-dev/okhttp,文档网站现位于 lysine.dev/okhttp,但该项目仍在积极开发中,并采用 Apache-2.0 许可证。Maven 坐标仍为 com.squareup.okhttp3:okhttp。

是否应该为每个请求创建一个新的 OkHttpClient?

不需要。该客户端维护着连接池和线程池,并且在设计上具有线程安全性。只需为您的应用程序创建一个实例并共享即可。当需要不同的配置时,请对现有客户端调用 newBuilder(),这样连接池和线程池仍可共享。

为什么必须关闭响应?

因为 Response 会一直保持连接直到被关闭,而泄漏的响应会耗尽连接池。其表现为请求挂起而非失败,这很难诊断。请始终使用 try-with-resources。

应用程序拦截器与网络拦截器有什么区别?

应用程序拦截器会处理原始请求,且仅被调用一次(即使是缓存响应也是如此),并可能执行短路处理或重试。网络拦截器则会处理每次单独的网络交互(包括重定向和重试),能够访问连接,并且在处理缓存响应时会被完全跳过。

如何在 OkHttp 中设置代理?

将 java.net.Proxy 传递给 OkHttpClient.Builder.proxy()。对于身份验证,请设置 proxyAuthenticator 而不是使用请求头——当已存在 Proxy-Authorization 请求头时,请返回 null;否则,错误的凭据会导致无限重试循环。

应该设置哪些超时参数?

connectTimeout 约 10 秒,readTimeout 和 writeTimeout 约 30 秒,以及——最重要的是——callTimeout,该参数默认值为零,是唯一能限制整个调用时长的设置。如果没有它,响应数据缓慢滴答式传输时将永远不会超时,因为读取超时会在每个字节读取后重置。

OkHttp 会自动处理 GZIP 吗?

是的,如果你没有手动设置 Accept-Encoding。它会请求压缩,解压响应,并丢弃 Content-Encoding 和 Content-Length,因为它们不再描述解压后的正文。如果手动设置该标头,你也将承担解压工作。

OkHttp 需要哪个 Java 版本?

Java 8 或更高版本,以及 Android 5.0(API 级别 21)或更高版本。它依赖于 Okio 和 Kotlin 标准库。旧的 3.12.x 分支支持 Java 7 和 Android 2.3,但不支持 TLS 1.2,因此不应使用。

总结

OkHttp 是一个基于经过深思熟虑的实现构建的轻量级 API,掌握两个习惯就能基本确保正确使用它:在整个应用程序中共享一个客户端,并在每次响应后调用 try-with-resources 进行关闭。这两种情况发生时都不会报错,最终都会表现为请求卡死,而非可读的错误信息。

拦截器是您将投入大量时间的地方,而“应用程序”与“网络”之间的区别值得您系统地学习,而非通过试错来掌握。应用程序拦截器会检测您的意图并仅执行一次;网络拦截器则会处理每个跳转,并在遇到缓存响应时被跳过。 在网络层注册授权拦截器是一个经典错误,它会在每次重定向时触发。

请设置callTimeout。该参数默认值为零,它是唯一能限定整个调用范围的设置,若未设置,本应仅需几秒的任务有时会一直运行,直到被其他进程终止。

此外,如果您正在参考旧版文档,请检查其中的链接。该项目已迁移至 lysine.dev/okhttp,而由 Square 托管的网站已不复存在——不过构建包的坐标未变,因此您的构建文件无需做任何修改。