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
来实现类似同步调用的效果。主要步骤如下:
- 初始化
Semaphore
:初始计数为 0。 - 发送异步请求:使用 OkHttp 发送异步请求。
- 等待许可 :主线程调用
semaphore.acquire()
,因为许可计数为 0,主线程会阻塞,等待异步请求完成。 - 异步请求回调 :当异步请求完成时,无论成功还是失败,回调方法 (
onResponse
或onFailure
) 中都会调用semaphore.release()
,将许可计数增加 1。 - 释放阻塞 :主线程收到许可后,
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("数据推送失败");
}
}