在Java开发中,HTTP请求是再常见不过的操作了。每次都要重复写创建OkHttpClient、构建Request、处理Response的代码,不仅效率低下,还容易出错。我见过不少项目里,同样的HTTP请求代码被复制粘贴了十几处,后期维护简直是一场噩梦。
封装工具类的好处显而易见:一是减少重复代码,二是统一处理异常和日志,三是方便后续功能扩展。就拿超时设置来说,如果没有统一封装,哪天需要把默认超时从30秒改成60秒,你就得把所有用到的地方都改一遍,想想都头大。
实际项目中,我遇到过最典型的场景是调用第三方支付接口。支付接口通常需要添加特定的请求头、处理复杂的签名逻辑,还要考虑重试机制。如果每次都从头写,不仅容易遗漏细节,还会让代码变得难以维护。这时候,一个设计良好的OkHttp工具类就能派上大用场。
先来看一个我实际项目中用过的工具类骨架:
java复制public class OkHttpUtils {
private static final OkHttpClient DEFAULT_CLIENT = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
// 私有构造防止实例化
private OkHttpUtils() {}
public static OkHttpClient getDefaultClient() {
return DEFAULT_CLIENT;
}
// 其他工具方法...
}
这个基础版本已经解决了几个关键问题:单例的OkHttpClient实例、合理的默认超时设置、防止工具类被实例化。你可能注意到我用了静态常量而不是每次创建新实例,这是因为OkHttpClient设计上是线程安全的,重复创建反而浪费资源。
GET请求虽然简单,但实际开发中也有很多细节要考虑。比如URL编码、异常处理、响应结果解析等。下面是我封装的一个增强版GET方法:
java复制public static String get(String url, Map<String, String> headers, Map<String, String> params) {
OkHttpClient client = getDefaultClient();
// 构建带参数的URL
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
if (params != null) {
params.forEach((k, v) -> urlBuilder.addEncodedQueryParameter(
URLEncoder.encode(k, StandardCharsets.UTF_8),
URLEncoder.encode(v, StandardCharsets.UTF_8)
));
}
// 构建请求头
Headers.Builder headerBuilder = new Headers.Builder();
if (headers != null) {
headers.forEach(headerBuilder::add);
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.headers(headerBuilder.build())
.get()
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return response.body().string();
} catch (IOException e) {
throw new RuntimeException("HTTP GET请求失败: " + url, e);
}
}
这个方法处理了几个关键点:
现在REST API大多使用JSON作为数据交换格式。封装一个健壮的JSON POST方法能省去很多重复工作:
java复制public static String postJson(String url, Object requestBody, Map<String, String> headers) {
OkHttpClient client = getDefaultClient();
String json = new Gson().toJson(requestBody);
RequestBody body = RequestBody.create(
json,
MediaType.parse("application/json; charset=utf-8")
);
Headers.Builder headerBuilder = new Headers.Builder();
if (headers != null) {
headers.forEach(headerBuilder::add);
}
Request request = new Request.Builder()
.url(url)
.headers(headerBuilder.build())
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return response.body().string();
} catch (IOException e) {
throw new RuntimeException("HTTP POST请求失败: " + url, e);
}
}
这个方法使用了Gson来处理JSON序列化,你也可以替换成Jackson或其他JSON库。我特意将requestBody参数设为Object类型,这样调用时可以直接传入POJO对象,工具类内部负责序列化。
处理表单提交和文件上传是另一个常见需求。OkHttp的MultipartBody可以很好地支持这种场景:
java复制public static String uploadFile(
String url,
File file,
String fileFieldName,
Map<String, String> formFields,
Map<String, String> headers
) {
OkHttpClient client = getDefaultClient();
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
// 添加文件部分
bodyBuilder.addFormDataPart(
fileFieldName,
file.getName(),
RequestBody.create(file, MediaType.parse("application/octet-stream"))
);
// 添加其他表单字段
if (formFields != null) {
formFields.forEach(bodyBuilder::addFormDataPart);
}
// 构建请求头
Headers.Builder headerBuilder = new Headers.Builder();
if (headers != null) {
headers.forEach(headerBuilder::add);
}
Request request = new Request.Builder()
.url(url)
.headers(headerBuilder.build())
.post(bodyBuilder.build())
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
return response.body().string();
} catch (IOException e) {
throw new RuntimeException("文件上传失败: " + url, e);
}
}
这个文件上传方法支持同时上传文件和其他表单字段,非常适用于用户上传头像并附带描述信息的场景。我在一个电商项目中就用这个方法来处理商品图片上传,配合七牛云等OSS服务使用效果很好。
OkHttp默认已经实现了连接池,但我们可以根据项目需求进行优化:
java复制public static OkHttpClient createOptimizedClient() {
return new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(
20, // 最大空闲连接数
5, // 保持时间(分钟)
TimeUnit.MINUTES
))
.retryOnConnectionFailure(true)
.build();
}
这个配置适合大多数中等规模的Web应用:
对于高并发系统,你可能需要增加连接池大小;而对于内部微服务调用,可以适当减少超时时间。
拦截器是OkHttp非常强大的功能,可以用来实现统一认证、日志记录等功能:
java复制public class AuthInterceptor implements Interceptor {
private final String apiKey;
public AuthInterceptor(String apiKey) {
this.apiKey = apiKey;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + apiKey)
.header("X-Request-ID", UUID.randomUUID().toString())
.build();
return chain.proceed(newRequest);
}
}
// 使用方式
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new AuthInterceptor("your-api-key"))
.addInterceptor(new HttpLoggingInterceptor())
.build();
这个拦截器做了两件事:
我特别推荐添加请求ID,这在排查分布式系统问题时非常有用。你可以在日志、数据库等各处记录这个ID,方便追踪完整的请求链路。
虽然前面的例子都是同步调用,但在UI应用中我们更常用异步方式:
java复制public static void getAsync(
String url,
Map<String, String> headers,
Consumer<String> onSuccess,
Consumer<Exception> onError
) {
OkHttpClient client = getDefaultClient();
Headers.Builder headerBuilder = new Headers.Builder();
if (headers != null) {
headers.forEach(headerBuilder::add);
}
Request request = new Request.Builder()
.url(url)
.headers(headerBuilder.build())
.get()
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
onError.accept(e);
}
@Override
public void onResponse(Call call, Response response) {
try {
if (!response.isSuccessful()) {
throw new IOException("Unexpected code " + response);
}
onSuccess.accept(response.body().string());
} catch (Exception e) {
onError.accept(e);
}
}
});
}
这个异步版本使用了Java 8的Consumer接口来处理成功和失败回调。在Android开发中,你可能会用Handler或RxJava来将回调切回主线程。
OkHttp的Call对象持有对Response的引用,如果不及时关闭可能会导致内存泄漏。我遇到过最典型的问题是下载大文件时忘记关闭ResponseBody:
java复制// 错误示例 - 会导致内存泄漏
Response response = client.newCall(request).execute();
byte[] data = response.body().bytes(); // 读取全部内容到内存
// 正确做法
try (Response response = client.newCall(request).execute()) {
try (ResponseBody body = response.body()) {
InputStream in = body.byteStream();
// 使用流式处理
}
}
对于大文件下载,一定要使用流式处理,避免一次性读取全部内容到内存。我在一个项目中就因为这个疏忽导致服务频繁OOM,后来通过接入LeakCanary才定位到问题。
网络不稳定的环境下,合理的重试策略很重要。OkHttp默认会重试失败的连接,但对于某些业务场景可能需要自定义:
java复制public class RetryInterceptor implements Interceptor {
private final int maxRetries;
public RetryInterceptor(int maxRetries) {
this.maxRetries = maxRetries;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException exception = null;
for (int i = 0; i <= maxRetries; i++) {
try {
response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
} catch (IOException e) {
exception = e;
}
if (i < maxRetries) {
try {
Thread.sleep(1000 * (i + 1)); // 指数退避
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
break;
}
}
}
if (response != null) {
throw new IOException("HTTP " + response.code());
}
throw exception != null ? exception : new IOException("Unknown error");
}
}
这个拦截器实现了带指数退避的重试机制。对于支付等关键业务,我通常会设置3次重试;而对于普通的查询接口,可能1次重试就够了。
合理使用缓存可以显著提升性能,特别是对于静态资源:
java复制public static OkHttpClient createCachedClient(File cacheDirectory) {
int cacheSize = 10 * 1024 * 1024; // 10MB
Cache cache = new Cache(cacheDirectory, cacheSize);
return new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(new CacheControlInterceptor())
.build();
}
public class CacheControlInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
// 缓存策略:公共缓存,最大有效期1小时
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(1, TimeUnit.HOURS)
.build();
return response.newBuilder()
.header("Cache-Control", cacheControl.toString())
.build();
}
}
这个配置适合缓存不经常变化的API响应。注意要根据业务需求调整缓存策略,比如对于实时性要求高的数据应该禁用缓存。