OkHttp使用不当引发的一场灾祸

OkHttp

OkHttp这里不再多讲,众所周知是一个高效的HTTP客户端库,和Spring的RestTemplate都是用于发送HTTP请求的客户端库。

一般大量在Android应用中使用,以其简洁的API、高效的性能和强大的功能而受到广泛欢迎。

这里简单说下优于RestTemplate的几个核心点,建议以后在我们的java应用中也大量使用起来。

  • 连接池和复用 :OkHttp内置了连接池和连接复用机制,这可以显著提高性能,特别是在大量短连接请求的场景下。连接池允许多个请求共享连接,减少连接建立的开销

  • HTTP/2 支持:OkHttp原生支持HTTP/2协议,这可以进一步提升性能,特别是在多请求场景下,HTTP/2允许多个请求共享一个TCP连接,减少了延迟和网络开销。

  • 缓存支持:OkHttp内置了响应缓存机制,可以自动缓存HTTP响应,减少不必要的网络请求,提高效率。

  • 异步请求处理:OkHttp提供了强大的异步请求支持,可以轻松实现非阻塞的HTTP请求,而RestTemplate主要是同步的。这对于需要并行处理大量HTTP请求的应用程序来说,OkHttp更具优势。

好,对于还不熟悉的伙伴可以看一个简单的demo

java 复制代码
OkHttpClient client = new OkHttpClient();

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

//异步获取响应结果
client.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        //错误情况
        e.printStackTrace();
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
        // 成功情况
        System.out.println(response.body().string());
    }
});

那么这看似简单的东西,怎么在生产环境中出现问题了呢。

刚才我们讲OkHttp提供了强大的异步请求支持,可以轻松实现非阻塞的HTTP请求,但是我们又想同步获取响应数据怎么办?

方法就有很多,比如Countdownlatch、CompletableFuture等,都能在异步线程中做到一个同步的效果。两者兼得。

我们用的是 Semaphore 一个用于控制多个线程访问共享资源的同步工具。

看下面事故代码,大家可否看出其中问题

事故代码

核心代码样例:

java 复制代码
public class OkHttpUtil {

    private static volatile OkHttpClient okHttpClient = null;
    private static volatile Semaphore semaphore = null;
    private Map<String, String> headerMap;
    private Map<String, String> paramMap;
    private String param;
    private String url;
    private Request.Builder request;

    //内部类构造http请求 设置参数
    private OkHttpUtil() {
        if (Objects.isNull(okHttpClient)) {
            synchronized (OkHttpUtil.class) {
                if (Objects.isNull(okHttpClient)) {
                    TrustManager[] trustManagers = buildTrustManager();
                    // 设置连接时长,读取超时时间,以及其他必要参数设置
                    okHttpClient = new OkHttpClient.Builder()
                            .connectTimeout(15, TimeUnit.SECONDS)
                            .writeTimeout(20, TimeUnit.SECONDS)
                            .readTimeout(20, TimeUnit.SECONDS)
                            .sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0])
                            .hostnameVerifier((hostname, session) -> true)
                            .retryOnConnectionFailure(true)
                            .build();
                    addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36");
                }
            }
        }
    }
   
    // 发起请求
    public OkHttpUtil initPost() {
        RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), !param.equals("") ? param : "");
        request = new Request.Builder().post(requestBody).url(url);
        return this;
    }

    /**
     * @Description:同步请求
     * @Author: yn
     * @Date: 2023-01-04 18:06
     * @return: java.lang.String
     **/
    public String sync() {
        setHeader(request);
        try {
            Response result = okHttpClient.newCall(request.build()).execute();
            if (Objects.nonNull(result)) {
                return result.body().string();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "请求失败";
    }

    /**
     * @Description:异步请求,有返回值
     * @Author: yn
     * @Date: 2023-01-04 18:05
     * @return: java.lang.String
     **/
    public String async() {
        StringBuffer buffer = new StringBuffer();
        setHeader(request);
        okHttpClient.newCall(request.build()).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                buffer.append("请求出错").append(e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (Objects.nonNull(response.body())) {
                    buffer.append(response.body().string());
                    getSemaphore().release();
                }
            }
        });
        try {
            getSemaphore().acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return buffer.toString();
    }

    // 信号量,控制多个线程访问共享资源的同步工具
    private static Semaphore getSemaphore() {
        synchronized (OkHttpUtil.class) {
            if (Objects.isNull(semaphore)) {
                semaphore = new Semaphore(0);
            }
        }
        return semaphore;
    }

    //下面这些是用builder模式构建对象,使用更简单便捷
    public static OkHttpUtil builder() {
        return new OkHttpUtil();
    }
    public OkHttpUtil url(String url) {
        this.url = url;
        return this;
    }
    public OkHttpUtil addParam(String json) {
        if (!json.equals("")) {
            param = json;
        }
        return this;
    }
    public OkHttpUtil addHeader(String key, String value) {
        if (Objects.isNull(headerMap)) {
            headerMap = new LinkedHashMap<>(16);
        }
        headerMap.put(key, value);
        return this;
    }
}

看出上面代码问题的大佬可以在下面评论。调用示例👇:

java 复制代码
@RequestMapping("/test")
public ResponseBO test(){
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("userId","123456");
        String result = OkHttpUtil.builder()
                .url("https://api.example.com/data")
                .addParam(jsonObject.toJSONString())
                .initPost()
                .async();
        ResponseBO responseBO = JSON.parseObject(result,ResponseBO.class);
        if (!responseBO.isResponseOk()){
            return ResponseBO.responseFail("失败");
        }
        return ResponseBO.responseOK();        
}

问题排查过程

首先现象是调用某接口一直无响应,并且服务运行一段时间 会自己死掉,重启之后还是这个循环,并且看不出任何的堆栈错误日志。

对于此类现象首先可以想到应该是资源被占用。排查发现由于三方接口调用不通,OkHttp请求一直处于等待状态,没有收到响应,可能会长时间挂起,导致资源占用,最终拖垮该服务

可是对于简单的一个请求,不应该存在问题。可是发现我们用了一个Semaphore

java 复制代码
public String async() {
    StringBuffer buffer = new StringBuffer();
    setHeader(request);
    okHttpClient.newCall(request.build()).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            buffer.append("请求出错").append(e.getMessage());
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            if (Objects.nonNull(response.body())) {
                buffer.append(response.body().string());
            }
            getSemaphore().release();
        }
    });
    try {
        //这是关键
        getSemaphore().acquire();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return buffer.toString();
}

Semaphore

Semaphore 是一个用于控制多个线程访问共享资源的同步工具。它维护了一个许可计数,线程可以通过 acquire() 方法请求一个许可,和通过 release() 方法释放一个许可。许可计数允许线程并发访问资源的最大数量。

java 复制代码
private static Semaphore getSemaphore() {
    synchronized (OkHttpUtil.class) {
        if (Objects.isNull(semaphore)) {
            semaphore = new Semaphore(0);
        }
    }
    return semaphore;
}

Semaphore 在代码中的作用

在异步请求处理中,你使用 Semaphore 来实现类似同步调用的效果。主要步骤如下:

  1. 初始化 Semaphore:初始计数为 0。
  2. 发送异步请求:使用 OkHttp 发送异步请求。
  3. 等待许可 :主线程调用 semaphore.acquire(),因为许可计数为 0,主线程会阻塞,等待异步请求完成。
  4. 异步请求回调 :当异步请求完成时,无论成功还是失败,回调方法 (onResponseonFailure) 中都会调用 semaphore.release(),将许可计数增加 1。
  5. 释放阻塞 :主线程收到许可后,acquire() 方法返回,async() 方法继续执行并返回结果。

为什么要释放 Semaphore?

Semaphore 需要在异步请求完成后释放,以确保主线程能够继续执行。如果在回调中不调用 release(),主线程会一直阻塞在 acquire() 方法处,导致 async() 方法永远不会返回。这会造成应用程序的死锁或长时间的阻塞。

所以最终问题是在发生异常的情况下,并没有release() 释放

java 复制代码
okHttpClient.newCall(request.build()).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                buffer.append("请求出错").append(e.getMessage());
                
                //应该在异常情况下同样释放锁
                getSemaphore().release();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (Objects.nonNull(response.body())) {
                    buffer.append(response.body().string());
                    getSemaphore().release();
                }
            }
        });

结论与优化

虽然最后发现问题不大,由OkHttp引起,最终是 OkHttp+Semaphore 结合使用问题。

我们可以在此基础上再进行下优化,假如不用Semaphore 也照样可以实现在异步线程中做到一个同步的效果。 刚才也举例了,可以用CompletableFuture 实现

java 复制代码
public void async(BiConsumer<Call, String> onSuccess, BiConsumer<Call, String> onFailure) {
    setHeader(request);
    okHttpClient.newCall(request.build()).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            onFailure.accept(call, e.getMessage());
        }
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            try (ResponseBody responseBody = response.body()) {
                if (Objects.nonNull(responseBody)) {
                    onSuccess.accept(call, responseBody.string());
                }
            }
        }
    });
}

BiConsumer 函数做回调接收。

java 复制代码
@RequestMapping("/test1")
public ResponseBO test1(){

    CompletableFuture<ResponseBO> future = new CompletableFuture<>();
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("userId","123456"); 
    OkHttpUtil.builder().url("https://api.example.com/data").addParam(jsonObject.toJSONString()).initPost()
    .async((call, response) -> {
        // 处理成功响应
        logger.info("数据推送成功,url -> {},req -> {},res -> {}", "127.0.0.1" , jsonObject.toJSONString(), response);
        future.complete(ResponseBO.responseOK());
    }, (call, error) -> {
        // 处理失败响应
        logger.info("数据推送失败,url -> {},req -> {},res -> {}", "127.0.0.1", jsonObject.toJSONString(), error);
        future.complete(ResponseBO.responseFail("数据处理失败"));
    });

    try {
        return future.join();
    } catch (Exception e) {
        e.getMessage();
        // 处理异常情况
        return ResponseBO.responseFail("数据推送失败");
    }
}
相关推荐
极客先躯5 分钟前
说说高级java每日一道面试题-2025年2月13日-数据库篇-请说说 MySQL 数据库的锁 ?
java·数据库·mysql·数据库的锁·模式分·粒度分·属性分
程序员侠客行7 分钟前
Spring事务原理 二
java·后端·spring
小猫猫猫◍˃ᵕ˂◍22 分钟前
备忘录模式:快速恢复原始数据
android·java·备忘录模式
liuyuzhongcc31 分钟前
List 接口中的 sort 和 forEach 方法
java·数据结构·python·list
五月茶35 分钟前
Spring MVC
java·spring·mvc
sjsjsbbsbsn44 分钟前
Spring Boot定时任务原理
java·spring boot·后端
yqcoder1 小时前
Express + MongoDB 实现在筛选时间段中用户名的模糊查询
java·前端·javascript
菜鸟蹦迪1 小时前
八股文实战之JUC:ArrayList不安全性
java
2501_903238651 小时前
Spring MVC配置与自定义的深度解析
java·spring·mvc·个人开发
逻各斯1 小时前
redis中的Lua脚本,redis的事务机制
java·redis·lua