avaje / avaje-http-client

A light weight wrapper to the JDK HttpClient

Home Page:https://avaje.io/http-client/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

MOVED !!!

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


Build Maven Central License

avaje-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

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-client</artifactId>
  <version>${avaje.client.version}</version>
</dependency>

Create HttpClientContext

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();
  }

Requests

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

Limitations:

  • No support for POSTing multipart-form currently
  • Retry (when specified) does not apply to async response processing`

JDK HttpClient

Example GET as String

HttpResponse<String> hres = clientContext.request()
  .path("hello")
  .GET()
  .asString();

Example GET as JSON marshalling into a java class/dto

CustomerDto customer = clientContext.request()
  .path("customers").path(42)
  .GET()
  .bean(CustomerDto.class);

Example Async GET as String

  • 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 responses

Overview of response types for sync calls.

sync processing 
asVoidHttpResponse<Void>
asStringHttpResponse<String>
bean<E>E
list<E>List<E>
stream<E>Stream<E>
handler(HttpResponse.BodyHandler<E>)HttpResponse<E>
  
async processing 
asVoidCompletableFuture<HttpResponse<Void>>
asStringCompletableFuture<HttpResponse<String>>
bean<E>CompletableFuture<E>
list<E>CompletableFuture<List<E>>
stream<E>CompletableFuture<Stream<E>>
handler(HttpResponse.BodyHandler<E>)CompletableFuture<HttpResponse<E>>

HttpResponse BodyHandlers

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

Overview of Request body

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

Examples

GET as String

HttpResponse<String> hres = clientContext.request()
  .path("hello")
  .GET()
  .asString();

Async GET as String

  • 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();
       ...
     }
   });

GET as json to single bean

Customer customer = clientContext.request()
  .path("customers").path(42)
  .GET()
  .bean(Customer.class);

GET as json to a list of beans

List<Customer> list = clientContext.request()
  .path("customers")
  .GET()
  .list(Customer.class);

GET as application/x-json-stream as a stream of beans

Stream<Customer> stream = clientContext.request()
  .path("customers/all")
  .GET()
  .stream(Customer.class);

POST a bean as json request body

Hello bean = new Hello(42, "rob", "other");

HttpResponse<Void> res = clientContext.request()
  .path("hello")
  .body(bean)
  .POST()
  .asDiscarding();

assertThat(res.statusCode()).isEqualTo(201);

Path

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();

MatrixParam

HttpResponse<String> httpRes = clientContext.request()
  .path("books")
  .matrixParam("author", "rob")
  .matrixParam("country", "nz")
  .path("foo")
  .matrixParam("extra", "banana")
  .GET()
  .asString();

QueryParam

List<Product> beans = clientContext.request()
  .path("products")
  .queryParam("sortBy", "name")
  .queryParam("maxCount", "100")
  .GET()
  .list(Product.class);

FormParam

HttpResponse<Void> res = clientContext.request()
  .path("register/user")
  .formParam("name", "Bazz")
  .formParam("email", "user@foo.com")
  .formParam("url", "http://foo.com")
  .formParam("startDate", "2020-12-03")
  .POST()
  .asDiscarding();

assertThat(res.statusCode()).isEqualTo(201);

Retry (Sync Requests Only)

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;
  }

Async processing

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 
asVoidCompletableFuture<HttpResponse<Void>>
asStringCompletableFuture<HttpResponse<String>>
bean<E>CompletableFuture<E>
list<E>CompletableFuture<List<E>>
stream<E>CompletableFuture<Stream<E>>
handler(HttpResponse.BodyHandler<E>)CompletableFuture<HttpResponse<E>>

.async().asDiscarding() - HttpResponse<Void>

clientContext.request()
   .path("hello/world")
   .GET()
   .async().asDiscarding()
   .whenComplete((hres, throwable) -> {

     if (throwable != null) {
       ...
     } else {
       int statusCode = hres.statusCode();
       ...
     }
   });

.async().asString() - HttpResponse<String>

clientContext.request()
   .path("hello/world")
   .GET()
   .async().asString()
   .whenComplete((hres, throwable) -> {

     if (throwable != null) {
       ...
     } else {
       int statusCode = hres.statusCode();
       String body = hres.body();
       ...
     }
   });

.async().bean(HelloDto.class)

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
       ...
     }
   });

.async().handler(...) - Any Response.BodyHandler implementation

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();
     ...
   });

HttpCall

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) -> {
    ...
  });

BasicAuthIntercept - Authorization Basic / Basic Auth

We can use BasicAuthIntercept to intercept all requests adding a Authorization: Basic ... header ("Basic Auth").

Example
HttpClientContext clientContext =
   HttpClientContext.builder()
     .baseUrl(baseUrl)
     ...
     .requestIntercept(new BasicAuthIntercept("myUsername", "myPassword"))  <!-- HERE
     .build();

AuthTokenProvider - Authorization Bearer token

For Authorization using Bearer tokens that are obtained and expire, implement AuthTokenProvider and register that when building the HttpClientContext.

1. Implement AuthTokenProvider

  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);
    }
  }

2. Register with HttpClientContext

    HttpClientContext ctx = HttpClientContext.builder()
      .baseUrl("https://foo")
      ...
      .authTokenProvider(new MyAuthTokenProvider()) <!-- HERE
      .build();

3. Token obtained and set automatically

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.

10K requests - Loom vs Async

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) -> {
    ...
  });

10K requests using Async and reactive streams

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();

10K requests using Loom

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)

About

A light weight wrapper to the JDK HttpClient

https://avaje.io/http-client/

License:Apache License 2.0


Languages

Language:Java 100.0%