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
We can see that |
On
- right-mouse-click |
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