CLI applications with Micronaut and Picocli
Tags: micronaut, java, picocli, microservices
Published: 2020-06-01 12:52:52

---
title: "CLI applications with Micronaut and Picocli"
published: true
description: "Use Micronaut and Picocli to create powerful CLI applications using JVM code !"
series: "Micronaut Discovery"
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/69quf600y4s3ao78ht95.jpg
---

In a [previous article](https://dev.to/stack-labs/a-micronaut-introduction-4d9l) we introduced Micronaut framework with a simple hello-world application. Today we will see how we can easily **create powerful CLI applications using Micronaut and Picocli**. In a future article we will see how we can compile our app down to a native binary using Micronaut and GraalVM ! 

<!--more-->

A great use case for Micronaut is to use it with the wonderful [Picocli](https://picocli.info/) library to build very simple yet very powerful Command Line Interface (CLI) applications. [Picocli️](https://picocli.info/) ❤️ is an awesome library that allows creating rich command line applications for the JVM, and we can use Micronaut to enrich our application with auto-generated web clients, auto-configuration and such.

So what exactly are we talking about here ? 🤔

The exercice today will be to develop a simple [weather CLI application](https://github.com/orevial/micronaut-weather-cli/tree/picocli-app) that will connect to a distant weather API to give us current weather and forecast for a given location.

We will use [Picocli️](https://picocli.info/) features to easily respond to CLI commands and we will use Micronaut to interact with the distant weather API, with Micronaut `http-client` library. Of course for this simple example it could make more sense to use [Java 11 native HTTP client](https://openjdk.java.net/groups/net/httpclient/intro.html) but for the sake of this exercise, let's stick with Micronaut 🚀. It will allow us to see how Micronaut works in this context (and of course it could be extended with richer features such as auto-configuration) and this simple CLI application will also be a good playground for the native compilation coming in the next article 😉 
 
In this exercice we will use [Weatherbit.io API](https://www.weatherbit.io/features) to add two commands to our CLI application:
  * `weather` command will return the current weather for a given location (city and country)
  * `forecast` command will return a forecast for the nehugxt 1 to 16 days for a given location (city and country)

👩‍💻👨‍💻 Code sample for this article can be found in [this Github repository](https://github.com/orevial/micronaut-weather-cli/tree/graalvm-app).

## Generate a CLI Micronaut application
  
As Micronaut applications are quite simple we could create ours from scratch, but as explained in our [Micronaut 101 article](https://dev.to/stack-labs/a-micronaut-introduction-4d9l) we will instead use Micronaut `mn` command to generate our app skeleton, mostly to pre-configure our build with Picocli dependency.

➡️ Run the following command to generate the application: 

```
> mn create-cli-app weather-cli --features http-client
```

ℹ️ We told Micronaut to create a CLI application, thus we avoid the use of an HTTP server (useless in our case).


➡️ Let's now have a look at generated build file `build.gradle`:

```
dependencies {
    annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    annotationProcessor "io.micronaut.configuration:micronaut-picocli"
    annotationProcessor "io.micronaut:micronaut-inject-java"
    annotationProcessor "io.micronaut:micronaut-validation"
    implementation platform("io.micronaut:micronaut-bom:$micronautVersion")
    implementation "io.micronaut:micronaut-runtime"
    implementation "info.picocli:picocli"
    implementation "io.micronaut.configuration:micronaut-picocli"
    implementation "io.micronaut:micronaut-inject"
    implementation "io.micronaut:micronaut-validation"
    implementation "io.micronaut:micronaut-http-client"
    runtimeOnly "ch.qos.logback:logback-classic:1.2.3"
    testAnnotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
    testAnnotationProcessor "io.micronaut:micronaut-inject-java"
    testImplementation platform("io.micronaut:micronaut-bom:$micronautVersion")
    testImplementation "org.junit.jupiter:junit-jupiter-api"
    testImplementation "io.micronaut.test:micronaut-test-junit5"
    testImplementation "io.micronaut:micronaut-inject-java"
    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
}
```

Amongst other things we can see that Micronaut added a few dependencies, including Micronaut core dependencies (java-inject, validation, runtime, etc.) along with Picocli dependency. 

## Add Picocli commands and Micronaut HTTP client

➡️ First let's look at `WeatherCliCommand`, the default main class that was generated for us: 

```java
@Command(name = "weather-cli", description = "...",
        mixinStandardHelpOptions = true)
public class WeatherCliCommand implements Runnable {

    @Option(names = {"-v", "--verbose"}, description = "...")
    boolean verbose;

    public static void main(String[] args) throws Exception {
        PicocliRunner.run(WeatherCliCommand.class, args);
    }

    public void run() {
        // business logic here
        if (verbose) {
            System.out.println("Hi!");
        }
    }
}
```

If you run the application now you should see a very basic CLI application that works but doesn't do much (except printing "Hi!" if called with the `-v` option). We will now add the `weather` and `forecast` subcommands to add behavior.

### Working with Picocli commands

As both subcommands will need to take similar city and country inputs, let's factorize this.

➡️ Create a `LocalizedCommand` class that simply declares a `city` and a `country`, and validates these inputs:

```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;

public abstract class LocalizedCommand implements Runnable {
    private static final Logger LOG = LoggerFactory.getLogger(ForecastSubcommand.class);

    @Spec
    protected CommandLine.Model.CommandSpec spec;

    private static final String DEFAULT_CITY = "paris";
    private static final String DEFAULT_COUNTRY_CODE = "fr";

    protected String city;
    protected String country;

    @Option(names = {"--country"},
        description = "the 2-letters country code",
        defaultValue = DEFAULT_COUNTRY_CODE,
        showDefaultValue = ALWAYS)
    public void setCountry(String country) {
        if (country.length() >= 2) {
            this.country = country.substring(0, 2).toLowerCase();
        } else {
            throw new CommandLine.ParameterException(
                spec.commandLine(),
                "Country parameter must be a 2-letters code");
        }
    }

    @Option(names = {"--city"},
        description = "the city name",
        defaultValue = DEFAULT_CITY,
        showDefaultValue = ALWAYS)
    public void setCity(String city) {
        this.city = city.toLowerCase();
    }

    @Override
    public void run() {
        LOG.info("Asking forecast for city {} and country {}", city, country);
        if (DEFAULT_CITY.equals(city)) {
            LOG.warn("Using default city {}...", city);
        }
        if (DEFAULT_COUNTRY_CODE.equals(country)) {
            LOG.warn("Using default country {}...", country);
        }
    }
}
```

Perfect, we now have the base parameters ready. Time to create real subcommands !

➡️ First, let's create the `weather` subcommand that simply returns the result of the Weather API call:
 
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine.Command;
import weather.cli.api.WeatherAPIClient;
import weather.cli.api.WeatherResponse;

@Command(name = "weather", description = "gives weather for a given location")
public class CurrentWeatherSubcommand extends LocalizedCommand {
    private static final Logger LOG = LoggerFactory.getLogger(CurrentWeatherSubcommand.class);

    @Inject
    private WeatherAPIClient weatherAPIClient;

    @Override
    public void run() {
        super.run();

        WeatherResponse weather = weatherAPIClient.weather(city, country);
        LOG.info("=====================================");
        LOG.info("Current weather in {}: \n{}", city, weather.getData().get(0).toString());
        LOG.info("=====================================");
    }
}
```

Of course this will not compile as we don't have the API client interface yet, nor the models. You can create the interface now if you want but we will see this interface in a minute. API models can be found in [source repository](https://github.com/orevial/micronaut-weather-cli/tree/picocli-app).

➡️ We can now create the `forecast` command:

```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import weather.cli.api.ForecastObservation;
import weather.cli.api.ForecastResponse;
import weather.cli.api.WeatherAPIClient;

import javax.inject.Inject;

import static java.util.Comparator.comparing;
import static picocli.CommandLine.Help.Visibility.ALWAYS;

@Command(name = "forecast", description = "gives forecast for given city")
public class ForecastSubcommand extends LocalizedCommand {
    private static final Logger LOG = LoggerFactory.getLogger(ForecastSubcommand.class);

    @Inject
    private WeatherAPIClient weatherAPIClient;

    private int nbDays;

    @Option(names = {"-d", "--days"},
        description = "the number of forecast days to fetch (between 1 and 16)",
        defaultValue = "1",
        showDefaultValue = ALWAYS)
    public void setNbDays(int nbDays) {
        if (nbDays < 1 || nbDays > 16) {
            throw new CommandLine.ParameterException(
                spec.commandLine(),
                "Forecast must be between 1 and 16 days");
        }
        this.nbDays = nbDays;
    }

    @Override
    public void run() {
        super.run();

        ForecastResponse forecast = weatherAPIClient.forecast(city, country, nbDays);
        LOG.info("=====================================");
        forecast.getData().stream()
            .sorted(comparing(ForecastObservation::getForecastDate))
            .forEach(forecastObservation -> LOG.info("Forecast in {} on day {}: \n{}",
                city, forecastObservation.getForecastDate(),
                forecastObservation.toString()));
        LOG.info("=====================================");
    }
}
```

This class is similar to the `weather` subcommand except that it takes (and validates) an extra `--days` parameter that corresponds to the number of days we want the forecast for, and loop on the API response to display forecast of each coming day.

➡️ Great, we now have our two subcommands, let's add them to the "main" command, we can now open `WeatherCliCommand`:

```java
import io.micronaut.configuration.picocli.PicocliRunner;
import picocli.CommandLine.Command;
import weather.cli.commands.CurrentWeatherSubcommand;
import weather.cli.commands.ForecastSubcommand;

@Command(subcommands = {
    CurrentWeatherSubcommand.class,
    ForecastSubcommand.class
})
public class WeatherCliCommand implements Runnable {

    public static void main(String[] args) throws Exception {
        PicocliRunner.run(WeatherCliCommand.class, args);
    }

    public void run() {
        System.out.println("Welcome to weather app...");
    }
}
```

### Working with Micronaut HTTP Client

Creating an HTTP client with Micronaut is dead-simple. Before creating it, let's look at [Weatherbit API endpoints](https://www.weatherbit.io/features):
* [Current weather endpoint](https://www.weatherbit.io/api/weather-current):
  * Requires an API key
  * Takes a `city` and a `country` to localize the request
* [16 days forecast endpoint](https://www.weatherbit.io/api/weather-forecast-16-day):
  * Requires an API key
  * Takes a `city` and a `country` to localize the request
  * Takes an optional `days` parameter to specify the number of days to include in the forecast (defaults to 16)

➡️ Alright, time to create the `WeatherAPIClient`: 

```java
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.http.client.annotation.Client;

@Client("${weather.api.url}")
public interface WeatherAPIClient {
    @Get("/current?key=${weather.api.key}")
    WeatherResponse weather(@QueryValue String city,
                   @QueryValue String country);

    @Get("/forecast/daily?key=${weather.api.key}")
    ForecastResponse forecast(@QueryValue String city,
                    @QueryValue String country,
                    @QueryValue(defaultValue = "1") int days);
}
```

What can we see here ?

* We just need an interface. No need to write a client implementation, Micronaut will inject an implementation for us when needed.
* `@Client` annotation declares a Micronaut HTTP client, and we pass it a configuration key named `weather.api.url` as the server base URL. Micronaut will resolve this configuration at runtime.
* `@Get` annotation declares a HTTP Get endpoint and we tell it to add a query parameter from the configuration key `weather.api.key`. We pass it by configuration to avoid putting our API key directly in the code (so you don't steal my API key 🔒👮)
* Both methods declare `@QueryValue` parameters that will be added to the URL by Micronaut at runtime.
* Both methods returns a simple POJO response that will be automatically converted from the API json response by Micronaut with the help of [Jackson ObjectMapper](https://docs.micronaut.io/latest/guide/index.html#_reading_json_responses).

As said above, we added a query parameter named `weather.api.key` that we want to be filled in our configuration.

➡️ Let's open the `application.yml` file in `main/resources` and modify it so it matches the following content:

```yaml
micronaut:
  application:
    name: weather-cli

weather:
  api:
    url: https://api.weatherbit.io/v2.0/
    key: invalid-api-key
```

But right now you might think that I'm crazy to put my configuration key in a configuration file that is going to be versioned in my public Github repository... well, I won't put my real key. To avoid this we could tell Micronaut that this key uses an environment variable by defining our property as such :

```yaml
weather.api.key: ${WEATHER_API_KEY:invalid-api-key}
```

In fact we don't need this extra definition as Micronaut uses "[Property Value Binding](https://docs.micronaut.io/latest/guide/index.html#_property_value_binding)", which means that `WEATHER_API_KEY` environment variable value will automatically be assigned and override our property key `weather.api.key`. 

## Running our application

➡️ Alright, time to run our application and generate a uber-jar:

```
> ./gradlew assemble
```

➡️ We can now run it and ask for current weather in Montreal:

```
# First export WEATHER_API_KEY (replace with your own key)
> export WEATHER_API_KEY=WEATHER-BIT-IO-GENERATED-API-KEY

# Run the app and ask for Montreal weather
> java -jar build/libs/weather-cli-0.1-all.jar weather --country CA --city montreal

Established active environments: [cli]
Asking weather for city montreal and country ca
=====================================
Current weather in montreal:
  - temperature: 8.3°C
  - wind speed: 3.6 km/h
  - cloud coverage: 100.0%
  - precipitation: 6.0 mm/h
=====================================
```

➡️ Let's now ask for Paris 3-days forecast:

```
# Ask for Paris 3-days forecast
> java -jar build/libs/weather-cli-0.1-all.jar forecast --city Paris --days 3

Established active environments: [cli]
Asking forecast for city paris and country fr
Using default city paris...
Using default country fr...
=====================================
Forecast in paris on day 2020-04-30:
  - average temperature: 13.4°C
  - min temperature: 11.6°C
  - max temperature: 14.9°C
  - wind speed: 17.962884000000003 km/h
  - probability of precipitation: 75.0%
  - average cloud coverage: 86.0%
Forecast in paris on day 2020-05-01:
  - average temperature: 13.1°C
  - min temperature: 10.0°C
  - max temperature: 17.7°C
  - wind speed: 13.19382 km/h
  - probability of precipitation: 75.0%
  - average cloud coverage: 74.0%
Forecast in paris on day 2020-05-02:
  - average temperature: 12.5°C
  - min temperature: 9.1°C
  - max temperature: 16.9°C
  - wind speed: 10.229436 km/h
  - probability of precipitation: 0.0%
  - average cloud coverage: 60.0%
=====================================
```

Great, everything works as expected 😊

👏 Congrats, you have now successfully created a CLI application using Micronaut and Picocli !  

## A note on performance...

Let's time the execution time of the help command (we don't time our subcommands because they use network time to talk to the API and this would make any comparison flawed) to have a better idea of the startup time:

```
> time "java -jar build/libs/weather-cli-0.1-all.jar --help"

Benchmark #1: java -jar build/libs/weather-cli-0.1-all.jar --help
  Time (mean ± σ):      2.279 s ±  0.172 s    [User: 2.764 s, System: 0.216 s]
  Range (min … max):    2.075 s …  2.732 s    10 runs
```

ℹ️ I used [`hyperfine`](https://github.com/sharkdp/hyperfine) here instead of `time` to run a benchmark on the help command

We have a mean answer time of approximately 2 seconds. Although it can be satisfying for a JVM app, it's not great for a CLI application.  

So how do we improve that ?

![spoiler alert](https://dev-to-uploads.s3.amazonaws.com/i/rlowvamgytnb5pqthgih.jpg)

The answer: [**GraalVM native compilation**](https://dev.to/stack-labs/native-images-with-micronaut-and-graalvm-3cn8) !