HTTP Client
UShip HTTP Client is based on java.net.http.HttpClient
.
It is configured with ExtendedHttpClientConfiguration
which enables to:
-
optionally provide a configured
HttpClient
instance - otherwise the default JVM one is used, -
optionally provide a set of `RequestListener`s which listen for requests.
Request Listeners
Request listener is a callback triggered before and after each request.
It enables to store data before the requests in the caller context and execute some callback once the request is finished.
To pass data between both steps (since the request can be asynchronous or not) it uses a State
which can hold any data the listener needs.
Tip
|
if you write custom listeners (to add OpenTracing capabilities for example), you can make them implement AutoCloseable and when closing the HTTP client the method will be called automatically.
|
Default Listeners
DefaultTimeout
This listener is pretty straight forward, if the request does not have a timeout, it sets it to the default one configured in the listener. It enables to enforce a global timeout to all requests.
SetUserAgent
This listener enforce a custom user-agent value. It defaults to chrome one.
ExchangeLogger
This listener enables to force all exchanges to be logged.
FilteringListener
This listener wraps another listener to filter the calls to before
/after
callbacks either based on the request or on the response.
Can be useful to ignore some data (for example to only capture errors in HARDumperListener
).
Tip
|
for ignoredPaths you can use the syntax regex:<java regex> to match more than an exact path at once.
|
HARDumperListener
This listener enables to capture a HAR
dump of all exchanges which went through the client.
It can be very useful to generate some test data you replay with a HAR server in a test or a demo environment.
It comes with its companion HARHttpClient
which enables to replay a HAR without actually doing the requests.
Tip
|
a sibling listener called NDJSONDumperListener exists and allows to log each entry in a ND-JSON output (better in case of error but not standard).
It can be combined to NDJSONHttpClient to replay captured requests (in order).
|
Important
|
the HttpClient companions of these "capture" listeners must be used in sequential order (until you know you can parallelize them all) because there is no matching logic as of today of requests/responses to enable a wider reuse of captures.
|
Sample usage
final var conf = new ExtendedHttpClientConfiguration();
// (optional) force a custom SSL context
conf.setDelegate(
HttpClient.newBuilder()
.sslContext(getSSLContext())
.build());
// (optional) force custom listeners
conf.setListeners(List.of(
new AutoTimeoutSetter(Duration.ofSeconds(3600)),
new AutoUserAgentSetter(),
new ExchangeLogger(
Logger.getLogger(getClass().getName()),
Clock.systemUTC(),
false)));
(Open) Tracing
Important
|
this is not in the same module, you must add tracing module to get this feature.
|
tracing
module provides a Tomcat valve you can set up on your web container to add tracing capabilities to your Tomcat:
final var listener = new TracingListener(
new ClientTracingConfiguration(), (1)
accumulator, (2)
new IdGenerator(IdGenerator.Type.HEX), (3)
new ServletContextAttributeEvaluator(servletRequest), (4)
systemUTC()); (5)
(6)
final var configuration = new ExtendedHttpClientConfiguration()
.setRequestListeners(List.of(listener));
final var client = new ExtendedHttpClient(configuration);
-
The configuration enables to customize the span tags and headers to enrich the request with,
-
The accumulator is what will send/log/… the spans once aggregated, ensure to configure it as needed,
-
The
IdGenerator
provides the span/trace identifiers, it must be compatible with your collector (hex
for zipkin for example), -
Actually a
Supplier<Span>
, the span evaluator enables to get parent span, here from therequest
in awebserver-tomcat
usingTracingvalve
but any evaluation will work, -
The clock enables to timestamp the span and compute its duration,
-
Finally, add the listener to your http client configuration and create your client.
Important
|
the accumulator should generally be closed if you reuse AccumulatingSpanCollector . You can combine it with ZipkinFlusher to flush to a zipkin collector v2.
|
Kubernetes client
kubernetes-client
modules providers a HTTP Client already configured for Kubernetes in cluster connection (from a POD).
This will typically be used from an operator or cloud native application to call Kubernetes API using a plain and very light HTTP Client from the JVM.
Tip
|
indeed you can combine it with the enhanced HTTP Client configuring it in the KubernetesClientConfiguration .
However, it is recommended to do it using setClientWrapper on the configuration and pass the automatically created client to ExtendedHttpClientConfiguration.setDelegate
to avoid to have to handle the SSLContext yourself.
|
Usage:
final var conf = new KubernetesClientConfiguration()
.setClientWrapper(client -> new ExtendedHttpClient(new ExtendedHttpClientConfiguration()
.setDelegate(client)));
final var k8s = new KubernetesClient(conf);
// now call any API you need:
final var response = k8s.send(
HttpRequest.newBuilder()
.GET()
.uri(URI.create(
"https://kubernetes.api/api/v1/namespaces/" + k8s.namespace().orElse("default") + "/configmaps?" +
"includeUninitialized=false&" +
"limit=1000&" +
"timeoutSeconds=600")
.header("Accept", "application/json")
.build(),
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
// handle the response
Important
|
as you can see, there is no need to pass the token to the request, it is done under the hood by the KubernetesClient .
The other important note is that https://kubernetes.api is automatically replaced by the conf.getMaster() value.
This enables your code to stay more straight forward in general but if you pass them, the client will handle it properly too.
|
JSON-RPC client
As a JSON-RPC server companion UShip also provides a JSON-RPC client.
<dependency>
<groupId>io.yupiik.uship</groupId>
<artifactId>jsonrpc-client</artifactId>
<version>${uship.version}</version>
</dependency>
The minimum configuration is to provide the JSON-RPC endpoint:
final var client = new JsonRpcClient(new JsonRpcClientConfiguration("http://app.service.com/jsonrpc"));
But more is customizable in JsonRpcClientConfiguration
and a common initialization would look like:
@Produces
@ApplicationScoped
public ExtendedHttpClient jsonRpcHttpClient() {
return ...; // create it as usual
}
public void releaseHttpClient(@Disposes final ExtendedHttpClient client) {
client.close();
}
@Produces
@ApplicationScoped
public JsonRpcClient jsonRpcClient(
// external config
@ConfigProperty("jsonrpc.base") final String base,
@ConfigProperty("jsonrpc.authorizationheader") final String auth,
// from uship json in general
final JsonBuilderFactory jbf, final Jsonb jsonb,
// created in the app (other producer)
final ExtendedHttpClient client) {
return new JsonRpcClient(new JsonRpcClientConfiguration(base + "/jsonrpc"))
// all these setters are optional but enables to use controlled instances (vs implicit) and optimize/customize the behavior/mem/security
.setJsonBuilderFactory(jbf)
.setJsonb(jsonb)
.setHttpClient(client) // enables to customize the async thread pool for example (highly encourage for executeAsync usage)
.setRequestCustomizer(req -> req.header("authorization", auth));
}
@Produces
@ApplicationScoped // to be able to @Inject JsonRpcClient.Sync sync; directory - or similarly for async
public JsonRpcClient.Sync jsonRpcClient(final JsonRpcClient root) {
return root.sync();
}
public void releaseJsonRpcClient(@Disposes final JsonRpcClient client) {
client.close();
}
Then you can simply use it in your application:
@ApplicationScoped
public class MyService {
@Inject
private JsonRpcClient client;
public CompletionStage<Customer> findCustomer(final String id) {
return client.async().execute("app-customer-find-by-id", Map.of("id", id))
// todo: better error handling if needed
.thenApply(r -> r.asSingle().as(Customer.class));
}
}
Bulk handling
Bulk is handled relying on the JSON-RPC protocol accessible from the client - or directly if you prefer:
final var protocol = client.protocol();
final var response = client.sync().execute(protocol.jsonBuilderFactory().createArrayBuilder()
.add(protocol.toJsonRpcRequest("m1", Map.of("foo", "bar")))
.add(protocol.toJsonRpcRequest("m2", new M2Params()))
.add(protocol.toJsonRpcRequest("m3", null))
.build());