August 28, 2018

Micronaut Mastery: Using Specific Configuration Properties For HTTP Client

One of the (many) great features of Micronaut is the HTTP client. We use the @Client annotation to inject a low-level HTTP client. Or we define a declarative HTTP client based on an interface, for which Micronaut will generate an implementation. The @Client annotation supports the configuration parameter to reference a configuration class with configuration properties for the HTTP client. The configuration class extends HttpClientConfiguration to support for example the configuration of timeouts and connection pooling. We can add our own configuration properties as well and use them in our application.

In the following example we want to access the OpenWeatherMap API using a declarative HTTP client. First we write a class that extends HttpClientConfiguration. This gives us HTTP client configuration properties and we also add some properties to define the OpenWeatherMap URI, path and access key we need to invoke the REST API. Finally we add configuration properties for a @Retryable annotation we want to use for our HTTP client.

// File: src/main/java/mrhaki/micronaut/WeatherClientConfiguration.java
package weather;

import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.runtime.ApplicationConfiguration;

import java.net.URI;
import java.time.Duration;

import static weather.WeatherClientConfiguration.PREFIX;

/**
 * Custom HTTP client configuration set via application
 * properties prefixed with "weather.client".
 */
@ConfigurationProperties(PREFIX)
public class WeatherClientConfiguration extends HttpClientConfiguration {

    public static final String PREFIX = "weather.client";

    /**
     * HTTP client connection pool configuration.
     */
    private final WeatherClientConnectionPoolConfiguration connectionPoolConfiguration;

    /**
     * OpenWeatherMap URI.
     */
    private URI url;

    /**
     * Path for requests sent to OpenWeatherMap.
     */
    private String path;
    
    /** 
     * Key needed to access OpenWeatherMap API.
     */
    private String apiKey;

    public WeatherClientConfiguration(
            final ApplicationConfiguration applicationConfiguration,
            final WeatherClientConnectionPoolConfiguration connectionPoolConfiguration) {
        super(applicationConfiguration);
        this.connectionPoolConfiguration = connectionPoolConfiguration;
    }

    public URI getUrl() {
        return url;
    }

    public void setUrl(final URI url) {
        this.url = url;
    }

    public String getPath() {
        return path;
    }

    public void setPath(final String path) {
        this.path = path;
    }

    public String getApiKey() {
        return apiKey;
    }

    public void setApiKey(final String apiKey) {
        this.apiKey = apiKey;
    }

    @Override
    public ConnectionPoolConfiguration getConnectionPoolConfiguration() {
        return connectionPoolConfiguration;
    }
    
    @ConfigurationProperties(ConnectionPoolConfiguration.PREFIX)
    public static class WeatherClientConnectionPoolConfiguration extends ConnectionPoolConfiguration {
    }

    /**
     * Extra configuration propertie to set the values
     * for the @Retryable annotation on the WeatherClient.
     */
    @ConfigurationProperties(WeatherClientRetryConfiguration.PREFIX)
    public static class WeatherClientRetryConfiguration {
        
        public static final String PREFIX = "retry";
        
        private Duration delay;
        
        private int attempts;

        public Duration getDelay() {
            return delay;
        }

        public void setDelay(final Duration delay) {
            this.delay = delay;
        }

        public int getAttempts() {
            return attempts;
        }

        public void setAttempts(final int attempts) {
            this.attempts = attempts;
        }
    }
}

Next we write the declarative HTTP client as Java interface with the @Client annotation. We refer to our custom configuration and use the configuration properties to set the URI and path for accessing the OpenWeatherMap API.

// File: src/main/java/mrhaki/micronaut/WeatherClient.java
package weather;

import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.Client;
import io.micronaut.retry.annotation.Retryable;
import io.reactivex.Single;

import java.util.Map;

// Declarative HTTP client with URL and path
// fetched from the application configuration.
// HTTP client configuration like pooled connections,
// timeouts are defined using WeatherClientConfiguration.
@Client(
        value = "${weather.client.url}",
        path = "${weather.client.path}",
        configuration = WeatherClientConfiguration.class)
// Retry accessing OpenWeatherMap REST API if error occurs.
@Retryable(
        attempts = "${weather.client.retry.attempts}",
        delay = "${weather.client.retry.delay}")
interface WeatherClient {

    /**
     * Get weather description for the town of Tilburg, NL. 
     * The APPID query parameter is filled in with the apiKey
     * argument value.
     *
     * @param apikey OpenWeatherMap API key to access REST API.
     * @return Response data from REST API.
     */
    @Get("weather?q=Tilburg,nl&APPID={apikey}")
    Single<Map<String, Object>> tilburg(String apikey);

}

Finally we write a controller that uses the declarative HTTP client WeatherClient to get a weather description for the town of Tilburg in The Netherlands:

// File: src/main/java/mrhaki/micronaut/WeatherController.java
package weather;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.reactivex.Single;

import java.util.List;
import java.util.Map;

/**
 * Controller to expose data from the 
 * OpenWeatherMap REST API.
 */
@Controller("/weather")
public class WeatherController {
    
    private final WeatherClient client;
    private final WeatherClientConfiguration configuration;

    public WeatherController(
            final WeatherClient client, 
            final WeatherClientConfiguration configuration) {
        this.client = client;
        this.configuration = configuration;
    }

    /**
     * Get weather data for town Tilburg, NL and get the
     * weather description to return.
     * 
     * @return Weather description as text.
     */
    @Get(value = "/tilburg", produces = MediaType.TEXT_PLAIN)
    public Single<String> weatherInTilburg() {
        return client.tilburg(configuration.getApiKey())
                     .map(response -> getWeatherDescription(response));
    }

    /**
     * Get weather description from response data.
     * 
     * @param data Response data from OpenWeatherMap API.
     * @return Textual description of weather.
     */
    private String getWeatherDescription(final Map<String, Object> data) {
        final List<Object> weatherList = (List<Object>) data.get("weather");
        final Map<String, Object> weather = (Map<String, Object>) weatherList.get(0);
        final String description = (String) weather.get("description");
        return description;
    }
    
}

In the application.yml configuration file we can set the values for the configuration properties:

# File: src/main/resources/application.yml
...
weather:
  client:
    url: http://api.openweathermap.org/
    path: /data/2.5/
    api-key: 39caa...
    read-timeout: 500ms
    retry:
      attempts: 2
      delay: 5s

When we run our application and access the URL http://localhost:8080/weather/tilburg using HTTPie we get the weather description:

$ http :8080/weather/tilburg
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain;charset=UTF-8

moderate rain

Written with Micronaut 1.0.0.M4.