OkHttp

131 阅读10分钟

一,介绍

OkHttp 是一款广泛使用的开源 HTTP 客户端库,由 Square 公司开发,专为 Android 和 Java 应用设计。它以高效、灵活和功能强大著称,支持同步和异步请求,并提供了丰富的功能来简化网络通信。

OkHttp 的核心优势

  1. 高性能
    • 内置连接池,减少延迟和重复连接开销。
    • 支持 HTTP/2 和 WebSocket,提升传输效率。
    • 自动 GZIP 压缩,减少数据传输量。
  2. 简洁易用的 API
    • 链式调用设计,代码可读性强。
    • 支持同步和异步请求,灵活适配不同场景。
  3. 强大的扩展性
    • 拦截器(Interceptor):可自定义请求/响应处理逻辑(如日志、重试、缓存)。
    • 支持 Cookie 持久化、缓存控制、HTTPS 安全配置等。
  4. 广泛的应用场景
    • 适用于 Android 应用、Java 后端服务等。
    • 是 Retrofit 等流行库的底层实现。

二,快速入门

  1. 添加依赖
 ```xml
 <dependency>
     <groupId>com.squareup.okhttp3</groupId>
     <artifactId>okhttp</artifactId>
     <version>4.12.0</version>
 </dependency>
 ```

2. 发送get请求

 ```java
 OkHttpClient client = new OkHttpClient();
 
 // 构建 GET 请求
 Request request = new Request.Builder()
         .url("https://api.example.com/data/1")  // 示例 URL
         .get()  // 显式声明 GET 方法(可省略,默认就是 GET)
         .build();
 
 try (Response response = client.newCall(request).execute()) {
     System.out.println("Response Code: " + response.code());
     System.out.println("Response Body: " + response.body().string());
 }

3. 发送post请求

 ```java
 OkHttpClient client = new OkHttpClient();
 
 // 构建 JSON 请求体
 String json = "{\"name\":\"John\", \"age\":30}";
 RequestBody body = RequestBody.create(json, MediaType.parse("application/json"));
 
 // 构建 POST 请求
 Request request = new Request.Builder()
         .url("https://api.example.com/data")
         .post(body)
         .build();
 
 try (Response response = client.newCall(request).execute()) {
     System.out.println("Response Code: " + response.code());
     System.out.println("Response Body: " + response.body().string());
 }

4. 发送put请求

 ```java
 OkHttpClient client = new OkHttpClient();
 
 // 构建表单请求体(或使用 JSON)
 RequestBody body = new FormBody.Builder()
         .add("id", "1")
         .add("status", "updated")
         .build();
 
 // 构建 PUT 请求
 Request request = new Request.Builder()
         .url("https://api.example.com/data/1")
         .put(body)
         .build();
 
 try (Response response = client.newCall(request).execute()) {
     System.out.println("Response Code: " + response.code());
     System.out.println("Response Body: " + response.body().string());
 }

5. 发送delete请求

 ```java
 OkHttpClient client = new OkHttpClient();
 
 // 构建 DELETE 请求(默认不带请求体)
 Request request = new Request.Builder()
         .url("https://api.example.com/data/1")
         .delete()
         .build();
 
 
 try (Response response = client.newCall(request).execute()) {
     System.out.println("Response Code: " + response.code());
     System.out.println("Response Body: " + response.body().string());
 }
 ```

三,扩展功能

3.1 添加请求头

    @Test
    public void testAddHeader(){
        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
                .url("https://api.github.com/users/octocat")
                .addHeader("Authorization", "Bearer YOUR_ACCESS_TOKEN")
                .addHeader("User-Agent", "YourAppName")
                .build();
    }

3.2 设置请求超时

    @Test
    public void testAddTimeout(){
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)  // 连接超时
                .writeTimeout(10, TimeUnit.SECONDS)    // 写超时
                .readTimeout(30, TimeUnit.SECONDS)     // 读超时
                .build();
    }

3.3 添加拦截器

拦截器可以用来修改请求和响应,或执行其他操作,比如日志记录或认证:

    @Test
    public void testAddInterceptor(){
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        Request request = chain.request();
                        // 打印请求信息
                        // 在请求之前添加日志
                        System.out.println("Sending request to " + request.url());
                        Response response = chain.proceed(request);
                        // 打印响应信息
                        // 在响应之后添加日志
                        System.out.println("Received response from " + response.request().url());
                        return response;
                    }
                })
                .build();
    }

3.4 使用Cookie

默认情况下,OkHttp 会自动处理 Cookie。当你使用 OkHttpClient 进行请求时,OkHttp 会处理从服务器响应中接收到的 Cookie,并在后续的请求中自动发送这些 Cookie。这是通过 CookieJar 来实现的

如果需要自定义 Cookie 处理逻辑,可以实现 CookieJar 接口。CookieJar 接口有两个主要方法:

  • saveFromResponse(HttpUrl url, List<Cookie> cookies):保存从服务器响应中获得的 Cookies。
  • loadForRequest(HttpUrl url):加载与请求 URL 相关的 Cookies
    @Test
    public void testCustomCookie(){
        // 创建一个存储 Cookie 的集合
        Set<Cookie> cookieStore = new CopyOnWriteArraySet<>();

        // 创建自定义的 CookieJar
        CookieJar cookieJar = new CookieJar() {
        	@Override
        	public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            	cookieStore.addAll(cookies); // 保存 Cookie
        	}

        	@Override
        	public List<Cookie> loadForRequest(HttpUrl url) {
            	return new ArrayList<>(cookieStore); // 加载 Cookie
        	}
    	};

    // 创建 OkHttpClient 并设置自定义的 CookieJar
    OkHttpClient client = new OkHttpClient.Builder()
            .cookieJar(cookieJar)
            .build();
        
}

3.5 处理缓存

OkHttp 支持 HTTP 缓存来减少网络流量和提高性能:要启用缓存,需要创建一个 Cache 对象,并将其配置到 OkHttpClient 中。Cache 对象用于存储和管理 HTTP 响应的缓存数据。

    @Test
    public void testUseCache(){
        // 创建缓存目录和缓存大小
        File cacheDirectory = new File("cache_directory");
        Cache cache = new Cache(cacheDirectory, 10 * 1024 * 1024); // 10 MB

        // 创建 OkHttpClient 并设置缓存
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(cache)
                .build();
    }

也可以配合设置缓存类型的header达到控制缓存,以下是缓存常用header

  • Cache-Control: max-age=<seconds>:指定资源可以在客户端缓存的最大时间。
  • Cache-Control: no-cache:指示客户端每次请求都要重新验证资源。
  • Cache-Control: no-store:指示客户端和服务器都不应缓存该资源。
  • Expires: <date>:指定资源的过期时间。

3.6 使用Multipart请求

使用 Multipart 请求通常用于上传文件和发送表单数据Multipart 请求允许你将文件和其他数据组合成一个 HTTP 请求,并发送到服务器。

Multipart 请求的基本概念

Multipart 请求通常使用 multipart/form-data 编码类型。这种请求方式允许你将多个部分(每部分可以是文件、文本或其他类型的数据)组合成一个请求体。每个部分都有自己的头部,描述该部分的数据类型和其他元数据。

创建 Multipart 请求

在 OkHttp 中,你可以使用 MultipartBody 类来构造 Multipart 请求体。MultipartBody 提供了 Builder 类,可以帮助你构建包含多个部分的请求体。

创建和发送一个简单 Multipart 请求的步骤:

  1. 创建 MultipartBody.Builder:用于构建 MultipartBody 对象。
  2. 添加表单字段:使用 addFormDataPart 方法添加表单字段。
  3. 添加文件字段:使用 addFormDataPart 方法添加文件字段。
  4. 构建 RequestBody:将 MultipartBody 构建成 RequestBody
  5. 构建请求:创建一个 Request 对象,将 RequestBody 设置为请求体。
  6. 发送请求:使用 OkHttpClient 发送请求。
public class MultipartRequestExample {
    public static void main(String[] args) {
        // 创建 OkHttpClient 实例
        OkHttpClient client = new OkHttpClient();

        // 创建 MultipartBody.Builder
        MultipartBody.Builder builder = new MultipartBody.Builder()
            .setType(MultipartBody.FORM) // 设置请求类型为 form-data
            .addFormDataPart("username", "example_user") // 添加表单字段
            .addFormDataPart("password", "example_password");

        // 添加文件字段
        File file = new File("path/to/your/file.txt");
        RequestBody fileBody = RequestBody.create(file, MediaType.parse("text/plain"));
        builder.addFormDataPart("file", file.getName(), fileBody);

        // 创建 RequestBody
        RequestBody requestBody = builder.build();

        // 创建 Request
        Request request = new Request.Builder()
            .url("https://your-api-endpoint.com/upload")
            .post(requestBody) // 使用 POST 方法发送请求
            .build();

        // 发送请求并处理响应
        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected code " + response);
            }

            // 打印响应体
            System.out.println(response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.7 配置连接池

在 OkHttp 中,连接池是通过 ConnectionPool 类来管理的。你可以创建一个自定义的 ConnectionPool 实例,并将其配置到 OkHttpClient 中。

    // 创建自定义连接池
    ConnectionPool connectionPool = new ConnectionPool(
            5, // 最大空闲连接数
            5, // 连接保持时间
            TimeUnit.MINUTES // 时间单位
    );
    @Test
    public void testConnectionPool(){
        // 创建 OkHttpClient 并设置自定义的连接池
        OkHttpClient client = new OkHttpClient.Builder()
                .connectionPool(connectionPool)
                .build();
    }

3.8 失败重连策略

要在 OkHttp 中实现失败重连策略,你可以通过自定义 Interceptor 实现。拦截器允许你在请求和响应的过程中执行自定义操作,包括重试逻辑。

import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

import java.io.IOException;

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;
        int tryCount = 0;
        
        while (tryCount < maxRetries) {
            try {
                response = chain.proceed(request);
                if (response.isSuccessful()) {
                    return response;
                }
            } catch (IOException e) {
                // 如果是网络异常,则进行重试
                if (tryCount >= maxRetries - 1) {
                    throw e; // 达到最大重试次数,抛出异常
                }
            }
            tryCount++;
        }
        throw new IOException("Failed after " + maxRetries + " retries");
    }

    public static void main(String[] args) {
        // 创建 OkHttpClient 并设置重试拦截器
        OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new RetryInterceptor(3)) // 最大重试次数为 3
            .build();

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

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Unexpected code " + response);
            }

            // 打印响应体
            System.out.println(response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

不同的应用场景可能需要不同的重试策略。以下是一些常见的重试策略:

  • 固定间隔重试:在每次重试之间使用固定的时间间隔。例如,每次失败后等待 1 秒再重试:
  • 指数退避重试:每次重试之间的等待时间逐渐增加。例如,第一次重试等待 1 秒,第二次等待 2 秒,第三次等待 4 秒等:
  • 只在特定异常情况下重试:可以根据异常类型或 HTTP 状态码决定是否重试。例如,只在网络异常或服务器错误(5xx 状态码)时重试:

3.9 异步请求

OkHttp 通过 enqueue(Callback) 方法实现异步请求,所有网络操作在后台线程池中执行,结果通过回调接口返回。

3.9.1 基础异步请求示例

import okhttp3.*;

public class AsyncExample {
    public static void main(String[] args) {
        OkHttpClient client = new OkHttpClient();

        // 构建请求(以 GET 为例)
        Request request = new Request.Builder()
                .url("https://api.example.com/data")
                .build();

        // 异步发送请求
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                // 注意:回调在 OkHttp 的线程池中执行,非主线程!
                try {
                    if (response.isSuccessful()) {
                        String result = response.body().string();
                        System.out.println("请求成功: " + result);
                    } else {
                        System.err.println("请求失败,状态码: " + response.code());
                    }
                } finally {
                    response.close(); // 手动关闭响应资源
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                // 网络错误、超时等异常
                System.err.println("请求失败: " + e.getMessage());
            }
        });

        // 主线程继续执行其他逻辑(非阻塞)
        System.out.println("已发起异步请求,主线程继续运行...");
    }
}

3.9.2 关键注意事项

  1. 回调线程特性

    • 回调在 OkHttp 的线程池中执行,与主线程(Java 应用的启动线程)是分离的。

    • 如果需要在回调中操作共享数据(如全局变量),需使用线程安全机制(如 synchronizedLock)。

  2. 资源释放

    • 异步请求的 Response 不会自动关闭,必须手动调用 response.close(),否则会导致资源泄漏。

3. 取消请求

  • 通过 Call.cancel() 终止请求(例如,用户取消操作或超时管理):

    Call call = client.newCall(request);
    call.enqueue(new Callback() { ... });
    
    // 在需要时取消请求
    call.cancel();
    

3.9.3 自定义线程池

import okhttp3.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        // 创建自定义线程池(例如固定大小为 5)
        ExecutorService executor = Executors.newFixedThreadPool(5);

        // 使用自定义线程池构建 OkHttpClient
        OkHttpClient client = new OkHttpClient.Builder()
                .dispatcher(new Dispatcher(executor))
                .build();

        // 发送异步请求(使用自定义线程池)
        Request request = new Request.Builder()
                .url("https://api.example.com/data")
                .build();
        
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                try {
                    System.out.println("请求成功: " + response.body().string());
                } finally {
                    response.close();
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                System.err.println("请求失败: " + e.getMessage());
            }
        });
    }
}

3.9.4 在异步请求中处理返回结果

方案1:使用 CompletableFuture 包装异步结果

import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class AsyncResultHandler {

    private static final OkHttpClient client = new OkHttpClient();

    /**
     * 发起异步 GET 请求,返回 CompletableFuture 包装的结果
     * @param url 请求地址
     * @return CompletableFuture<String> 异步结果
     */
    public static CompletableFuture<String> asyncGet(String url) {
        CompletableFuture<String> future = new CompletableFuture<>();

        Request request = new Request.Builder().url(url).build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                try (ResponseBody body = response.body()) {
                    if (response.isSuccessful()) {
                        // 将结果传递给 future
                        future.complete(body.string());
                    } else {
                        // 传递异常
                        future.completeExceptionally(new IOException("HTTP 错误码: " + response.code()));
                    }
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                // 传递网络错误
                future.completeExceptionally(e);
            }
        });

        return future;
    }

    public static void main(String[] args) {
        try {
            // 发起异步请求并获取 CompletableFuture
            CompletableFuture<String> future = asyncGet("https://api.example.com/data");

            // 设置超时时间(可选)
            String result = future.get(5, TimeUnit.SECONDS); // 阻塞等待结果(最多 5 秒)

            // 处理成功结果
            System.out.println("异步请求结果: " + result);
            
        } catch (InterruptedException e) {
            System.err.println("请求被中断: " + e.getMessage());
        } catch (ExecutionException e) {
            System.err.println("请求执行异常: " + e.getCause().getMessage());
        } catch (TimeoutException e) {
            System.err.println("请求超时: " + e.getMessage());
        }
    }
}

方案2:自定义回调接口

import okhttp3.*;
import java.io.IOException;

public class CallbackHandlerExample {

    // 自定义回调接口
    public interface ResultCallback {
        void onSuccess(String result);
        void onFailure(Throwable error);
    }

    private static final OkHttpClient client = new OkHttpClient();

    /**
     * 发起异步 GET 请求,通过回调返回结果
     * @param url 请求地址
     * @param callback 自定义回调接口
     */
    public static void asyncGetWithCallback(String url, ResultCallback callback) {
        Request request = new Request.Builder().url(url).build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                try (ResponseBody body = response.body()) {
                    if (response.isSuccessful()) {
                        callback.onSuccess(body.string());
                    } else {
                        callback.onFailure(new IOException("HTTP 错误码: " + response.code()));
                    }
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                callback.onFailure(e);
            }
        });
    }

    public static void main(String[] args) {
        // 调用异步请求并处理结果
        asyncGetWithCallback("https://api.example.com/data", new ResultCallback() {
            @Override
            public void onSuccess(String result) {
                System.out.println("异步请求成功: " + result);
                // 可以在这里触发后续业务逻辑
            }

            @Override
            public void onFailure(Throwable error) {
                System.err.println("异步请求失败: " + error.getMessage());
                // 可以在这里处理错误(如重试、记录日志等)
            }
        });

        // 主线程继续执行其他任务(非阻塞)
        System.out.println("主线程继续运行...");

        // 防止程序提前退出(模拟长时间运行的应用)
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

方案3:处理多个异步请求

import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.List;
import java.util.ArrayList;

public class MultipleAsyncRequests {

    private static final OkHttpClient client = new OkHttpClient();

    /**
     * 并行发起多个异步请求,合并结果
     */
    public static void main(String[] args) {
        List<String> urls = List.of(
            "https://api.example.com/data/1",
            "https://api.example.com/data/2",
            "https://api.example.com/data/3"
        );

        List<CompletableFuture<String>> futures = new ArrayList<>();

        // 发起所有异步请求
        for (String url : urls) {
            CompletableFuture<String> future = new CompletableFuture<>();
            Request request = new Request.Builder().url(url).build();

            client.newCall(request).enqueue(new Callback() {
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    try (ResponseBody body = response.body()) {
                        if (response.isSuccessful()) {
                            future.complete(body.string());
                        } else {
                            future.completeExceptionally(new IOException("HTTP 错误码: " + response.code()));
                        }
                    }
                }

                @Override
                public void onFailure(Call call, IOException e) {
                    future.completeExceptionally(e);
                }
            });

            futures.add(future);
        }

        // 等待所有请求完成
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        );

        // 处理最终结果
        allFutures.thenRun(() -> {
            for (int i = 0; i < futures.size(); i++) {
                CompletableFuture<String> future = futures.get(i);
                try {
                    String result = future.get();
                    System.out.println("请求 " + urls.get(i) + " 结果: " + result);
                } catch (InterruptedException | ExecutionException e) {
                    System.err.println("请求 " + urls.get(i) + " 失败: " + e.getCause().getMessage());
                }
            }
        }).join(); // 阻塞主线程等待所有结果(仅示例,生产环境应避免)
    }
}