Micronaut HTTP Client

Learn how to use Micronaut low-level HTTP Client. Simplify your code with the declarative HTTP client.

Authors: Sergio del Amo

Micronaut Version: 1.0.0

1 Getting Started

In this guide we are going to create a Micronaut app written in Java to consume the Bintray API with the Micronaut HTTP Client.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the App

Create an app using the Micronaut Command Line Interface.

mn create-app example.micronaut.complete

The previous command createas a micronaut app with the default package example.micronaut in a folder named complete.

By default, create-app generates a Java Micronaut app and it uses Gradle build system. However, you could use other build tool such as Maven or other programming languages such as Groovy or Kotlin.

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

3 Bintray API

In this guide, you are going to consume the Bintray API from a Micronaut application.

In particular, we consume the Get Packages endpoint.

Get a list of packages in the specified repository, optionally specify a starting position and/or a name prefix filter.

This API resource can be consumed by both authenticated and anonymous clients.

Initially, you will consume it anonymously, later we will discuss authentication.

Modify src/main/resources/application.yml to create some configuration parameters.

src/main/resources/application.yml
bintray:
    organization: micronaut
    repository: profiles
    apiversion: v1

To encapsulate type-safe configuration retrieval, we use a @ConfigurationProperties object:

src/main/java/example/micronaut/BintrayConfiguration.java
package example.micronaut;

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Requires;

import java.util.HashMap;
import java.util.Map;

@ConfigurationProperties(BintrayConfiguration.PREFIX)
@Requires(property = BintrayConfiguration.PREFIX)
public class BintrayConfiguration {

    public static final String PREFIX = "bintray";
    public static final String BINTRAY_API_URL = "https://bintray.com";

    private String apiversion;

    private String organization;

    private String repository;

    private String username;

    private String token;

    public String getApiversion() {
        return apiversion;
    }

    public void setApiversion(String apiversion) {
        this.apiversion = apiversion;
    }

    public String getOrganization() {
        return organization;
    }

    public void setOrganization(String organization) {
        this.organization = organization;
    }

    public String getRepository() {
        return repository;
    }

    public void setRepository(String repository) {
        this.repository = repository;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public Map<String, Object> toMap() {
        Map<String, Object> m = new HashMap<>();
        m.put("apiversion", getApiversion());
        m.put("organization", getOrganization());
        m.put("repository", getRepository());
        m.put("username", getUsername());
        m.put("token", getToken());
        return m;
    }
}

In this guide, you are going to fetch Micronaut profiles packages. Those packages are used by the Micronaut CLI.

To consume the Bintray API, you will use Micronaut HTTP Client.

4 Low Level Client

Initially, you will create a Bean which uses the low-level Client API.

Create a POJO to parse the JSON response into a Java object:

src/main/java/example/micronaut/BintrayPackage.java
package example.micronaut;

public class BintrayPackage {
    String name;
    boolean linked;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isLinked() {
        return linked;
    }

    public void setLinked(boolean linked) {
        this.linked = linked;
    }
}

Create BintrayLowLevelClient:

src/main/java/example/micronaut/BintrayLowLevelClient.java
package example.micronaut;

import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.uri.UriTemplate;
import io.reactivex.Flowable;
import io.reactivex.Maybe;

import javax.inject.Singleton;
import java.util.List;

@Singleton (1)
public class BintrayLowLevelClient {


    private final RxHttpClient httpClient;
    private final String uri;

    public BintrayLowLevelClient(@Client(BintrayConfiguration.BINTRAY_API_URL) RxHttpClient httpClient,  (2)
                                 BintrayConfiguration configuration) {  (3)
        this.httpClient = httpClient;
        String path = "/api/{apiversion}/repos/{organization}/{repository}/packages";
        uri = UriTemplate.of(path).expand(configuration.toMap());
    }

    Maybe<List<BintrayPackage>> fetchPackages() {
        HttpRequest<?> req = HttpRequest.GET(uri);  (4)
        Flowable flowable = httpClient.retrieve(req, Argument.of(List.class, BintrayPackage.class)); (5)
        return (Maybe<List<BintrayPackage>>) flowable.firstElement(); (6)
    }

}
1 Use javax.inject.Singleton to designate a class a a singleton.
2 Inject RxClient via constructor injection
3 Inject the previously defined configuration parameters.
4 Creating HTTP Requests is easy thanks to Micronaut’s fluid API.
5 Use retrieve to perform an HTTP request for the given request object and convert the full HTTP response’s body into the specified type. e.g. List<BintrayPackage>.
6 The retrieve method returns a Flowable which has a firstElement method that returns the first emitted item or nothing
Instead of retrieve we could have used jsonStream. You can use jsonStream() to stream arrays of type application/json or JSON streams of type application/x-json-stream. If we use retrieve, such as in the previous code listing, the operation will not block. However, it will not return until all the data has been received from the server. In the case of a JSON array that would be the whole array. However, if you are interested in just the first element of the array, jsonStream provides a better alternative since it starts streaming data from the server without needing the whole response. For example, jsonStream().firstElement() will only parse the first item in a JSON array. Hence it is more efficient.

5 Declarative Client

It is time to take a look at Micronaut’s support for declarative clients via the Client annotation.

Create BintrayClient which clearly illustrates how a declarative Micronaut HTTP Client, which is generated at compile-time, simplifies our code.

src/main/java/example/micronaut/BintrayClient.java
package example.micronaut;

import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;

@Client(BintrayConfiguration.BINTRAY_API_URL) (1)
public interface BintrayClient {

    @Get("/api/${bintray.apiversion}/repos/${bintray.organization}/${bintray.repository}/packages")  (2)
    Flowable<BintrayPackage> fetchPackages(); (3)
}
1 URL of the remote service
2 You can use configuration parameter interpolation when you define the path of the GET endpoint.
3 You can return reactive types, such as an RxJava Flowable.

6 Controller

Create a Controller. It uses both (low-level and declarative clients). It showcases several Micronaut’s capabilities.

  • Micronaut supports any framework that implements Reactive Streams, including RxJava, and Reactor. Thus, you can easily and efficiently compose multiple HTTP client calls without blocking (which will limit the throughput and scalability of your application).

  • Micronaut enables you to consume/produce JSON Streams.

src/main/java/example/micronaut/BintrayController.java
package example.micronaut;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.reactivex.Flowable;
import io.reactivex.Maybe;

import java.util.List;

@Controller("/bintray") (1)
public class BintrayController {

    private final BintrayLowLevelClient bintrayLowLevelClient;

    private final BintrayClient bintrayClient;

    public BintrayController(BintrayLowLevelClient bintrayLowLevelClient, (2)
                             BintrayClient bintrayClient) {
        this.bintrayLowLevelClient = bintrayLowLevelClient;
        this.bintrayClient = bintrayClient;
    }

    @Get("/packages-lowlevel") (3)
    Maybe<List<BintrayPackage>> packagesWithLowLevelClient() { (4)
        return bintrayLowLevelClient.fetchPackages();
    }

    @Get(uri = "/packages", produces = MediaType.APPLICATION_JSON_STREAM)  (5)
    Flowable<BintrayPackage> packages() { (6)
        return bintrayClient.fetchPackages();
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /bintray
2 Inject beans via constructor injection.
3 The @Get annotation is used to map the index method to all requests that use an HTTP GET
4 The packagesWithLowLevelClient returns a Maybe which may or may not emit an item. If an item is not emitted a 404 is returned.
5 In order to do JSON streaming you can declare a controller method that returns a application/x-json-stream of JSON objects.
6 You can return reactive types, such as an RxJava Flowable.

7 Test

Micronaut is test framework agnostic. You can use JUnit, Spock Framework or Spek.

In this Guide, we test the app with Spock Framework.

We need to modify build.gradle.

Replace apply plugin: 'java' with apply plugin: 'groovy' and add the necessary dependencies:

build.gradle
dependencies {
...
..
    testCompile "org.spockframework:spock-core:1.1-groovy-2.4", {
        exclude module:'groovy-all'
    }
}

Edit micronaut-cli.yml to set Spock as the test framework:

micronaut-cli.yml
profile: service
defaultPackage: example.micronaut
---
testFramework: spock
sourceLanguage: java

Create a test which verifies both clients work as expected and the controller echoes the output of the Bintray API in a Reactive way.

src/test/groovy/example/micronaut/BintrayControllerSpec.groovy
package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.RxStreamingHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import io.reactivex.Flowable
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class BintrayControllerSpec extends Specification {

    @Shared
    @AutoCleanup (1)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) (2)

    @Shared
    @AutoCleanup
    RxStreamingHttpClient client = embeddedServer.applicationContext.createBean(RxStreamingHttpClient, embeddedServer.getURL()) (3)

    @Shared
    List<String> expectedProfileNames = ['base', 'federation', 'function', 'function-aws', 'service']

    def "Verify bintray packages can be fetched with low level HttpClient"() {
        when:
        HttpRequest request = HttpRequest.GET('/bintray/packages-lowlevel')

        HttpResponse<List<BintrayPackage>> rsp = client.toBlocking().exchange(request, (4)
                Argument.of(List, BintrayPackage)) (5)

        then: 'the endpoint can be accessed'
        rsp.status == HttpStatus.OK  (6)
        rsp.body() (7)

        when:
        List<BintrayPackage> packages = rsp.body()

        then:
        for (String name : expectedProfileNames) {
            assert packages*.name.contains(name)
        }
    }

    def "Verify bintray packages can be fetched with compile-time autogenerated @Client"() {
        when:
        HttpRequest request = HttpRequest.GET('/bintray/packages')

        Flowable<BintrayPackage> bintrayPackageStream = client.jsonStream(request, BintrayPackage) (8)
        Iterable<BintrayPackage> bintrayPackages = bintrayPackageStream.blockingIterable()

        then:
        for (String name : expectedProfileNames) {
            assert bintrayPackages*.name.contains(name)
        }
    }
}
1 The AutoCleanup extension makes sure the close() method of an object (e.g. EmbeddedServer) is called each time a feature method is finished
2 To run the application from a unit test you can use the EmbeddedServer interface
3 Micronaut’s HTTP client includes support for streaming data over HTTP via the RxStreamingHttpClient. Register a RxStreamingHttpClient bean in the application context and point it to the embedded server URL. The EmbeddedServer interface provides the URL of the server under test which runs on a random port.
4 Sometimes, receiving just the object is not enough and you need information about the response. In this case, instead of retrieve you should use the exchange method.
5 Micronaut makes it easy to parse JSON into Java objects.
6 Use status to check the HTTP status code.
7 Use .body() to retrieve the parsed payload.
8 Use the jsonStream method, which returns a Flowable, to consume the endpoint which generates a JSON Stream.

8 Testing the Application

To run the tests:

$ ./gradlew test
$ open build/reports/tests/test/index.html

9 Http Client Filter

Often, you need to include the same HTTP headers or URL parameters in a set of requests against a third-party API or when calling another Microservice.

To simplify this, Micronaut includes the ability to define HttpClientFilter classes that are applied to all matching HTTP clients.

For real world example, let us provide Bintray Authentication via a HttpClientFilter:

The Bintray REST API requires an applicative API key. An API key can be obtained from the user profile page. Authentication is achieved using HTTP Basic Authentication with the user’s name as username and the API key as the password. Authenticated REST calls should only be used via HTTPs.

Create a Filter:

src/main/java/example/micronaut/BintrayFilter.java
package example.micronaut;

import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.ClientFilterChain;
import io.micronaut.http.filter.HttpClientFilter;
import org.reactivestreams.Publisher;

@Filter("/api/${bintray.apiversion}/repos/**") (1)
@Requires(property = BintrayConfiguration.PREFIX + ".username") (2)
@Requires(property = BintrayConfiguration.PREFIX + ".token") (2)
public class BintrayFilter  implements HttpClientFilter {

    private final BintrayConfiguration configuration;

    public BintrayFilter(BintrayConfiguration configuration ) { (3)
        this.configuration = configuration;
    }

    @Override
    public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
        return chain.proceed(request.basicAuth(configuration.getUsername(), configuration.getToken())); (4)
    }
}
1 Supply the pattern you want to match to the @Filter annotation.
2 Bean will not loaded unless configuration properties are set.
3 Constructor injection of the configuration parameters.
4 Enhance every request sent to Bintray API providing Basic Authentication.

Configuration Parameters

Add your Bintray username and token to src/main/resource/application.yml

bintray:
    organization: micronaut
    repository: profiles
    apiversion: v1
    username: yourbintrayusername
    token: XXXXXXXXXXX

Add to src/main/resources/logback.xml, a logger to see Micronaut’s HTTP client output.

<logger name="io.micronaut.http.client" level="TRACE"/>

If you run again the tests, you will see the that the Filter is invoked and HTTP Basic Auth is used against Bintray API.

13:21:08.981 [nioEventLoopGroup-1-14] TRACE i.m.http.client.DefaultHttpClient - Authorization: Basic XXXXXXXXXXXXXX

10 Where To Go From Here?