This project has been moved to be a sub-module of avaje-http.
It is now at: https://github.com/avaje/avaje-http/tree/master/http-client
Documentation at avaje.io/http-client
A lightweight wrapper to the JDK 11+ Java Http Client
- Use Java 11.0.8 or higher (some SSL related bugs prior to 11.0.8 with JDK HttpClient)
- Adds a fluid API for building URL and payload
- Adds JSON marshalling/unmarshalling of request and response using Jackson or Gson
- Gzip encoding/decoding
- Logging of request/response logging
- Interception of request/response
- Built in support for authorization via Basic Auth and Bearer Token
- Provides async and sync API
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-http-client</artifactId>
<version>${avaje.client.version}</version>
</dependency>
Create a HttpClientContext with a baseUrl, Jackson or Gson based JSON body adapter, logger.
public HttpClientContext client() {
return HttpClientContext.builder()
.baseUrl(baseUrl)
.bodyAdapter(new JsonbBodyAdapter())
//.bodyAdapter(new JacksonBodyAdapter(new ObjectMapper()))
//.bodyAdapter(new GsonBodyAdapter(new Gson()))
.build();
}
From HttpClientContext:
-
Create a request
-
Build the url via path(), matrixParam(), queryParam()
-
Optionally set headers(), cookies() etc
-
Optionally specify a request body (JSON, form, or any JDK BodyPublisher)
-
Http verbs - GET(), POST(), PUT(), PATCH(), DELETE(), HEAD(), TRACE()
-
Sync processing response body as:
- a bean, list of beans, stream of beans, String, Void or any JDK Response.BodyHandler
-
Async processing of the request using CompletableFuture
- a bean, list of beans, stream of beans, String, Void or any JDK Response.BodyHandler
- No support for POSTing multipart-form currently
- Retry (when specified) does not apply to
async
response processing`
-
Introduction to JDK HttpClient at JDK HttpClient Introduction
-
Javadoc for JDK HttpClient
HttpResponse<String> hres = clientContext.request()
.path("hello")
.GET()
.asString();
CustomerDto customer = clientContext.request()
.path("customers").path(42)
.GET()
.bean(CustomerDto.class);
- All async requests use CompletableFuture<T>
- throwable is a CompletionException
- In the example below hres is of type HttpResponse<String>
clientContext.request()
.path("hello")
.GET()
.async().asString() // CompletableFuture<HttpResponse<String>>
.whenComplete((hres, throwable) -> {
if (throwable != null) {
// CompletionException
...
} else {
// HttpResponse<String>
int statusCode = hres.statusCode();
String body = hres.body();
...
}
});
Overview of response types for sync calls.
sync processing | |
asVoid | HttpResponse<Void> |
asString | HttpResponse<String> |
bean<E> | E |
list<E> | List<E> |
stream<E> | Stream<E> |
handler(HttpResponse.BodyHandler<E>) | HttpResponse<E> |
async processing | |
asVoid | CompletableFuture<HttpResponse<Void>> |
asString | CompletableFuture<HttpResponse<String>> |
bean<E> | CompletableFuture<E> |
list<E> | CompletableFuture<List<E>> |
stream<E> | CompletableFuture<Stream<E>> |
handler(HttpResponse.BodyHandler<E>) | CompletableFuture<HttpResponse<E>> |
JDK HttpClient provides a number of BodyHandlers
including reactive Flow based subscribers. With the handler()
method we can use any of these or our own HttpResponse.BodyHandler
implementation.
Refer to HttpResponse.BodyHandlers
discarding() | Discards the response body |
ofByteArray() | byte[] |
ofString() | String, additional charset option |
ofLines() | Stream<String> |
ofInputStream() | InputStream |
ofFile(Path file) | Path with various options |
ofByteArrayConsumer(...) | |
fromSubscriber(...) | various options |
fromLineSubscriber(...) | various options |
When sending body content we can use:
- Object which is written as JSON content by default
- byte[], String, Path (file), InputStream
- formParams() for url encoded form (
application/x-www-form-urlencoded
) - Any HttpRequest.BodyPublisher
HttpResponse<String> hres = clientContext.request()
.path("hello")
.GET()
.asString();
- All async requests use JDK httpClient.sendAsync(...) returning CompletableFuture<T>
- throwable is a CompletionException
- In the example below hres is of type HttpResponse<String>
clientContext.request()
.path("hello")
.GET()
.async().asString()
.whenComplete((hres, throwable) -> {
if (throwable != null) {
// CompletionException
...
} else {
// HttpResponse<String>
int statusCode = hres.statusCode();
String body = hres.body();
...
}
});
Customer customer = clientContext.request()
.path("customers").path(42)
.GET()
.bean(Customer.class);
List<Customer> list = clientContext.request()
.path("customers")
.GET()
.list(Customer.class);
Stream<Customer> stream = clientContext.request()
.path("customers/all")
.GET()
.stream(Customer.class);
Hello bean = new Hello(42, "rob", "other");
HttpResponse<Void> res = clientContext.request()
.path("hello")
.body(bean)
.POST()
.asDiscarding();
assertThat(res.statusCode()).isEqualTo(201);
Multiple calls to path()
append with a /
. This is make it easier to build a path
programmatically and also build paths that include matrixParam()
HttpResponse<String> res = clientContext.request()
.path("customers")
.path("42")
.path("contacts")
.GET()
.asString();
// is the same as ...
HttpResponse<String> res = clientContext.request()
.path("customers/42/contacts")
.GET()
.asString();
HttpResponse<String> httpRes = clientContext.request()
.path("books")
.matrixParam("author", "rob")
.matrixParam("country", "nz")
.path("foo")
.matrixParam("extra", "banana")
.GET()
.asString();
List<Product> beans = clientContext.request()
.path("products")
.queryParam("sortBy", "name")
.queryParam("maxCount", "100")
.GET()
.list(Product.class);
HttpResponse<Void> res = clientContext.request()
.path("register/user")
.formParam("name", "Bazz")
.formParam("email", "[email protected]")
.formParam("url", "http://foo.com")
.formParam("startDate", "2020-12-03")
.POST()
.asDiscarding();
assertThat(res.statusCode()).isEqualTo(201);
To add Retry funtionality, use .retryHandler(yourhandler)
on the builder to provide your retry handler. The RetryHandler
interface provides two methods, one for status exceptions (e.g. you get a 4xx/5xx from the server) and another for exceptions thrown by the underlying client (e.g. server times out or client couldn't send request). Here is example implementation of RetryHandler
.
public final class ExampleRetry implements RetryHandler {
private static final int MAX_RETRIES = 2;
@Override
public boolean isRetry(int retryCount, HttpResponse<?> response) {
final var code = response.statusCode();
if (retryCount >= MAX_RETRIES || code <= 400) {
return false;
}
return true;
}
@Override
public boolean isExceptionRetry(int retryCount, HttpException response) {
//unwrap the exception
final var cause = response.getCause();
if (retryCount >= MAX_RETRIES) {
return false;
}
if (cause instanceof ConnectException) {
return true;
}
return false;
}
All async requests use JDK httpClient.sendAsync(...) returning CompletableFuture. Commonly the
whenComplete()
callback will be used to process the async responses.
The bean()
, list()
and stream()
responses throw a HttpException
if the status code >= 300
(noting that by default redirects are followed apart for HTTPS to HTTP).
async processing | |
asVoid | CompletableFuture<HttpResponse<Void>> |
asString | CompletableFuture<HttpResponse<String>> |
bean<E> | CompletableFuture<E> |
list<E> | CompletableFuture<List<E>> |
stream<E> | CompletableFuture<Stream<E>> |
handler(HttpResponse.BodyHandler<E>) | CompletableFuture<HttpResponse<E>> |
clientContext.request()
.path("hello/world")
.GET()
.async().asDiscarding()
.whenComplete((hres, throwable) -> {
if (throwable != null) {
...
} else {
int statusCode = hres.statusCode();
...
}
});
clientContext.request()
.path("hello/world")
.GET()
.async().asString()
.whenComplete((hres, throwable) -> {
if (throwable != null) {
...
} else {
int statusCode = hres.statusCode();
String body = hres.body();
...
}
});
clientContext.request()
...
.POST().async()
.bean(HelloDto.class)
.whenComplete((helloDto, throwable) -> {
if (throwable != null) {
HttpException httpException = (HttpException) throwable.getCause();
int statusCode = httpException.getStatusCode();
// maybe convert json error response body to a bean (using Jackson/Gson)
MyErrorBean errorResponse = httpException.bean(MyErrorBean.class);
..
} else {
// use helloDto
...
}
});
The example below is a line subscriber processing response content line by line.
CompletableFuture<HttpResponse<Void>> future = clientContext.request()
.path("hello/lineStream")
.GET().async()
.handler(HttpResponse.BodyHandlers.fromLineSubscriber(new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(String item) {
// process the line of response content
...
}
@Override
public void onError(Throwable throwable) {
...
}
@Override
public void onComplete() {
...
}
}))
.whenComplete((hres, throwable) -> {
int statusCode = hres.statusCode();
...
});
If we are creating an API and want the client code to choose to execute
the request asynchronously or synchronously then we can use call()
.
The client can then choose to execute()
the request synchronously or
choose async()
to execute the request asynchronously.
HttpCall<List<Customer>> call =
clientContext.request()
.path("customers")
.GET()
.call().list(Customer.class);
// Either execute synchronously
List<Customer> customers = call.execute();
// Or execute asynchronously
call.async()
.whenComplete((customers, throwable) -> {
...
});
We can use BasicAuthIntercept
to intercept all requests adding a Authorization: Basic ...
header ("Basic Auth").
HttpClientContext clientContext =
HttpClientContext.builder()
.baseUrl(baseUrl)
...
.requestIntercept(new BasicAuthIntercept("myUsername", "myPassword")) <!-- HERE
.build();
For Authorization using Bearer
tokens that are obtained and expire, implement AuthTokenProvider
and register that when building the HttpClientContext.
class MyAuthTokenProvider implements AuthTokenProvider {
@Override
public AuthToken obtainToken(HttpClientRequest tokenRequest) {
AuthTokenResponse res = tokenRequest
.url("https://foo/v2/token")
.header("content-type", "application/json")
.body(authRequestAsJson())
.POST()
.bean(AuthTokenResponse.class);
Instant validUntil = Instant.now().plusSeconds(res.expires_in).minusSeconds(60);
return AuthToken.of(res.access_token, validUntil);
}
}
HttpClientContext ctx = HttpClientContext.builder()
.baseUrl("https://foo")
...
.authTokenProvider(new MyAuthTokenProvider()) <!-- HERE
.build();
All requests using the HttpClientContext will automatically get
an Authorization
header with Bearer
token added. The token will be
obtained for initial request and then renewed when the token has expired.
The following is a very quick and rough comparison of running 10,000 requests
using Async
vs Loom
.
The intention is to test the thought that in a "future Loom world" the
desire to use async()
execution with HttpClient reduces.
TLDR: Caveat, caveat, more caveats ... initial testing shows Loom to be just a touch faster (~10%) than async.
To run my tests I use Jex as the server (Jetty based) and have it running using Loom. For whatever testing you do you will need a server that can handle a very large number of concurrent requests.
The Loom blocking request (make 10K of these)
HttpResponse<String> hres = httpClient.request()
.path("s200")
.GET()
.asString();
The equivalent async request (make 10K of these joining the CompletableFuture's).
CompletableFuture<HttpResponse<String>> future = httpClient.request()
.path("s200")
.GET()
.async()
.asString()
.whenComplete((hres, throwable) -> {
...
});
Use .async()
to execute the requests which internally is using JDK
HttpClient's reactive streams. The whenComplete()
callback is invoked
when the response is ready. Collect all the resulting CompletableFuture
and wait for them all to complete.
Outline:
// Collect all the CompletableFuture's
List<CompletableFuture<HttpResponse<String>>> futures = new ArrayList<>();
for (int i = 0; i < 10_000; i++) {
futures.add(httpClient.request().path("s200")
.GET()
.async().asString()
.whenComplete((hres, throwable) -> {
// confirm 200 response etc
...
}));
}
// wait for all requests to complete via join() ...
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
With Loom Java 17 EA Release we can use Executors.newVirtualThreadExecutor()
to return an ExecutorService that uses Loom Virtual Threads. These are backed
by "Carrier threads" (via ForkedJoinPool).
Outline:
// use Loom's Executors.newVirtualThreadExecutor()
try (ExecutorService executorService = Executors.newVirtualThreadExecutor()) {
for (int i = 0; i < 10_000; i++) {
executorService.submit(this::task);
}
}
private void task() {
HttpResponse<String> hres =
httpClient.request().path("s200")
.GET()
.asString();
// confirm 200 response etc
...
}
Caveat: Proper performance benchmarks are really hard and take a lot of effort.
Running some "rough/approx performance comparison tests" using Loom
build 17 EA 2021-09-14 / (build 17-loom+7-342)
vs Async
for my environment
and 10K request scenarios has loom execution around 10% faster than async.
It looks like Loom and Async run in pretty much the same time although it currently looks that Loom is just a touch faster (perhaps due to how it does park/unpark). More investigation required.
Date: 2021-06
Build: 17 EA 2021-09-14 / (build 17-loom+7-342)
.
openjdk version "17-loom" 2021-09-14
OpenJDK Runtime Environment (build 17-loom+7-342)
OpenJDK 64-Bit Server VM (build 17-loom+7-342, mixed mode, sharing)