A1. Add dependencies

Add avaje-http-client and jackson-databind as a dependencies.

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

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.3</version>
</dependency>
Gradle
dependencies {
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
  implementation 'io.avaje:avaje-http-client:1.20'
  ...
}

A2. Create HttpClient

We will use https://api.github.com as the base URL and use JacksonBodyAdapter to decode the json response.

Often we want to use a specific Jackson ObjectMapper and pass that to JacksonBodyAdapter. The default constructor for JacksonBodyAdapter uses an ObjectMapper with reasonable default options.

final HttpClient client = HttpClient.builder()
  .baseUrl("https://api.github.com")
  .bodyAdapter(new JacksonBodyAdapter())
  .build();

A3. Add "record/data" class Contributor

We will use this to hold the response json results.

public static class Contributor {
  // just using public fields to get going ...
  public String login;
  public int contributions;
}

A4. Make the request

The below used avaje as the organisation and avaje-http as the repo.

final List<Contributor> contributors = client.request()
  .path("repos/avaje/avaje-http/contributors")
  .GET()
  .list(Contributor.class);

for (Contributor contributor : contributors) {
  System.out.println(contributor.login + " contributions:" + contributor.contributions);
}

All the code

All the code from the prior steps combined is:

package org.example;

import io.avaje.http.client.HttpClient;
import io.avaje.http.client.JacksonBodyAdapter;

import java.util.List;

public class Main {

  public static class Contributor {
    public String login;
    public int contributions;
  }

  public static void main(String[] args) {

    final HttpClient client = HttpClient.builder()
      .baseUrl("https://api.github.com")
      .bodyAdapter(new JacksonBodyAdapter())
      .build();

    final List<Contributor> contributors = client.request()
      .path("repos/avaje/avaje-http/contributors")
      .GET()
      .list(Contributor.class);

    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " contributions:" + contributor.contributions);
    }
  }
}

Client API start

B1. Add dependencies

In addition to avaje-http-client and jackson-databind we need to add avaje-http-api and avaje-http-client-generator as dependencies.

Note that avaje-http-client-generator is the annotation processor that generates the implementation for our Client API.

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

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.3</version>
</dependency>

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

<!-- Annotation processor -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-http-client-generator</artifactId>
  <version>${avaje.http.version}</version>
  <scope>provided</scope>
</dependency>

JDK 23+ note:

In JDK 23+, annotation processors are disabled by default, so we need to add a compiler property to re-enable.

<properties>
  <maven.compiler.proc>full</maven.compiler.proc>
</properties>

Gradle

dependencies {
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.3'
    implementation 'io.avaje:avaje-http-client:1.20'

    implementation 'io.avaje:avaje-http-api:1.20'
    annotationProcessor 'io.avaje:avaje-http-client-generator:1.20'
    ...
}

B2. Create interface

Create a package org.example and create an interface called Github in that package like below:

package org.example;

import io.avaje.http.api.Client;
import io.avaje.http.api.Get;

import java.util.List;

@Client
public interface Github {

  @Get("/repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(String owner, String repo);


  class Contributor {
    public String login;
    public int contributions;
  }
}

@Client

The interface marked with @Client it will be picked up by the avaje-http-client-generator annotation processor which will generate the implementation for the interface.

B3. IntelliJ IDEA

When using IntelliJ we can check if target / generated-sources / annotations is marked as a generated source.

Here target / generated-sources / annotations is currently not treated as a generated source (still orange)

We can see that Github$HttpClient.java has been generated but IntelliJ isn't aware of it as generated source yet.

 

On target / generated-sources / annotations

- right-mouse-click
- Mark Directory as
- Generated Sources Root

 

target / generated-sources / annotations is now marked as generated source

In IntelliJ IDE we will now be able to navigate from the interface to the implementation.

We can change the interface and recompile and the implementation will be regenerated to suit.

B4. Generated source

For the Github interface there would be generated source for Github$HttpClient found in target / generated-sources / annotations.

The generated source will look like:

package org.example.httpclient;

import io.avaje.http.api.*;
import io.avaje.http.client.HttpClient;
import java.util.List;
import org.example.Github;
import org.example.Github.Contributor;

@Generated("avaje-http-client-generator")
public class Github$HttpClient implements Github {

  private final HttpClient clientContext;

  public Github$HttpClient(HttpClient ctx) {
    this.clientContext = ctx;
  }

  // GET /repos/{owner}/{repo}/contributors
  @Override
  public List<Contributor> contributors(String owner, String repo) {
    return clientContext.request()
      .path("repos").path(owner).path(repo).path("contributors")
      .GET()
      .list(Contributor.class);
  }

}

B5. Use the interface

We can obtain the implementation via HttpClient create(T.class) method.

final Github github = httpClient.create(Github.class);

The full code example is below.

package org.example;

import io.avaje.http.client.HttpClient;
import io.avaje.http.client.JacksonBodyAdapter;

import java.util.List;

public class ApiMain {

  public static void main(String[] args) {

    final HttpClient httpClient = HttpClient.builder()
      .baseUrl("https://api.github.com")
      .bodyAdapter(new JacksonBodyAdapter())
      .build();

    // obtain API ...
    final Github github = httpClient.create(Github.class);

    // use the API ...
    final List<Github.Contributor> contributors = github.contributors("avaje", "avaje-http");
    for (Github.Contributor contributor : contributors) {
      System.out.println(contributor.login+" contributions: "+contributor.contributions);
    }

  }
}

<h2 id="jpms">Java Module Setup</h2>
<p>
  If using java modules, in the <code>module-info.java</code> we need to:
</p>
<ol>
  <li>Add a <em>requires</em> clause for <em>io.avaje.http.api</em></li>
  <li>Add a <em>requires</em> clause for <em>io.avaje.http.client</em></li>
  <li>Add a <em>provides</em> clause for <em>io.avaje.http.client.HttpClient.GeneratedComponent</em></li>
</ol>

<h5>Example module-info</h5>
<pre content="java">
import io.avaje.http.client.HttpClient.GeneratedComponent
module org.example {

  requires io.avaje.http.api;
  requires io.avaje.http.client
  // you must define the fully qualified class name of the generated classes. if you use an import statement, compilation will fail
  provides GeneratedComponent with org.example.GeneratedHttpComponent;
}

In the example above, org.example.GeneratedHttpComponent is generated code typically found in target/generated-sources/annotations.

Logging

By default request response logging is built in (via RequestLogger) and we enable it via setting the log level to DEBUG or TRACE for io.avaje.http.client.RequestLogger

Example: Logback
<logger name="io.avaje.http.client.RequestLogger" level="trace"/>

DEBUG

Summary logging that includes the response status code and execution time is logged at DEBUG level.

TRACE

Logging that includes the request and response headers plus the body content is logged at TRACE level.

DI Integrations

Annotations placed on a client interface are copied to the generated client implementation, so placing a DI annotation on the interface will make Avaje Inject generate DI classes.

@Client
@Singleton
public interface Github {

  @AspectAnnotation
  @Get("/repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(String owner, String repo);
}

The generated source will look like:

@Generated("avaje-http-client-generator")
@Singleton // now DI frameworks can autowire
public class Github$HttpClient implements Github {

  private final HttpClient clientContext;

  public Github$HttpClient(HttpClient ctx) {
    this.clientContext = ctx;
  }

  // GET /repos/{owner}/{repo}/contributors
  @AspectAnnotation // AOP frameworks can now proxy this method.
  @Override
  public List<Contributor> contributors(String owner, String repo) {
    return clientContext.request()
      .path("repos").path(owner).path(repo).path("contributors")
      .GET()
      .list(Contributor.class);
  }

}

Next

Back to the main documentation