我们的立场:我们是 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 托管的网站已不复存在——不过构建包的坐标未变,因此您的构建文件无需做任何修改。
