This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 8f1f38a1da7fd6297607e4b53779ecdfec4a923d Author: Robert Lazarski <[email protected]> AuthorDate: Sun Apr 19 18:17:44 2026 -1000 AXIS2-6103 Add HTTP/2 Java client example and documentation New Http2JsonClient.java in userguide samples — standalone Apache HttpClient 5 HTTP/2 client for Axis2 JSON-RPC services. Two modes: buffered (returns String) and streaming (writes 64KB chunks to OutputStream). Includes timeout cancellation and interrupt handling. New http2-java-client.xml xdoc page with usage examples for both modes, dependency info, and end-to-end streaming flow diagram. Linked in toc.xml as section 18.4a under Transports. --- .../springboot/client/Http2JsonClient.java | 288 +++++++++++++++++++++ src/site/xdoc/docs/http2-java-client.xml | 159 ++++++++++++ src/site/xdoc/docs/toc.xml | 21 +- 3 files changed, 458 insertions(+), 10 deletions(-) diff --git a/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java new file mode 100644 index 0000000000..7a91dc9fdf --- /dev/null +++ b/modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java @@ -0,0 +1,288 @@ +/* + * 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 userguide.springboot.client; + +import javax.net.ssl.SSLContext; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Timeout; + +/** + * Standalone HTTP/2 JSON client for Apache Axis2 services. + * + * <p>Uses Apache HttpClient 5 async with HTTP/2 (ALPN over TLS) to call + * any Axis2 JSON-RPC endpoint. Two execution modes:</p> + * + * <ul> + * <li>{@link #execute} — returns the full response as a String</li> + * <li>{@link #executeStreaming} — writes response bytes to an OutputStream + * in 64KB chunks as HTTP/2 DATA frames arrive; memory stays flat</li> + * </ul> + * + * <p>Requires: Java 11+, Apache HttpClient 5.4+ (httpcore5-h2 for HTTP/2).</p> + * + * <h3>Quick start</h3> + * <pre> + * // 1. POST JSON-RPC, get response as String + * String response = Http2JsonClient.execute( + * "https://localhost:8443/axis2-json-api/services/FinancialBenchmarkService", + * "{\"monteCarlo\":[{\"arg0\":{\"nSimulations\":100000}}]}", + * 300); + * + * // 2. Stream a large response to a file + * try (FileOutputStream fos = new FileOutputStream("/tmp/result.json")) { + * int status = Http2JsonClient.executeStreaming( + * "https://localhost:8443/axis2-json-api/services/BigDataH2Service", + * "{\"generate\":[{\"arg0\":{\"datasetSize\":52428800}}]}", + * 300, fos); + * } + * + * // 3. Shutdown when done + * Http2JsonClient.shutdown(); + * </pre> + * + * @see <a href="https://axis.apache.org/axis2/java/core/docs/http2-integration-guide.html"> + * Axis2 HTTP/2 Integration Guide</a> + */ +public class Http2JsonClient { + + private static volatile CloseableHttpAsyncClient sharedClient; + + /** + * Get or create the shared HTTP/2 async client. + * + * <p>Initialized once, reused for all requests. Uses TLS with + * TrustAllStrategy for development — replace with proper trust + * material in production.</p> + */ + private static synchronized CloseableHttpAsyncClient getClient() throws Exception { + if (sharedClient == null) { + H2Config h2Config = H2Config.custom() + .setMaxConcurrentStreams(100) + .setPushEnabled(false) + .setInitialWindowSize(65536) + .build(); + + IOReactorConfig ioConfig = IOReactorConfig.custom() + .setTcpNoDelay(true) + .setSoTimeout(Timeout.ofMinutes(5)) + .build(); + + SSLContext sslContext = SSLContexts.custom() + .loadTrustMaterial(null, TrustAllStrategy.INSTANCE) + .build(); + + PoolingAsyncClientConnectionManager connManager = + PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(ClientTlsStrategyBuilder.create() + .setSslContext(sslContext) + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + .build()) + .setMaxConnTotal(50) + .setMaxConnPerRoute(10) + .build(); + + sharedClient = HttpAsyncClients.custom() + .setH2Config(h2Config) + .setIOReactorConfig(ioConfig) + .setConnectionManager(connManager) + .build(); + + sharedClient.start(); + } + return sharedClient; + } + + /** + * POST JSON to an Axis2 service and return the response as a String. + * + * @param url HTTPS endpoint (e.g., {@code https://host:8443/axis2-json-api/services/MyService}) + * @param json JSON-RPC request body + * @param timeoutSeconds maximum wait time for the response + * @return response body as a String + * @throws Exception on HTTP error or timeout + */ + public static String execute(String url, String json, int timeoutSeconds) throws Exception { + CloseableHttpAsyncClient client = getClient(); + + SimpleHttpRequest request = SimpleRequestBuilder.post(url) + .setBody(json, ContentType.APPLICATION_JSON) + .setHeader("Accept", "application/json") + .build(); + + CompletableFuture<SimpleHttpResponse> future = new CompletableFuture<>(); + Future<SimpleHttpResponse> requestFuture = client.execute(request, + new FutureCallback<SimpleHttpResponse>() { + @Override public void completed(SimpleHttpResponse r) { future.complete(r); } + @Override public void failed(Exception ex) { future.completeExceptionally(ex); } + @Override public void cancelled() { future.cancel(true); } + }); + + SimpleHttpResponse response; + try { + response = future.get(timeoutSeconds, TimeUnit.SECONDS); + } catch (Exception e) { + requestFuture.cancel(true); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw e; + } + + int status = response.getCode(); + if (status < 200 || status >= 300) { + throw new java.io.IOException("HTTP " + status + ": " + + response.getBodyText().substring(0, Math.min(500, + response.getBodyText() != null ? response.getBodyText().length() : 0))); + } + return response.getBodyText(); + } + + /** + * POST JSON to an Axis2 service and stream the response to an OutputStream. + * + * <p>Instead of buffering the entire response in memory, writes 64KB chunks + * to the provided OutputStream as HTTP/2 DATA frames arrive. Memory stays + * flat regardless of response size.</p> + * + * <p>When paired with Axis2's + * {@code MoshiStreamingMessageFormatter} (AXIS2-6103), data flows + * end-to-end in 64KB chunks: server flushes → HTTP/2 DATA frames → + * this callback → your OutputStream.</p> + * + * @param url HTTPS endpoint + * @param json JSON-RPC request body + * @param timeoutSeconds maximum wait time for the response + * @param outputStream destination for response bytes + * @return HTTP status code + * @throws Exception on HTTP error or timeout + */ + public static int executeStreaming(String url, String json, + int timeoutSeconds, OutputStream outputStream) throws Exception { + CloseableHttpAsyncClient client = getClient(); + + org.apache.hc.core5.http.nio.AsyncRequestProducer requestProducer = + org.apache.hc.core5.http.nio.support.AsyncRequestBuilder.post(url) + .setEntity(json, ContentType.APPLICATION_JSON) + .addHeader("Accept", "application/json") + .build(); + + final long[] totalBytes = {0}; + final int[] chunkCount = {0}; + final int[] statusCode = {0}; + + org.apache.hc.client5.http.async.methods.AbstractBinResponseConsumer<Integer> consumer = + new org.apache.hc.client5.http.async.methods.AbstractBinResponseConsumer<Integer>() { + + private byte[] transferBuffer; + + @Override + protected void start(org.apache.hc.core5.http.HttpResponse response, + org.apache.hc.core5.http.ContentType contentType) + throws org.apache.hc.core5.http.HttpException, java.io.IOException { + statusCode[0] = response.getCode(); + } + + @Override + protected int capacityIncrement() { + return 64 * 1024; + } + + @Override + protected void data(java.nio.ByteBuffer src, boolean endOfStream) + throws java.io.IOException { + int len = src.remaining(); + if (len > 0) { + if (src.hasArray()) { + outputStream.write(src.array(), src.arrayOffset() + src.position(), len); + src.position(src.position() + len); + } else { + if (transferBuffer == null || transferBuffer.length < len) { + transferBuffer = new byte[len]; + } + src.get(transferBuffer, 0, len); + outputStream.write(transferBuffer, 0, len); + } + outputStream.flush(); + totalBytes[0] += len; + chunkCount[0]++; + } + } + + @Override + protected Integer buildResult() { return statusCode[0]; } + + @Override + public void releaseResources() { } + }; + + CompletableFuture<Integer> future = new CompletableFuture<>(); + Future<Integer> requestFuture = client.execute(requestProducer, consumer, + new FutureCallback<Integer>() { + @Override public void completed(Integer r) { future.complete(r); } + @Override public void failed(Exception ex) { future.completeExceptionally(ex); } + @Override public void cancelled() { future.cancel(true); } + }); + + int result; + try { + result = future.get(timeoutSeconds, TimeUnit.SECONDS); + } catch (Exception e) { + requestFuture.cancel(true); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw e; + } + + return result; + } + + /** + * Shut down the shared HTTP/2 client. Call once at application exit. + */ + public static void shutdown() { + if (sharedClient != null) { + try { + sharedClient.close(); + } catch (Exception e) { + // ignore + } + } + } +} diff --git a/src/site/xdoc/docs/http2-java-client.xml b/src/site/xdoc/docs/http2-java-client.xml new file mode 100644 index 0000000000..61d57ca593 --- /dev/null +++ b/src/site/xdoc/docs/http2-java-client.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ 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. + --> +<document xmlns="http://maven.apache.org/XDOC/2.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd"> + + <properties> + <title>HTTP/2 Java Client for Axis2 JSON-RPC Services</title> + </properties> + + <body> + <section name="HTTP/2 Java Client"> + + <p>A standalone Java client for calling Axis2 JSON-RPC services over + HTTP/2. Uses Apache HttpClient 5 async with ALPN negotiation over TLS.</p> + + <p>Two execution modes:</p> + <ul> + <li><strong>Buffered</strong> — returns the full response as a + <code>String</code>. Simple, suitable for responses that fit + in memory.</li> + <li><strong>Streaming</strong> — writes response bytes to an + <code>OutputStream</code> in 64KB chunks as HTTP/2 DATA + frames arrive. Memory stays flat regardless of response size. + When paired with the + <a href="json-streaming-formatter.html">Streaming JSON + Formatter</a> (AXIS2-6103), data flows end-to-end in 64KB + chunks.</li> + </ul> + + </section> + + <section name="Dependencies"> + + <p>Requires Java 11+ and Apache HttpClient 5.4+:</p> + +<source> +<!-- Maven --> +<dependency> + <groupId>org.apache.httpcomponents.client5</groupId> + <artifactId>httpclient5</artifactId> + <version>5.4.3</version> +</dependency> +<dependency> + <groupId>org.apache.httpcomponents.core5</groupId> + <artifactId>httpcore5-h2</artifactId> + <version>5.4.1</version> +</dependency> +</source> + + </section> + + <section name="Buffered Execution"> + + <p>POST JSON-RPC to any Axis2 service, get the response as a String:</p> + +<source> +String url = "https://localhost:8443/axis2-json-api/services/FinancialBenchmarkService"; +String json = "{\"monteCarlo\":[{\"arg0\":{\"nSimulations\":100000,\"nPeriods\":252," + + "\"initialValue\":1000000,\"expectedReturn\":0.10,\"volatility\":0.223," + + "\"nPeriodsPerYear\":252,\"randomSeed\":42}}]}"; + +String response = Http2JsonClient.execute(url, json, 300); +System.out.println(response); + +Http2JsonClient.shutdown(); +</source> + + <p>The client negotiates HTTP/2 via ALPN on the TLS handshake. + Connections are pooled and multiplexed — multiple concurrent requests + share a single TCP connection.</p> + + </section> + + <section name="Streaming Execution"> + + <p>For large responses (10MB+), stream to a file or parser instead + of buffering in heap:</p> + +<source> +String url = "https://localhost:8443/axis2-json-api/services/BigDataH2Service"; +String json = "{\"generate\":[{\"arg0\":{\"datasetSize\":52428800}}]}"; + +try (FileOutputStream fos = new FileOutputStream("/tmp/result.json")) { + int status = Http2JsonClient.executeStreaming(url, json, 300, fos); + System.out.println("HTTP " + status); +} + +Http2JsonClient.shutdown(); +</source> + + <p>Each HTTP/2 DATA frame triggers a callback that writes directly + to your <code>OutputStream</code>. The <code>capacityIncrement()</code> + returns 64KB, creating natural HTTP/2 flow control backpressure — + the client tells the server "I can accept 64KB more" after each + chunk.</p> + + <p>When the server uses the + <a href="json-streaming-formatter.html">Streaming JSON Formatter</a>, + data flows end-to-end without full-body buffering on either side:</p> + +<source> +Server (MoshiStreamingMessageFormatter) + → FlushingOutputStream flushes every 64KB + → HTTP/2 DATA frames + → Http2JsonClient.data() callback + → your OutputStream +</source> + + </section> + + <section name="Timeout and Cancellation"> + + <p>Both methods accept a <code>timeoutSeconds</code> parameter for + <code>Future.get()</code>. If the timeout expires or the thread is + interrupted, the underlying HTTP request is cancelled to prevent + zombie requests that would continue consuming resources:</p> + +<source> +try { + response = future.get(timeoutSeconds, TimeUnit.SECONDS); +} catch (Exception e) { + requestFuture.cancel(true); // Cancel the HTTP request + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); // Restore interrupt flag + } + throw e; +} +</source> + + </section> + + <section name="Source Code"> + + <p>The complete client is available in the userguide samples:</p> + + <p><code>modules/samples/userguide/src/userguide/springbootdemo-tomcat11/src/main/java/userguide/springboot/client/Http2JsonClient.java</code></p> + + </section> + + </body> +</document> diff --git a/src/site/xdoc/docs/toc.xml b/src/site/xdoc/docs/toc.xml index 754690cbe1..5559ce14f7 100644 --- a/src/site/xdoc/docs/toc.xml +++ b/src/site/xdoc/docs/toc.xml @@ -116,16 +116,17 @@ Guide</a></li> <li>Transports <ul> <li>18.1 <a href="http-transport.html">HTTP -Transport sender</a></li> -<li>18.2 <a href="axis2-http2-simplified-example.html">HTTP/2 -Simplified Configuration</a></li> -<li>18.3 <a href="http2-transport-additions.html">HTTP/2 -Transport</a></li> -<li>18.4 <a href="http2-integration-guide.html">HTTP/2 -Integration Guide</a></li> -<li>18.5 <a href="wildfly-http2-integration-guide.html">WildFly HTTP/2 -Integration Guide</a></li> -<li>18.6 <a href="json-streaming-formatter.html">Streaming JSON Formatter for Large HTTP/2 Responses</a></li> +Transport sender</a> (HTTP/1.1)</li> +<li>18.2 <a href="http2-integration-guide.html"><strong>HTTP/2 +Overview — start here</strong></a> (architecture, framework comparison, why Axis2 is different)</li> +<li>18.3 <a href="axis2-http2-simplified-example.html">HTTP/2 +Simplified Configuration</a> (minimal axis2.xml — 2 parameters)</li> +<li>18.4 <a href="json-streaming-formatter.html">Streaming JSON Formatter + Field Selection</a> (64 KB flush, <code>?fields=</code>)</li> +<li>18.4a <a href="http2-java-client.html">HTTP/2 Java Client</a> (standalone HttpClient 5, buffered + streaming)</li> +<li>18.5 <a href="wildfly-http2-integration-guide.html">WildFly HTTP/2</a> | +<a href="tomcat-http2-integration-guide.html">Tomcat HTTP/2</a> (container-specific)</li> +<li>18.6 <a href="http2-transport-additions.html">HTTP/2 +Transport Module Reference</a> (transport-h2 internals)</li> <li>18.7 <a href="servlet-transport.html">HTTP servlet transport</a></li> <li>18.8 <a href="jms-transport.html">JMS Transport</a></li>
