本站内文章,凡是转载/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);
}
}需要注意:
Response必须关闭。ResponseBody.string()只能读取一次。- HTTP 状态码不是
2xx时,isSuccessful()会返回false。 - 网络请求可能抛出
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=1addQueryParameter 会自动处理空格、中文和特殊字符。
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-urlencoded6.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()或者同步请求抛出:
IOException12.2 HTTP 错误
例如:
400 Bad Request401 Unauthorized403 Forbidden404 Not Found500 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 关闭核心原则可以总结为:
OkHttpClient尽量复用。Response一定关闭。ResponseBody只能消费一次。- 网络异常和 HTTP 错误分别处理。
- 大文件使用流式读写。
- 不要信任所有 HTTPS 证书。
- 生产环境谨慎记录请求日志。
- 不要对非幂等请求进行无条件重试。
- 使用
HttpUrl构造查询参数。 - 让 JSON 库负责 JSON 序列化和反序列化。
OkHttp 的核心 API 实际上并不复杂:OkHttpClient 负责配置和执行,Request 描述请求,Call 表示一次请求任务,Response 表示服务器响应。掌握这几个对象之后,再逐步加入拦截器、缓存、上传下载和统一错误处理,就可以满足绝大多数 Java HTTP 客户端需求。