Java扩展——OkHttp

OkHttp

OkHttp 是由 Square 公司开源的高性能 HTTP & HTTP/2 客户端,也是目前 Java / Android 生态中最主流、事实上的 HTTP 网络库标准(Android 官方从 Android 4.4 起底层已将 HttpURLConnection 替换为 OkHttp 实现)。

1、核心特性与架构设计

  • HTTP/2 与连接池支持:共享同一个 Socket 连接,大幅降低延迟与 TCP 握手开销。
  • 透明的 GZIP 压缩:自动压缩请求体并解压响应体,节省网络流量。
  • 响应缓存:支持 HTTP 响应头的本地缓存,避免重复的网络请求。
  • 拦截器链(Interceptor Chain):极其强大的责任链设计模式,允许轻松插拔日志记录、统一 Token 注入、失败重试、自动重定向等功能。
  • 基于 Okio 的高性能 IO:底层结合 Square 的 Okio 库,在缓冲管理和内存占用上性能顶尖。

2、核心 API 分类列表全景图

核心类 (Class) 角色与功能说明
OkHttpClient 全局单例配置中心。管理连接池、超时时间、拦截器、缓存等。通常全局共享一个实例。
Request HTTP 请求描述。使用 Builder 模式构建,包含 URL、HTTP 方法、请求头(Headers)、请求体(RequestBody)。
Response HTTP 响应结果。包含状态码(Code)、响应头、响应体(ResponseBody)。注意:响应体流只能读取一次且必须关闭。
Call 请求执行任务。代表一个已准备就绪的 HTTP 请求,可同步(execute())或异步(enqueue())执行,支持取消操作。
RequestBody 请求体抽象。支持传输 JSON、Form 表单、Multipart 多部分文件上传、流式数据等。
ResponseBody 响应体抽象。支持将响应转为 String、byte\[\] 或 InputStream(用于大文件下载)。
Interceptor 拦截器接口。应用拦截器(Application Interceptor)与网络拦截器(Network Interceptor)的核心入口。

3、常用 API 与实战代码示例

依赖引入 (Maven / Gradle)

xml 复制代码
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version> <!-- 推荐使用 4.x/5.x 稳定版 -->
</dependency>

1. 初始化全局 OkHttpClient

最佳实践:OkHttpClient 内部拥有连接池和线程池,千万不要在每次请求时 new OkHttpClient(),必须作为单例复用,或者通过 .newBuilder() 基于现有配置衍生新对象。

java 复制代码
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;

public class OkHttpConfig {
    // 全局单例
    public static final OkHttpClient CLIENT = new OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS) // 连接超时
            .readTimeout(10, TimeUnit.SECONDS)    // 读取超时
            .writeTimeout(10, TimeUnit.SECONDS)   // 写入超时
            .retryOnConnectionFailure(true)       // 失败重试
            .build();
}

2. 同步与异步 GET 请求

① 同步 GET 请求 (execute)

java 复制代码
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public void syncGetDemo() {
    Request request = new Request.Builder()
            .url("https://api.github.com/users/octocat")
            .header("User-Agent", "OkHttp-Demo") // 添加 Header
            .build();

    // try-with-resources 自动关闭 Response / ResponseBody 释放底层 Socket
    try (Response response = OkHttpConfig.CLIENT.newCall(request).execute()) {
        if (!response.isSuccessful()) {
            System.err.println("请求失败, 状态码: " + response.code());
            return;
        }

        ResponseBody body = response.body();
        if (body != null) {
            String jsonResult = body.string(); // 获取字符串格式的响应体
            System.out.println("响应内容: " + jsonResult);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

② 异步 GET 请求 (enqueue)

java 复制代码
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;

public void asyncGetDemo() {
    Request request = new Request.Builder()
            .url("https://api.github.com/users/octocat")
            .build();

    // 放入异步线程池队列中执行,不阻塞当前线程
    OkHttpConfig.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 (ResponseBody body = response.body()) {
                if (response.isSuccessful() && body != null) {
                    System.out.println("异步响应结果: " + body.string());
                }
            }
        }
    });
}

3. POST 请求 (JSON & 表单提交)

① 发送 JSON 数据 (MediaType)

java 复制代码
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public void postJsonDemo() throws Exception {
    MediaType JSON = MediaType.get("application/json; charset=utf-8");
    String jsonString = "{\"name\":\"Alice\", \"age\":25}";

    // 构建 RequestBody
    RequestBody body = RequestBody.create(jsonString, JSON);

    Request request = new Request.Builder()
            .url("https://httpbin.org/post")
            .post(body)
            .build();

    try (Response response = OkHttpConfig.CLIENT.newCall(request).execute()) {
        System.out.println("POST 返回: " + response.body().string());
    }
}

② 提交 Form 表单数据 (FormBody)

java 复制代码
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public void postFormDemo() throws Exception {
    // 模拟常规 HTML 表单提交
    RequestBody formBody = new FormBody.Builder()
            .add("username", "admin")
            .add("password", "123456")
            .build();

    Request request = new Request.Builder()
            .url("https://httpbin.org/post")
            .post(formBody)
            .build();

    try (Response response = OkHttpConfig.CLIENT.newCall(request).execute()) {
        System.out.println("Form 返回: " + response.body().string());
    }
}

4. 文件上传 (MultipartBody) 与 文件下载

① Multipart 多部分文件上传

java 复制代码
import okhttp3.*;
import java.io.File;

public void uploadFileDemo(File uploadFile) throws Exception {
    RequestBody fileBody = RequestBody.create(uploadFile, MediaType.parse("image/png"));

    // 构建包含表单参数和文件对象的 MultipartBody
    RequestBody multipartBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("user", "JohnDoe")
            .addFormDataPart("avatar", uploadFile.getName(), fileBody)
            .build();

    Request request = new Request.Builder()
            .url("https://httpbin.org/post")
            .post(multipartBody)
            .build();

    try (Response response = OkHttpConfig.CLIENT.newCall(request).execute()) {
        System.out.println("文件上传响应: " + response.code());
    }
}

② 文件流下载

java 复制代码
import okhttp3.*;
import java.io.FileOutputStream;

public void downloadFileDemo(String fileUrl, File saveFile) throws Exception {
    Request request = new Request.Builder().url(fileUrl).build();

    try (Response response = OkHttpConfig.CLIENT.newCall(request).execute()) {
        if (!response.isSuccessful()) return;

        // 使用 InputStream 分块写入本地文件,防止大文件导致 OOM
        try (var is = response.body().byteStream();
             var fos = new FileOutputStream(saveFile)) {
            byte[] buffer = new byte[8192];
            int read;
            while ((read = is.read(buffer)) != -1) {
                fos.write(buffer, 0, read);
            }
        }
    }
}

5. 核心高级功能:拦截器(Interceptor)

拦截器是 OkHttp 最强大的拓展机制。常用于统一添加认证 Token、打印日志、修改请求头等。

java 复制代码
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;

// 1. 定义一个用于自动注入 Auth Token 的拦截器
public class AuthInterceptor implements Interceptor {
    private final String token;

    public AuthInterceptor(String token) {
        this.token = token;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();

        // 拦截并修改 Request:追加 Authorization 请求头
        Request authenticatedRequest = originalRequest.newBuilder()
                .header("Authorization", "Bearer " + token)
                .build();

        // 放行请求,交给责任链中的下一个节点执行
        return chain.proceed(authenticatedRequest);
    }
}

// 2. 挂载到 OkHttpClient 示例
OkHttpClient clientWithAuth = OkHttpConfig.CLIENT.newBuilder()
        .addInterceptor(new AuthInterceptor("secret_token_123"))
        .build();

4、避坑指南与最佳实践

  • 务必显式关闭 Response 或 ResponseBody:
    body.string() 或 body.byteStream() 底层持有 Socket 连接流,如果未关闭会导致连接无法复用并发生连接池/句柄泄漏。建议统一使用 try-with-resources 语法。
  • body.string() 只能被调用一次:
    调用 body.string() 后,响应流的内容会被全部读取并从内存缓存中释放。第二次调用会直接抛出 IllegalStateException: closed。如果需要二次读取,需在拦截器层缓存 Buffer。
  • 避免在大文件下载时使用 body.string() 或 body.bytes():
    这会将整个文件一次性全量载入 JVM 堆内存,极易引发 OutOfMemoryError(OOM)。大文件应使用 body.byteStream() 流式边读边写。
相关推荐
卓怡学长1 小时前
w210基于springboot反诈平台设计与实现
java·spring boot·spring·intellij-idea
啊阿狸不会拉杆1 小时前
《计算机网络-自顶向下方法》5.7 网络管理、SNMP和NETCONF/YANG 读书笔记
开发语言·计算机网络·php
长友cy1 小时前
Qt官方示例D-BusListNames阅读
开发语言
萧瑟余晖1 小时前
Java深入解析篇六十一之编译器API(javax.tools)详解
java·开发语言
蜡台1 小时前
Kotlin 零基础完整版实战教程|从语法入门到Android工程实战
android·开发语言·kotlin
jimy12 小时前
C++ 的 “移动语义”——Move Semantics
开发语言·c++
码云数智-园园2 小时前
C++ STL 容器怎么选?vector、list、map、unordered_map 优缺点对比
java·开发语言
en.en..2 小时前
竖线 |:管道、按位或、掩码组合
java·开发语言
techdashen2 小时前
Go Map 详解:键值对实际上是如何存储的
开发语言·后端·golang