This is an automated email from the ASF dual-hosted git repository.
jianbin pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new d456cea081 feature : Upgrade HTTP client in common module to support
HTTP/2 (#7492)
d456cea081 is described below
commit d456cea08128f0457d6dc920d1cb60e4e43bf59d
Author: Yongjun Hong <[email protected]>
AuthorDate: Wed Jul 30 23:37:11 2025 +0900
feature : Upgrade HTTP client in common module to support HTTP/2 (#7492)
---
changes/en-us/2.x.md | 2 +
changes/zh-cn/2.x.md | 2 +
common/pom.xml | 4 +
.../apache/seata/common/executor/HttpCallback.java | 44 ++++
.../apache/seata/common/util/Http5ClientUtil.java | 155 +++++++++++++
.../apache/seata/common/util/HttpClientUtil.java | 1 +
.../seata/common/util/Http5ClientUtilTest.java | 249 +++++++++++++++++++++
7 files changed, 457 insertions(+)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index cd3a82d3ef..b53587bf1c 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -21,6 +21,7 @@ Add changes here for all PR submitted to the 2.x branch.
### feature:
- [[#7485](https://github.com/apache/incubator-seata/pull/7485)] Add http
request filter for seata-server
+- [[#7492](https://github.com/apache/incubator-seata/pull/7492)] upgrade HTTP
client in common module to support HTTP/2
### bugfix:
@@ -63,6 +64,7 @@ Thanks to these contributors for their code commits. Please
report an unintended
- [slievrly](https://github.com/slievrly)
- [YvCeung](https://github.com/YvCeung)
- [xjlgod](https://github.com/xjlgod)
+- [YongGoose](https://github.com/YongGoose)
Also, we receive many valuable issues, questions and advices from our
community. Thanks for you all.
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index e84b0a1148..697dacb4de 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -21,6 +21,7 @@
### feature:
- [[#7485](https://github.com/apache/incubator-seata/pull/7485)]
给seata-server端的http请求添加过滤器
+- [[#7492](https://github.com/apache/incubator-seata/pull/7492)] 升级 common
模块中的 HTTP 客户端以支持 HTTP/2
### bugfix:
@@ -62,6 +63,7 @@
- [slievrly](https://github.com/slievrly)
- [YvCeung](https://github.com/YvCeung)
- [xjlgod](https://github.com/xjlgod)
+- [YongGoose](https://github.com/YongGoose)
同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。
diff --git a/common/pom.xml b/common/pom.xml
index 517aae769a..0db3696a5d 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -52,5 +52,9 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git
a/common/src/main/java/org/apache/seata/common/executor/HttpCallback.java
b/common/src/main/java/org/apache/seata/common/executor/HttpCallback.java
new file mode 100644
index 0000000000..9f53241ac7
--- /dev/null
+++ b/common/src/main/java/org/apache/seata/common/executor/HttpCallback.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.common.executor;
+
+/**
+ * The interface HttpCallback.
+ *
+ * @param <T> the type parameter
+ */
+public interface HttpCallback<T> {
+
+ /**
+ * Called when the HTTP request is successful.
+ *
+ * @param result the result of the HTTP request
+ */
+ void onSuccess(T result);
+
+ /**
+ * Called when the HTTP request fails.
+ *
+ * @param e the exception that occurred during the HTTP request
+ */
+ void onFailure(Throwable e);
+
+ /**
+ * Called when the HTTP request is cancelled.
+ */
+ void onCancelled();
+}
diff --git
a/common/src/main/java/org/apache/seata/common/util/Http5ClientUtil.java
b/common/src/main/java/org/apache/seata/common/util/Http5ClientUtil.java
new file mode 100644
index 0000000000..bcf95a00e5
--- /dev/null
+++ b/common/src/main/java/org/apache/seata/common/util/Http5ClientUtil.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.common.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.FormBody;
+import okhttp3.Headers;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.apache.seata.common.executor.HttpCallback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class Http5ClientUtil {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(Http5ClientUtil.class);
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(10, TimeUnit.SECONDS)
+ .writeTimeout(10, TimeUnit.SECONDS)
+ .build();
+
+ public static final MediaType MEDIA_TYPE_JSON =
MediaType.parse("application/json");
+ public static final MediaType MEDIA_TYPE_FORM_URLENCODED =
MediaType.parse("application/x-www-form-urlencoded");
+
+ public static void doPostHttp(
+ String url, Map<String, String> params, Map<String, String>
headers, HttpCallback<Response> callback) {
+ try {
+ Headers.Builder headerBuilder = new Headers.Builder();
+ if (headers != null) {
+ headers.forEach(headerBuilder::add);
+ }
+
+ String contentType = headers != null ? headers.get("Content-Type")
: "";
+ RequestBody requestBody = createRequestBody(params, contentType);
+
+ Request request = new Request.Builder()
+ .url(url)
+ .headers(headerBuilder.build())
+ .post(requestBody)
+ .build();
+
+ executeAsync(HTTP_CLIENT, request, callback);
+
+ } catch (JsonProcessingException e) {
+ LOGGER.error(e.getMessage(), e);
+ callback.onFailure(e);
+ }
+ }
+
+ public static void doPostHttp(
+ String url, String body, Map<String, String> headers,
HttpCallback<Response> callback) {
+ Headers.Builder headerBuilder = new Headers.Builder();
+ if (headers != null) {
+ headers.forEach(headerBuilder::add);
+ }
+
+ RequestBody requestBody = RequestBody.create(body, MEDIA_TYPE_JSON);
+
+ Request request = new Request.Builder()
+ .url(url)
+ .headers(headerBuilder.build())
+ .post(requestBody)
+ .build();
+
+ executeAsync(HTTP_CLIENT, request, callback);
+ }
+
+ public static void doGetHttp(
+ String url, Map<String, String> headers, final
HttpCallback<Response> callback, int timeout) {
+ OkHttpClient client = new OkHttpClient.Builder()
+ .connectTimeout(timeout, TimeUnit.SECONDS)
+ .readTimeout(timeout, TimeUnit.SECONDS)
+ .writeTimeout(timeout, TimeUnit.SECONDS)
+ .build();
+
+ 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();
+
+ executeAsync(client, request, callback);
+ }
+
+ private static RequestBody createRequestBody(Map<String, String> params,
String contentType)
+ throws JsonProcessingException {
+ if (params == null || params.isEmpty()) {
+ return RequestBody.create(new byte[0]);
+ }
+
+ if (MEDIA_TYPE_FORM_URLENCODED.toString().equals(contentType)) {
+ FormBody.Builder formBuilder = new FormBody.Builder();
+ params.forEach(formBuilder::add);
+ return formBuilder.build();
+ } else {
+ String json = OBJECT_MAPPER.writeValueAsString(params);
+ return RequestBody.create(json, MEDIA_TYPE_JSON);
+ }
+ }
+
+ private static void executeAsync(OkHttpClient client, Request request,
final HttpCallback<Response> callback) {
+ client.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ try {
+ callback.onSuccess(response);
+ } finally {
+ response.close();
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, IOException e) {
+ if (call.isCanceled()) {
+ callback.onCancelled();
+ } else {
+ callback.onFailure(e);
+ }
+ }
+ });
+ }
+}
diff --git
a/common/src/main/java/org/apache/seata/common/util/HttpClientUtil.java
b/common/src/main/java/org/apache/seata/common/util/HttpClientUtil.java
index 0490ad80f4..31a5f03ae3 100644
--- a/common/src/main/java/org/apache/seata/common/util/HttpClientUtil.java
+++ b/common/src/main/java/org/apache/seata/common/util/HttpClientUtil.java
@@ -51,6 +51,7 @@ public class HttpClientUtil {
private static final PoolingHttpClientConnectionManager
POOLING_HTTP_CLIENT_CONNECTION_MANAGER =
new PoolingHttpClientConnectionManager();
+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
diff --git
a/common/src/test/java/org/apache/seata/common/util/Http5ClientUtilTest.java
b/common/src/test/java/org/apache/seata/common/util/Http5ClientUtilTest.java
new file mode 100644
index 0000000000..058ea439e0
--- /dev/null
+++ b/common/src/test/java/org/apache/seata/common/util/Http5ClientUtilTest.java
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.seata.common.util;
+
+import okhttp3.Protocol;
+import okhttp3.Response;
+import org.apache.seata.common.executor.HttpCallback;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class Http5ClientUtilTest {
+
+ @Test
+ void testDoPostHttp_param_onSuccess() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response result) {
+ assertNotNull(result);
+ assertEquals(Protocol.HTTP_2, result.protocol());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ Map<String, String> params = new HashMap<>();
+ params.put("key", "value");
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Content-Type", "application/json");
+
+ Http5ClientUtil.doPostHttp("https://www.apache.org/", params, headers,
callback);
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void testDoPostHttp_param_onFailure() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response response) {
+ fail("Should not succeed");
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ assertNotNull(t);
+ latch.countDown();
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ Map<String, String> params = new HashMap<>();
+ params.put("key", "value");
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Content-Type", "application/json");
+
+ Http5ClientUtil.doPostHttp("http://localhost:9999/invalid", params,
headers, callback);
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void testDoPostHttp_body_onSuccess() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response result) {
+ assertNotNull(result);
+ assertEquals(Protocol.HTTP_2, result.protocol());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Content-Type", "application/json");
+
+ Http5ClientUtil.doPostHttp("https://www.apache.org/",
"{\"key\":\"value\"}", headers, callback);
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void testDoPostHttp_body_onFailure() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response response) {
+ fail("Should not succeed");
+ }
+
+ @Override
+ public void onFailure(Throwable t) {
+ assertNotNull(t);
+ latch.countDown();
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Content-Type", "application/json");
+
+ Http5ClientUtil.doPostHttp("http://localhost:9999/invalid",
"{\"key\":\"value\"}", headers, callback);
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void testDoPostHttp_param_onSuccess_forceHttp1() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response result) {
+ assertNotNull(result);
+ assertEquals(Protocol.HTTP_1_1, result.protocol());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ Map<String, String> params = new HashMap<>();
+ params.put("key", "value");
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Content-Type", "application/json");
+
+ Http5ClientUtil.doPostHttp("http://httpbin.org/post", params, headers,
callback);
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void testDoGetHttp_onSuccess() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response result) {
+ assertNotNull(result);
+ assertEquals(Protocol.HTTP_2, result.protocol());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Accept", "application/json");
+
+ Http5ClientUtil.doGetHttp("https://www.apache.org/", headers,
callback, 1);
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void testDoPostHttp_body_onSuccess_forceHttp1() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+
+ HttpCallback<Response> callback = new HttpCallback<Response>() {
+ @Override
+ public void onSuccess(Response result) {
+ assertNotNull(result);
+ assertEquals(Protocol.HTTP_1_1, result.protocol());
+ latch.countDown();
+ }
+
+ @Override
+ public void onFailure(Throwable e) {
+ fail("Should not fail");
+ }
+
+ @Override
+ public void onCancelled() {
+ fail("Should not be cancelled");
+ }
+ };
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Content-Type", "application/json");
+
+ Http5ClientUtil.doPostHttp("http://httpbin.org/post",
"{\"key\":\"value\"}", headers, callback);
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]