You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2026/05/27 08:15:54 UTC
(httpcomponents-client) branch master updated: Improved efficincy of Jakarta REST Response implementation
This is an automated email from the ASF dual-hosted git repository.
ok2c pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git
The following commit(s) were added to refs/heads/master by this push:
new d989173f9 Improved efficincy of Jakarta REST Response implementation
d989173f9 is described below
commit d989173f9e9c67e1710be47ac44a4617507e1c53
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Mon May 25 11:10:22 2026 +0200
Improved efficincy of Jakarta REST Response implementation
---
.../hc/client5/http/rest/RestClientResponse.java | 189 ++++++++-------------
.../client5/http/rest/RestInvocationHandler.java | 30 +++-
.../hc/client5/http/rest/RestResponseConsumer.java | 160 +++++++++++++++++
.../client5/http/rest/RestClientResponseTest.java | 7 +-
4 files changed, 257 insertions(+), 129 deletions(-)
diff --git a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java
index bf939fe36..2a16f56bb 100644
--- a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java
+++ b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestClientResponse.java
@@ -36,8 +36,8 @@
import java.time.Instant;
import java.util.Collections;
import java.util.Date;
+import java.util.Iterator;
import java.util.LinkedHashSet;
-import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@@ -54,13 +54,16 @@
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
-
import org.apache.hc.client5.http.utils.DateUtils;
+import org.apache.hc.client5.http.validator.ETag;
+import org.apache.hc.client5.http.validator.ValidatorType;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.message.MessageSupport;
import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.CharArrayBuffer;
/**
* Minimal {@link Response} implementation backed by a consumed {@link JsonNode}
@@ -83,30 +86,26 @@ final class RestClientResponse extends Response {
private static final byte[] EMPTY = new byte[0];
- private final int status;
- private final String reasonPhrase;
- private final JsonNode body;
- private final MediaType mediaType;
- private final MultivaluedMap<String, Object> metadata;
- private final MultivaluedMap<String, String> stringHeaders;
private final ObjectMapper objectMapper;
+ private final HttpResponse response;
+ private final JsonNode body;
+ private final ContentType contentType;
+ private final long len;
private boolean closed;
private Object cachedEntity;
- RestClientResponse(final HttpResponse response, final JsonNode body, final ObjectMapper objectMapper) {
- this.status = response.getCode();
- this.reasonPhrase = response.getReasonPhrase();
- this.body = body;
+ RestClientResponse(
+ final ObjectMapper objectMapper,
+ final HttpResponse response,
+ final JsonNode jsonNode,
+ final ContentType contentType,
+ final long len) {
this.objectMapper = Args.notNull(objectMapper, "Object mapper");
- this.metadata = new MultivaluedHashMap<>();
- this.stringHeaders = new MultivaluedHashMap<>();
- for (final Header h : response.getHeaders()) {
- this.metadata.add(h.getName(), h.getValue());
- this.stringHeaders.add(h.getName(), h.getValue());
- }
- final Header ct = response.getFirstHeader(HttpHeaders.CONTENT_TYPE);
- this.mediaType = ct != null ? toMediaType(ContentType.parse(ct.getValue())) : null;
+ this.response = Args.notNull(response, "Response");
+ this.contentType = contentType;
+ this.body = jsonNode;
+ this.len = len;
}
private static MediaType toMediaType(final ContentType ct) {
@@ -125,20 +124,21 @@ private static MediaType toMediaType(final ContentType ct) {
@Override
public int getStatus() {
- return status;
+ return response.getCode();
}
@Override
public StatusType getStatusInfo() {
- final Status standard = Status.fromStatusCode(status);
- final String reason = reasonPhrase != null ? reasonPhrase
+ final int statusCode = response.getCode();
+ final Status standard = Status.fromStatusCode(statusCode);
+ final String reason = response.getReasonPhrase() != null ? response.getReasonPhrase()
: standard != null ? standard.getReasonPhrase() : "";
- final Status.Family family = Status.Family.familyOf(status);
+ final Status.Family family = Status.Family.familyOf(statusCode);
return new StatusType() {
@Override
public int getStatusCode() {
- return status;
+ return statusCode;
}
@Override
@@ -162,7 +162,7 @@ public Object getEntity() {
@Override
public <T> T readEntity(final Class<T> entityType) {
- return readEntity(entityType, (Annotation[]) null);
+ return readEntity(entityType, null);
}
@Override
@@ -245,13 +245,7 @@ private byte[] bodyAsBytes() {
}
private Charset charset() {
- if (mediaType != null) {
- final String cs = mediaType.getParameters().get(MediaType.CHARSET_PARAMETER);
- if (cs != null) {
- return Charset.forName(cs);
- }
- }
- return StandardCharsets.UTF_8;
+ return ContentType.getCharset(contentType, StandardCharsets.UTF_8);
}
@Override
@@ -278,43 +272,25 @@ private void ensureOpen() {
@Override
public MediaType getMediaType() {
- return mediaType;
+ return toMediaType(contentType);
}
@Override
public Locale getLanguage() {
- final String lang = getHeaderString(HttpHeaders.CONTENT_LANGUAGE);
- return lang != null ? Locale.forLanguageTag(lang) : null;
+ final Header h = response.getFirstHeader(HttpHeaders.CONTENT_LANGUAGE);
+ return h != null ? Locale.forLanguageTag(h.getValue()) : null;
}
@Override
public int getLength() {
- final String len = getHeaderString(HttpHeaders.CONTENT_LENGTH);
- if (len != null) {
- try {
- return Integer.parseInt(len);
- } catch (final NumberFormatException ignore) {
- }
- }
- return hasEntity() ? bodyAsBytes().length : -1;
+ return (int) len;
}
@Override
public Set<String> getAllowedMethods() {
- final List<String> values = headerValues(HttpHeaders.ALLOW);
- if (values == null || values.isEmpty()) {
- return Collections.emptySet();
- }
- final Set<String> result = new LinkedHashSet<>();
- for (final String v : values) {
- for (final String m : v.split(",")) {
- final String trimmed = m.trim();
- if (!trimmed.isEmpty()) {
- result.add(trimmed.toUpperCase(Locale.ROOT));
- }
- }
- }
- return result;
+ final LinkedHashSet<String> allowedMethods = new LinkedHashSet<>();
+ MessageSupport.parseTokens(response, HttpHeaders.ALLOW, allowedMethods::add);
+ return allowedMethods;
}
@Override
@@ -324,51 +300,32 @@ public Map<String, NewCookie> getCookies() {
@Override
public EntityTag getEntityTag() {
- final String etag = getHeaderString(HttpHeaders.ETAG);
- if (etag == null) {
- return null;
- }
- String raw = etag.trim();
- boolean weak = false;
- if (raw.startsWith("W/")) {
- weak = true;
- raw = raw.substring(2).trim();
- }
- if (raw.length() >= 2 && raw.charAt(0) == '"' && raw.charAt(raw.length() - 1) == '"') {
- raw = raw.substring(1, raw.length() - 1);
- }
- return new EntityTag(raw, weak);
+ final ETag eTag = ETag.get(response);
+ return eTag != null ? new EntityTag(eTag.getValue(), eTag.getType() == ValidatorType.WEAK) : null;
}
@Override
public Date getDate() {
- return parseHttpDate(getHeaderString(HttpHeaders.DATE));
+ final Instant instant = DateUtils.parseStandardDate(response, HttpHeaders.DATE);
+ return instant != null ? Date.from(instant) : null;
}
@Override
public Date getLastModified() {
- return parseHttpDate(getHeaderString(HttpHeaders.LAST_MODIFIED));
- }
-
- private static Date parseHttpDate(final String value) {
- if (value == null) {
- return null;
- }
- final Instant instant = DateUtils.parseDate(value, DateUtils.STANDARD_PATTERNS);
+ final Instant instant = DateUtils.parseStandardDate(response, HttpHeaders.LAST_MODIFIED);
return instant != null ? Date.from(instant) : null;
}
@Override
public URI getLocation() {
- final String loc = getHeaderString(HttpHeaders.LOCATION);
- if (loc == null) {
- return null;
- }
- try {
- return new URI(loc);
- } catch (final URISyntaxException ex) {
- return null;
+ final Header h = response.getFirstHeader(HttpHeaders.LOCATION);
+ if (h != null) {
+ try {
+ return new URI(h.getValue());
+ } catch (final URISyntaxException ignore) {
+ }
}
+ return null;
}
@Override
@@ -394,52 +351,46 @@ public Link.Builder getLinkBuilder(final String relation) {
@Override
public MultivaluedMap<String, Object> getMetadata() {
- return metadata;
+ final MultivaluedMap<String, Object> multimap = new MultivaluedHashMap<>();
+ for (final Iterator<Header> it = response.headerIterator(); it.hasNext(); ) {
+ final Header h = it.next();
+ multimap.add(h.getName(), h.getValue());
+ }
+ return multimap;
}
@Override
public MultivaluedMap<String, String> getStringHeaders() {
- return stringHeaders;
+ final MultivaluedMap<String, String> multimap = new MultivaluedHashMap<>();
+ for (final Iterator<Header> it = response.headerIterator(); it.hasNext(); ) {
+ final Header h = it.next();
+ multimap.add(h.getName(), h.getValue());
+ }
+ return multimap;
}
@Override
public String getHeaderString(final String name) {
- final List<String> values = headerValues(name);
- if (values == null || values.isEmpty()) {
+ final Header[] headers = response.getHeaders(name);
+ if (headers.length == 0) {
return null;
- }
- if (values.size() == 1) {
- return values.get(0);
- }
- final StringBuilder sb = new StringBuilder();
- for (final String v : values) {
- if (sb.length() > 0) {
- sb.append(',');
+ } else if (headers.length == 1) {
+ return headers[0].getValue();
+ } else {
+ final CharArrayBuffer buf = new CharArrayBuffer(128);
+ buf.append(headers[0].getValue());
+ for (int i = 1; i < headers.length; i++) {
+ buf.append(", ");
+ buf.append(headers[i].getValue());
}
- sb.append(v);
- }
- return sb.toString();
- }
- private List<String> headerValues(final String name) {
- for (final Map.Entry<String, List<String>> entry : stringHeaders.entrySet()) {
- if (entry.getKey().equalsIgnoreCase(name)) {
- return entry.getValue();
- }
+ return buf.toString();
}
- return null;
}
@Override
public String toString() {
- final StringBuilder sb = new StringBuilder("RestClientResponse[status=");
- sb.append(status);
- if (mediaType != null) {
- sb.append(", mediaType=").append(mediaType);
- }
- sb.append(", length=").append(getLength());
- sb.append(']');
- return sb.toString();
+ return response.toString();
}
}
\ No newline at end of file
diff --git a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java
index 5858f4a65..aebb088b0 100644
--- a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java
+++ b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestInvocationHandler.java
@@ -43,6 +43,7 @@
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -68,7 +69,6 @@
import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
-import org.apache.hc.core5.jackson2.http.JsonNodeEntityFallbackConsumer;
import org.apache.hc.core5.jackson2.http.JsonObjectEntityProducer;
import org.apache.hc.core5.jackson2.http.JsonResponseConsumers;
import org.apache.hc.core5.net.URIBuilder;
@@ -183,7 +183,7 @@ private Object executeRequest(final ResourceMethod rm,
final boolean isAsync = isAsync(rm.getMethod());
final Class<?> rawType = resolveResponseType(rm.getMethod(), isAsync);
- final CompletableFuture<Object> future = dispatchAsync(rawType, requestProducer);
+ final Future<?> future = dispatchAsync(rawType, requestProducer);
if (isAsync) {
return future;
}
@@ -215,7 +215,7 @@ private static Class<?> resolveResponseType(final Method method, final boolean a
return Object.class;
}
- private CompletableFuture<Object> dispatchAsync(final Class<?> rawType,
+ private CompletableFuture<?> dispatchAsync(final Class<?> rawType,
final BasicRequestProducer requestProducer) {
if (rawType == void.class || rawType == Void.class) {
return submit(requestProducer, new BasicResponseConsumer<>(new DiscardingEntityConsumer<>()))
@@ -225,8 +225,26 @@ private CompletableFuture<Object> dispatchAsync(final Class<?> rawType,
});
}
if (rawType == Response.class) {
- return submit(requestProducer, new BasicResponseConsumer<>(new JsonNodeEntityFallbackConsumer(objectMapper)))
- .thenApply(result -> new RestClientResponse(result.getHead(), result.getBody(), objectMapper));
+ final CompletableFuture<Object> cf = new CompletableFuture<>();
+ httpClient.execute(requestProducer, new RestResponseConsumer(objectMapper), null,
+ new FutureCallback<>() {
+
+ @Override
+ public void completed(final Response result) {
+ cf.complete(result);
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ cf.completeExceptionally(ex);
+ }
+
+ @Override
+ public void cancelled() {
+ cf.cancel(false);
+ }
+ });
+ return cf;
}
if (rawType == byte[].class) {
return submit(requestProducer, new BasicResponseConsumer<>(new BasicAsyncEntityConsumer()))
@@ -277,7 +295,7 @@ public void cancelled() {
return cf;
}
- private static Object awaitSync(final CompletableFuture<Object> future) {
+ private static Object awaitSync(final Future<?> future) {
try {
return future.get();
} catch (final ExecutionException ex) {
diff --git a/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestResponseConsumer.java b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestResponseConsumer.java
new file mode 100644
index 000000000..42ca110c1
--- /dev/null
+++ b/httpclient5-jakarta-rest-client/src/main/java/org/apache/hc/client5/http/rest/RestResponseConsumer.java
@@ -0,0 +1,160 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.rest;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+
+import jakarta.ws.rs.core.Response;
+import org.apache.hc.core5.concurrent.CallbackContribution;
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.nio.AsyncEntityConsumer;
+import org.apache.hc.core5.http.nio.AsyncResponseConsumer;
+import org.apache.hc.core5.http.nio.CapacityChannel;
+import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.jackson2.http.JsonNodeEntityConsumer;
+import org.apache.hc.core5.util.Args;
+
+final class RestResponseConsumer implements AsyncResponseConsumer<Response> {
+
+ private final ObjectMapper objectMapper;
+ private final AtomicReference<AsyncEntityConsumer<?>> entityConsumerRef;
+
+ public RestResponseConsumer(final ObjectMapper objectMapper) {
+ this.objectMapper = Args.notNull(objectMapper, "Object mapper");
+ this.entityConsumerRef = new AtomicReference<>();
+ }
+
+ @Override
+ public void informationResponse(final HttpResponse response, final HttpContext context) {
+ }
+
+ @Override
+ public void consumeResponse(final HttpResponse httpResponse,
+ final EntityDetails entityDetails,
+ final HttpContext context,
+ final FutureCallback<Response> resultCallback) throws HttpException, IOException {
+ if (entityDetails == null) {
+ resultCallback.completed(new RestClientResponse(
+ objectMapper,
+ httpResponse,
+ null,
+ null,
+ -1));
+ return;
+ }
+ final ContentType contentType = ContentType.parseLenient(entityDetails.getContentType());
+ if (contentType == null || ContentType.APPLICATION_JSON.isSameMimeType(contentType)) {
+ final AsyncEntityConsumer<JsonNode> entityConsumer = new JsonNodeEntityConsumer(objectMapper.getFactory());
+ entityConsumerRef.set(entityConsumer);
+ entityConsumer.streamStart(entityDetails, new CallbackContribution<>(resultCallback) {
+
+ @Override
+ public void completed(final JsonNode result) {
+ resultCallback.completed(new RestClientResponse(
+ objectMapper,
+ httpResponse,
+ result,
+ contentType,
+ entityDetails.getContentLength()));
+ }
+
+ });
+ } else {
+ final AsyncEntityConsumer<String> entityConsumer = new StringAsyncEntityConsumer();
+ entityConsumerRef.set(entityConsumer);
+ entityConsumer.streamStart(entityDetails, new CallbackContribution<>(resultCallback) {
+
+ @Override
+ public void completed(final String result) {
+ resultCallback.completed(new RestClientResponse(
+ objectMapper,
+ httpResponse,
+ JsonNodeFactory.instance.textNode(result),
+ contentType,
+ entityDetails.getContentLength()));
+ }
+
+ });
+ }
+ }
+
+ @Override
+ public void updateCapacity(final CapacityChannel capacityChannel) throws IOException {
+ final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+ if (entityConsumer != null) {
+ entityConsumer.updateCapacity(capacityChannel);
+ } else {
+ capacityChannel.update(Integer.MAX_VALUE);
+ }
+ }
+
+ @Override
+ public void consume(final ByteBuffer data) throws IOException {
+ final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+ if (entityConsumer != null) {
+ entityConsumer.consume(data);
+ }
+ }
+
+ @Override
+ public void streamEnd(final List<? extends Header> trailers) throws HttpException, IOException {
+ final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+ if (entityConsumer != null) {
+ entityConsumer.streamEnd(trailers);
+ }
+ }
+
+ public void failed(final Exception cause) {
+ final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.get();
+ if (entityConsumer != null) {
+ entityConsumer.failed(cause);
+ }
+ releaseResources();
+ }
+
+ @Override
+ public void releaseResources() {
+ final AsyncEntityConsumer<?> entityConsumer = entityConsumerRef.getAndSet(null);
+ if (entityConsumer != null) {
+ entityConsumer.releaseResources();
+ }
+ }
+
+}
diff --git a/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java b/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java
index 64217853d..f3a735138 100644
--- a/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java
+++ b/httpclient5-jakarta-rest-client/src/test/java/org/apache/hc/client5/http/rest/RestClientResponseTest.java
@@ -26,6 +26,8 @@
*/
package org.apache.hc.client5.http.rest;
+import static org.assertj.core.api.Assertions.assertThat;
+
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
@@ -44,7 +46,6 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Response;
-
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.core5.http.ContentType;
@@ -54,8 +55,6 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import static org.assertj.core.api.Assertions.assertThat;
-
class RestClientResponseTest {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@@ -144,7 +143,7 @@ void responseJsonEntityIsBackedByJsonNode() {
final JsonNode node = OBJECT_MAPPER.createObjectNode().put("id", "abc");
- try (Response response = new RestClientResponse(httpResponse, node, OBJECT_MAPPER)) {
+ try (Response response = new RestClientResponse(OBJECT_MAPPER, httpResponse, node, ContentType.APPLICATION_JSON, -1)) {
assertThat(response.readEntity(JsonNode.class)).isSameAs(node);
assertThat(response.readEntity(Echo.class).id).isEqualTo("abc");
assertThat(response.readEntity(String.class)).isEqualTo("{\"id\":\"abc\"}");