OkHttp 4 入门教程
面向第一次接触 OkHttp 的开发者。本文以 OkHttp 4.x 为背景,示例以 Java 为主,并在关键位置给出 Kotlin 写法。
1. OkHttp 是什么
OkHttp 是一个 HTTP 客户端库。它负责帮你完成这些工作:
- 创建 HTTP 请求
- 建立和复用 TCP/TLS 连接
- 发送请求头和请求体
- 接收响应状态码、响应头和响应体
- 处理连接复用、HTTP/2、缓存、重定向、超时和取消
- 通过拦截器统一添加日志、认证信息或错误处理
你通常不需要手动操作 Socket,也不需要自己拼接完整的 HTTP 报文。你只需要描述:
- 使用哪个 OkHttpClient。
- 创建什么样的 Request。
- 通过 client.newCall(request) 创建一个 Call。
- 同步执行或异步执行这个 Call。
- 从 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)这一步同时做两件事:
- 把 HTTP 方法设置为 POST。
- 把 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、读写、超时、取消等网络层异常
}建议把错误分成三层:
- 网络层错误:没有收到可用响应,通常是 IOException。
- HTTP 层错误:收到了响应,但状态码是 4xx 或 5xx。
- 业务层错误: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.toRequestBody26. 常见误区
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. 从一个请求到网络完成的完整过程
下面是一次同步请求的大致过程:
- 你创建 Request.Builder。
- 通过 url()、header()、get() 或 post() 填写请求。
- build() 生成不可变的 Request。
- client.newCall(request) 创建一次性的 Call。
- execute() 或 enqueue() 触发执行。
- OkHttp 解析 URL,查找或建立连接。
- 发送请求行、请求头和请求体。
- 接收服务器的状态码、响应头和响应体。
- 生成 Response 并返回或回调给你。
- 你读取响应体并根据状态码、响应头和业务内容处理结果。
- 关闭 Response,让连接能够复用或释放。
这个流程也解释了各对象的职责:
Client 负责通信配置和资源管理
Request 负责描述“要发送什么”
Call 负责表示“一次可执行任务”
Response 负责描述“服务器返回了什么”
ResponseBody 负责提供响应内容28. 推荐的学习顺序
建议按下面顺序练习:
- 用 OkHttpClient 和 Request.Builder 完成 GET。
- 学会检查 response.code()、isSuccessful() 和 body().string()。
- 学会用 try-with-resources 关闭 Response。
- 用 HttpUrl.Builder 添加查询参数。
- 分别练习 JSON、表单和 multipart 请求体。
- 对比 execute() 和 enqueue()。
- 增加超时和取消。
- 使用拦截器统一添加请求头和日志。
- 用 MockWebServer 编写请求测试。
- 最后再加入缓存、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();
}
}