feat(http-client): add CDI provider and priority-ordered factory fall… · a2aproject/a2a-java@ba5f199 · GitHub
Skip to content

Commit ba5f199

Browse files
committed
feat(http-client): add CDI provider and priority-ordered factory fallback
- extras/http-client-cdi: CdiA2AHttpClientProvider at priority 200 resolves a user-provided A2AHttpClient bean from CDI; falls back gracefully when no container is active or no unambiguous bean is found - A2AHttpClientFactory now sorts providers at class init and iterates in order, catching exceptions per-provider; create(String) reuses the cached list instead of reloading ServiceLoader on each call - tests use Weld SE SeContainerInitializer (no mocks) covering all four paths: bean present, no bean, ambiguous, no CDI container - update A2AHttpClientProvider Javadoc with full four-tier priority table - fix VertxA2AHttpClientProvider Javadoc and rename log field to LOGGER Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
1 parent c30adb8 commit ba5f199

17 files changed

Lines changed: 555 additions & 35 deletions

File tree

‎boms/extras/pom.xml‎

Lines changed: 5 additions & 0 deletions

‎boms/extras/src/it/extras-usage-test/pom.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
<groupId>org.a2aproject.sdk</groupId>
5353
<artifactId>a2a-java-sdk-http-client-vertx</artifactId>
5454
</dependency>
55+
<dependency>
56+
<groupId>org.a2aproject.sdk</groupId>
57+
<artifactId>a2a-java-sdk-http-client-cdi</artifactId>
58+
</dependency>
5559
<dependency>
5660
<groupId>org.a2aproject.sdk</groupId>
5761
<artifactId>a2a-java-sdk-opentelemetry-common</artifactId>

‎docs/content/dev/extra/http-clients.md‎

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ layout: page
66

77
# HTTP Clients
88

9-
The A2A SDK uses an `A2AHttpClient` abstraction for all HTTP communication — fetching agent cards, making transport calls, and Server-Sent Events (SSE) streaming. By default, the SDK uses a JDK 11+ HttpClient implementation. These extras provide alternative implementations.
9+
The A2A SDK uses an `A2AHttpClient` abstraction for all HTTP communication — fetching agent cards, making transport calls, and Server-Sent Events (SSE) streaming. By default, the SDK uses a JDK 11+ HttpClient implementation. These extras provide alternative implementations or a CDI-based extension point for custom clients.
1010

1111
## Vert.x HTTP Client
1212

@@ -53,7 +53,19 @@ For non-Quarkus environments, also add `vertx-web-client`:
5353

5454
### 2. Automatic Discovery (No Code Changes)
5555

56-
The `VertxA2AHttpClientProvider` has **priority 100** (vs. 0 for the JDK client). The SDK's `A2AHttpClientFactory` uses `ServiceLoader` to discover and select the highest-priority provider available:
56+
The `VertxA2AHttpClientProvider` has **priority 100**. The SDK's `A2AHttpClientFactory` discovers all registered providers via `ServiceLoader`, sorts them by descending priority, and tries each in order, falling back to the next one on failure.
57+
58+
To force a specific provider by name (useful in tests), use `A2AHttpClientFactory.create(String providerName)`:
59+
60+
```java
61+
// Force the JDK client regardless of what else is on the classpath
62+
try (A2AHttpClient client = A2AHttpClientFactory.create("jdk")) {
63+
// ...
64+
}
65+
```
66+
67+
Available built-in provider names: `jdk`, `vertx`, `android`, `cdi`.
68+
5769

5870
```java
5971
// No changes needed — A2A SDK automatically uses VertxA2AHttpClient
@@ -65,7 +77,7 @@ Client client = Client.builder(card)
6577
.build();
6678
```
6779

68-
If Vert.x classes are not on the classpath, the provider returns priority `-1` and the SDK falls back to the JDK HttpClient.
80+
If Vert.x classes are not on the classpath, the provider returns priority `-1` and the SDK falls back to the next available provider.
6981

7082
### Usage Examples
7183

@@ -205,3 +217,52 @@ The `AndroidA2AHttpClientProvider` has **priority 110** and detects the Android
205217
- **SSE streaming** via `getAsyncSSE()` and `postAsyncSSE()` — async operations run on a dedicated cached thread pool (`A2A-Android-Net`)
206218
- **Timeouts**: 15 s connection, 60 s read
207219
- **Response size limit**: 10 MB
220+
221+
## CDI HTTP Client
222+
223+
Allows providing a fully custom `A2AHttpClient` bean through the CDI container. When this module is on the classpath and a user-provided `A2AHttpClient` bean is registered, it is used in place of any other provider.
224+
225+
### When to Use
226+
227+
- Quarkus or Jakarta EE applications that need a custom HTTP client (specific TLS configuration, connection pooling, tracing, etc.)
228+
- When neither the JDK nor the Vert.x client meets your requirements out of the box
229+
230+
### 1. Add Dependency
231+
232+
```xml
233+
<dependency>
234+
<groupId>org.a2aproject.sdk</groupId>
235+
<artifactId>a2a-java-sdk-http-client-cdi</artifactId>
236+
</dependency>
237+
```
238+
239+
### 2. Produce an A2AHttpClient Bean
240+
241+
Declare a CDI producer in your application. The SDK will discover and use it automatically:
242+
243+
```java
244+
@ApplicationScoped
245+
public class MyHttpClientProducer {
246+
247+
@Produces
248+
@ApplicationScoped
249+
public A2AHttpClient produce(MyConfig config) {
250+
// return any A2AHttpClient implementation
251+
}
252+
}
253+
```
254+
255+
> **Scope requirement:** Use a normal scope such as `@ApplicationScoped`. A `@Dependent`-scoped producer will leak a new unmanaged instance on every `create()` call because the `Instance` used to obtain the bean is discarded immediately and CDI cannot destroy the dependent instance.
256+
257+
### How It Works
258+
259+
`CdiA2AHttpClientProvider` has **priority 200** — the highest of all built-in providers (Android: 110, Vert.x: 100, JDK: 0). The `A2AHttpClientFactory` iterates providers in descending priority order and falls back to the next one if a provider is unavailable:
260+
261+
| Priority | Provider | Activates when |
262+
|----------|----------|----------------|
263+
| 200 | CDI | A CDI container is active and an `A2AHttpClient` bean is registered |
264+
| 110 | Android | Android runtime detected |
265+
| 100 | Vert.x | `vertx-web-client` is on the classpath |
266+
| 0 | JDK | Always (fallback) |
267+
268+
If the CDI container is active but no `A2AHttpClient` bean is registered, this provider is skipped and the next available provider is used.

‎docs/content/dev/extras.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Import the extras BOM to manage versions:
3030

3131
## [HTTP Clients](../extra/http-clients)
3232

33-
Alternative `A2AHttpClient` implementations. The **Vert.x HTTP Client** (`a2a-java-sdk-http-client-vertx`) replaces the default JDK HttpClient with a reactive, non-blocking Vert.x WebClient — recommended for Quarkus, reactive frameworks, and high-throughput scenarios. An **Android HTTP Client** (`a2a-java-sdk-http-client-android`) is also available for mobile applications.
33+
Alternative `A2AHttpClient` implementations. The **Vert.x HTTP Client** (`a2a-java-sdk-http-client-vertx`) replaces the default JDK HttpClient with a reactive, non-blocking Vert.x WebClient — recommended for Quarkus, reactive frameworks, and high-throughput scenarios. An **Android HTTP Client** (`a2a-java-sdk-http-client-android`) is available for mobile applications. The **CDI HTTP Client** (`a2a-java-sdk-http-client-cdi`) lets you supply any custom `A2AHttpClient` bean via the CDI container, taking highest priority (200) over all built-in providers.
3434

3535
## [Storage & Persistence](../extra/storage)
3636

‎extras/http-client-cdi/pom.xml‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version="1.0"?>
2+
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.a2aproject.sdk</groupId>
9+
<artifactId>a2a-java-sdk-parent</artifactId>
10+
<version>1.2.1.Final-SNAPSHOT</version>
11+
<relativePath>../../pom.xml</relativePath>
12+
</parent>
13+
<artifactId>a2a-java-sdk-http-client-cdi</artifactId>
14+
15+
<packaging>jar</packaging>
16+
17+
<name>Java SDK A2A HTTP Client - CDI Implementation</name>
18+
<description>CDI-based A2AHttpClientProvider that resolves a user-provided A2AHttpClient bean from the CDI container</description>
19+
20+
<dependencies>
21+
<dependency>
22+
<groupId>${project.groupId}</groupId>
23+
<artifactId>a2a-java-sdk-http-client</artifactId>
24+
</dependency>
25+
<dependency>
26+
<groupId>jakarta.enterprise</groupId>
27+
<artifactId>jakarta.enterprise.cdi-api</artifactId>
28+
<scope>provided</scope>
29+
</dependency>
30+
31+
<dependency>
32+
<groupId>org.junit.jupiter</groupId>
33+
<artifactId>junit-jupiter-api</artifactId>
34+
<scope>test</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>org.jboss.weld.se</groupId>
38+
<artifactId>weld-se-core</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
</dependencies>
42+
43+
</project>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.a2aproject.sdk.client.http.cdi;
2+
3+
import jakarta.enterprise.inject.Instance;
4+
import jakarta.enterprise.inject.spi.CDI;
5+
6+
import org.a2aproject.sdk.client.http.A2AHttpClient;
7+
import org.a2aproject.sdk.client.http.A2AHttpClientProvider;
8+
9+
/**
10+
* CDI-based {@link A2AHttpClientProvider} that resolves an {@link A2AHttpClient} bean
11+
* from the CDI container.
12+
*
13+
* <p>When a user declares a CDI producer for {@link A2AHttpClient}, this provider
14+
* picks it up at priority 200 (above Vert.x at 100 and JDK at 0), allowing
15+
* application-level customization without replacing the ServiceLoader mechanism.
16+
*
17+
* <p>If no CDI container is active, or if no unambiguous {@link A2AHttpClient} bean
18+
* is registered, this provider throws and the factory falls back to the next
19+
* lower-priority provider.
20+
*
21+
* <p><strong>Scope requirement:</strong> The producer must use a normal scope (e.g.
22+
* {@code @ApplicationScoped}). A {@code @Dependent}-scoped producer will leak a new
23+
* unmanaged instance on every {@code create()} call, because the {@link Instance}
24+
* used to obtain the bean is discarded after this method returns and CDI cannot
25+
* destroy the dependent instance.
26+
*
27+
* <h2>Usage</h2>
28+
* <pre>{@code
29+
* @Produces
30+
* @ApplicationScoped
31+
* public A2AHttpClient myHttpClient(MyConfig config) {
32+
* return ...; // custom client
33+
* }
34+
* }</pre>
35+
*/
36+
public final class CdiA2AHttpClientProvider implements A2AHttpClientProvider {
37+
38+
@Override
39+
public A2AHttpClient create() {
40+
Instance<A2AHttpClient> instance = CDI.current().select(A2AHttpClient.class);
41+
if (!instance.isResolvable()) {
42+
throw new IllegalStateException("No unambiguous A2AHttpClient CDI bean found");
43+
}
44+
return instance.get();
45+
}
46+
47+
@Override
48+
public int priority() {
49+
return 200;
50+
}
51+
52+
@Override
53+
public String name() {
54+
return "cdi";
55+
}
56+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@NullMarked
2+
package org.a2aproject.sdk.client.http.cdi;
3+
4+
import org.jspecify.annotations.NullMarked;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.a2aproject.sdk.client.http.cdi.CdiA2AHttpClientProvider
Lines changed: 25 additions & 0 deletions

0 commit comments

Comments
 (0)