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 <yongj...@apache.org> 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: notifications-unsubscr...@seata.apache.org For additional commands, e-mail: notifications-h...@seata.apache.org