Welcome to this Micronaut workshop 🚀 !

The goal of this workshop is to create JVM microservices using Micronaut to discover this framework.

We will learn how to create a Micronaut app using the command line, but of course we will mostly code to learn how to use dependency injection and IoC with Micronaut, how to handle configuration, how to create HTTP servers and clients (even reactive ones 🏎), how to do simple tests and much more ! We will also deep into very interesting aspects of Micronaut such as native compilation with GraalVM or cloud-native features.

You might not be a Micronaut expert at the end of this lab (there is no certification anyway 🎓) but it should give you a good overview of all the things Micronaut offers. Of course we don't have enough time to look at everything so if you later want to go deeper, the official documentation is a great place to start and there are also some great guides that are step-by-step tutorial that will walk you through various specific topics by coding them.

How to read this workshop

This workshop has been thought to be done in a sequential fashion, however here are a little information:

Software

In order to do this workshop, you need the following:

Micronaut CLI

🔌 Network install

  1. Install SDKMAN! if you haven't done so already.

  2. Install Micronaut CLI:

    $ sdk install micronaut
    
  3. Ensure the CLI is installed properly:

    $ mn --version
    | Micronaut Version: 1.3.1
    | JVM Version: 11.0.2
    

💾 USB install

Copy micronaut directory to your computer (micronaut-$version folder). Now using the command line you can simply move to the bin directory and execute the mn command (mn.bat for Windows). Example:

mn --version

Clone this repository

➡️ Once done, you can clone and move to this repo:

git clone https://github.com/orevial/micronaut-workshop-java-maven.git
cd micronaut-workshop-java-maven

Download Maven

➡️ From the micronaut-workshop-java-maven project root run a simple build to download Maven distribution:

./mvnw clean install

The Micronaut CLI is the recommended way to create new Micronaut projects. The CLI includes commands for generating specific categories of projects, allowing you to choose between build tools, test frameworks, and even pick the language you wish to use in your application. The CLI also provides commands for generating artifacts such as controllers, client interfaces, and serverless functions.

The create-app command is the starting point for creating Micronaut applications. The CLI is based on the concept of profiles. A profile consist of a project template (or skeleton), optional features, and profile-specific commands. Commands from a profile are specific to the profile application type; for example, the service profile (designed for creation of microservice applications) provides the create-controller and create-client commands.

Listing profiles

➡️ You can list the available profiles with the list-profiles command:

mn list-profiles
| Available Profiles
--------------------
  cli                 The cli profile
  configuration       The profile for creating the configuration
  federation          The federation profile
  function-aws        The function profile for AWS Lambda
  function-aws-alexa  The function profile for AWS Alexa-Lambda
  grpc                Profile for Creating GRPC Services
  kafka               The Kafka messaging profile
  profile             A profile for creating new Micronaut profiles
  rabbitmq            The RabbitMQ messaging profile
  service             The service profile

Applications generated from a profile can be personalised with features. A feature further customises the newly created project by adding additional dependencies to the build, more files to the project skeleton, etc.

Getting information about a profile

➡️ To see all the features of a profile, you can use the profile-info command:

mn profile-info service                                                                                
| Profile: service
--------------------
The service profile

| Provided Commands:
--------------------
  create-bean              Creates a singleton bean
  create-client            Creates a client interface
  create-controller        Creates a controller and associated test
  create-job               Creates a job with scheduled method
  create-repository        Creates a repository and associated test
  create-test              Creates a simple test for the project's testing framework
  create-websocket-client  Creates a Websocket client
  create-websocket-server  Creates a Websocket server
  help                     Prints help information for a specific command

| Provided Features:
| (+) denotes features included by default.
--------------------
  annotation-api (+)        Adds Java annotation API
  application (+)           Facilitates creating an executable JVM application and adds support for creating fat/uber JARs
  asciidoctor               Adds Asciidoctor documentation
  aws-api-gateway           Adds support for AWS API Gateway
  aws-api-gateway-graal     Creates an AWS API Gateway Proxy Lambda with Graal Native Image
  cassandra                 Adds support for Cassandra in the application
  config-consul             Adds support for Distributed Configuration with Consul (https://www.consul.io)
  data-hibernate-jpa        Adds support for Micronaut Data Hibernate/JPA
  data-jdbc                 Adds support for Micronaut Data JDBC
  discovery-consul          Adds support for Service Discovery with Consul (https://www.consul.io)
  discovery-eureka          Adds support for Service Discovery with Eureka
  ehcache                   Adds support for Ehcache (https://www.ehcache.org/)
  elasticsearch             Adds support for Elasticsearch in the application
  file-watch                Adds automatic restarts and file watch
  flyway                    Adds support for Flyway database migrations (https://flywaydb.org/)
  graal-native-image        Allows Building a GraalVM Native Image
  graphql                   Adds support for GraphQL in the application
  groovy                    Creates a Groovy application
  hazelcast                 Adds support for Hazelcast (https://hazelcast.org)
  hibernate-gorm            Adds support for GORM persistence framework
  hibernate-jpa             Adds support for Hibernate/JPA
  http-client (+)           Adds support for creating HTTP clients
  http-server (+)           Adds support for running a Netty server
  java                      Creates a Java application
  jdbc-dbcp                 Configures SQL DataSource instances using Commons DBCP
  jdbc-hikari               Configures SQL DataSource instances using Hikari Connection Pool
  jdbc-tomcat               Configures SQL DataSource instances using Tomcat Connection Pool
  jib                       Adds support for Jib builds
  jrebel                    Adds support for class reloading with JRebel (requires separate JRebel installation)
  junit                     Adds support for the JUnit 5 testing framework
  kafka                     Adds support for Kafka
  kafka-streams             Adds support for Kafka Streams
  kotlin                    Creates a Kotlin application
  kotlintest                Adds support for the KotlinTest testing framework
  kubernetes                Adds support for Kubernetes
  liquibase                 Adds support for Liquibase database migrations (http://www.liquibase.org/)
  log4j2                    Adds Log4j2 Logging
  logback (+)               Adds Logback Logging
  management                Adds support for management endpoints
  micrometer                Adds support for Micrometer metrics
  micrometer-appoptics      Adds support for Micrometer metrics (w/ AppOptics reporter)
  micrometer-atlas          Adds support for Micrometer metrics (w/ Atlas reporter)
  micrometer-azure-monitor  Adds support for Micrometer metrics (w/ Azure Monitor reporter)
  micrometer-cloudwatch     Adds support for Micrometer metrics (w/ AWS Cloudwatch reporter)
  micrometer-datadog        Adds support for Micrometer metrics (w/ Datadog reporter)
  micrometer-dynatrace      Adds support for Micrometer metrics (w/ Dynatrace reporter)
  micrometer-elastic        Adds support for Micrometer metrics (w/ Elastic reporter)
  micrometer-ganglia        Adds support for Micrometer metrics (w/ Ganglia reporter)
  micrometer-graphite       Adds support for Micrometer metrics (w/ Graphite reporter)
  micrometer-humio          Adds support for Micrometer metrics (w/ Humio reporter)
  micrometer-influx         Adds support for Micrometer metrics (w/ Influx reporter)
  micrometer-jmx            Adds support for Micrometer metrics (w/ Jmx reporter)
  micrometer-kairos         Adds support for Micrometer metrics (w/ Kairos reporter)
  micrometer-new-relic      Adds support for Micrometer metrics (w/ New Relic reporter)
  micrometer-prometheus     Adds support for Micrometer metrics (w/ Prometheus reporter)
  micrometer-signalfx       Adds support for Micrometer metrics (w/ SignalFx reporter)
  micrometer-stackdriver    Adds support for Micrometer metrics (w/ Stackdriver reporter)
  micrometer-statsd         Adds support for Micrometer metrics (w/ Statsd reporter)
  micrometer-wavefront      Adds support for Micrometer metrics (w/ Wavefront reporter)
  mongo-gorm                Configures GORM for MongoDB for Groovy applications
  mongo-reactive            Adds support for the Mongo Reactive Streams Driver
  neo4j-bolt                Adds support for the Neo4j Bolt Driver
  neo4j-gorm                Configures GORM for Neo4j for Groovy applications
  netflix-archaius          Adds support for Netflix Archaius in the application
  netflix-hystrix           Adds support for Netflix Hystrix in the application
  netflix-ribbon            Adds support for Netflix Ribbon in the application
  picocli                   Adds support for command line parsing (http://picocli.info)
  postgres-reactive         Adds support for the Reactive Postgres driver in the application
  rabbitmq                  Adds support for RabbitMQ in the application
  redis-lettuce             Configures the Lettuce driver for Redis
  security-jwt              Adds support for JWT (JSON Web Token) based Authentication
  security-session          Adds support for Session based Authentication
  spek                      Adds support for the Spek testing framework
  spock                     Adds support for the Spock testing framework
  springloaded              Adds support for class reloading with Spring-Loaded
  swagger-groovy            Configures Swagger (OpenAPI) Integration for Groovy
  swagger-java              Configures Swagger (OpenAPI) Integration for Java
  swagger-kotlin            Configures Swagger (OpenAPI) Integration for Kotlin
  tracing-jaeger            Adds support for distributed tracing with Jaeger (https://www.jaegertracing.io)
  tracing-zipkin            Adds support for distributed tracing with Zipkin (https://zipkin.io)
  vertx-mysql-client        Add support for the Reactive MySQL Client in the application
  vertx-pg-client           Add support for the Reactive Postgres Client in the application

Creating and running an hello-world application

As explained above, the create-app command can be used to create new projects. It accepts some flags:

FlagDescriptionExample
buildBuild tool (one of gradle, maven - default is gradle)--build maven
profileProfile to use for the project (default is service)--profile function-aws
featuresFeatures to use for the project, comma-separated--features security-jwt,mongo-gorm
inplaceIf present, generates the project in the current directory (project name is optional if this flag is set)--inplace

➡️ In our case we will simply try our command with a simple hello-world microservice. For this we simply need an HTTP client & server and junit feature so we can write a small unit test. First move outside the cloned project to create this sample hello-world:

# move outside the cloned project
cd ..
# create simple hello-world app
mn create-app com.stacklabs.micronaut.workshop.hello-world --lang=java --features=junit
| Generating Java project...
| Application created at /Users/path/to/the/micronaut-workshop/hello-world

➡️ Move to this project:

cd hello-world

➡️ We would like our hello-world application to actually say "Hello", so let's add a controller for that:

mn create-controller Hello
| Rendered template Controller.java to destination src/main/java/com/stacklabs/micronaut/workshop/agency/HelloController.java
| Rendered template Test.java to destination src/test/java/com/stacklabs/micronaut/workshop/agency/HelloControllerTest.java

Our first controller

➡️ Open the generated src/main/java/com/stacklabs/micronaut/workshop/HelloController.java with your favorite IDE and make it return "Hello Micronaut !":

package com.stacklabs.micronaut.workshop;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello")
public class HelloController {

    @Get("/")
    public String index() {
        return "Hello Micronaut !";
    }
}

So what exactly do we have here ?

➡️ Now, run the application:

./mvnw install exec:exec

You will see a line similar to the following once the application has started:

14:40:01.187 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 957ms. Server Running: http://localhost:8080

➡️ Then, in another shell, make a request to your service:

$ curl localhost:8080/hello
Hello Micronaut !

I know I know, it's just an Hello World, but hey, that's a start !

Write an automated test

While testing manually is acceptable in some situations, going forward it's better to have automated tests to exercise our applications. Fortunately, Micronaut makes testing super easy!

Micronaut applications can be tested with any testing framework, because io.micronaut.context.ApplicationContext is capable of spinning up embedded instances quite easily. The CLI adds support for using JUnit, Spock and Spek.

In addition to that, if you are using JUnit 5 or Spock, there is special support that allows to remove most of the boilerplate about starting/stopping server and injecting beans. Check the Micronaut Test project for more information.

We will use Maven to run the tests, however, if you want to run them from your IDE, make sure you enable annotation processors (see requirements for more information).

➡️ We will now update the generated test located at src/test/java/com/stacklabs/micronaut/workshop/HelloControllerTest.java to make it look like this:

package com.stacklabs.micronaut.workshop;

import io.micronaut.http.client.RxHttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
public class HelloControllerTest {

    @Inject
    EmbeddedServer embeddedServer;

    @Test
    public void testIndex() throws Exception {
        try(RxHttpClient client = embeddedServer.getApplicationContext().createBean(RxHttpClient.class, embeddedServer.getURL())) {
            assertEquals("Hello Micronaut !", client.retrieve("/hello").blockingFirst());
        }
    }
}

What do we do have here ?

➡️ We can now run the tests:

./mvnw test

Once finished, you should see an output similar to:

[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.stacklabs.micronaut.workshop.agency.HelloControllerTest
23:18:45.538 [main] INFO  i.m.context.env.DefaultEnvironment - Established active environments: [test]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.62 s - in com.stacklabs.micronaut.workshop.agency.HelloControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Congratulations, you have written your very first Micronaut application with a simple controller and test !

Ok ok, I know what you want to tell me: hello-world are great, but when is it becoming real ? Right now !

In the previous step we have created a simple hello-world microservice with very basic feature (a basic REST controller).

For the rest of the workshop though we will be focusing on a more real use case to write some real Micronaut code 🚀️ !

The use case in question is a rental agency system that will follow the following architecture:

Microservices architecture

Explanations:

In this workshop we will mainly work with the rental-agency, which is part of a Maven multi-module build.

Alright, now that introductions have been made, let's code ⌨️ !

Moving to cloned repository

Time to move to the workshop repository, where we will now stay for the rest of this workshop.

➡️ Move to the workshop directory

cd ../micronaut-workshop-java-maven

Remember Micronaut features of mn profile-info service command from first steps ? Well we will now use them 🙃 In particular we will use jdbc-tomcat and hibernate-jpa features.

➡️ We can now open rental-agency/pom.xml file and verify that Micronaut has added three dependencies for us:

<dependency>
    <groupId>io.micronaut.configuration</groupId>
    <artifactId>micronaut-hibernate-jpa</artifactId>
</dependency>
<dependency>
    <groupId>io.micronaut.configuration</groupId>
    <artifactId>micronaut-jdbc-tomcat</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

As you can see, we now have Hibernate framework along with a Tomcat-based JDBC connection pool and a H2 in-memory database.

➡️ We can verify that the configuration for the H2 database has been added to our application configuration by opening src/main/resources/application.yml:

datasources:
  default:
    url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    driverClassName: org.h2.Driver
    username: sa
    password: ''
jpa:
  default:
    properties:
      hibernate:
        hbm2ddl:
          auto: update

➡️ We will now add the domain entity Car to the package agency.domain. The entity can be a simple POJO and will use classical JPA annotations. The table name is "cars" and the fields are indicated below:

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.domain;

import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import io.micronaut.http.hateoas.Link;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

@Entity
@Table(name = "cars")
public class Car {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID id = UUID.fromString("00000000-0000-0000-0000-000000000000");

    @NotNull
    @Column(name = "registration", nullable = false, unique = true)
    private String registration;

    @NotNull
    @Column(name = "brand", nullable = false)
    private String brand;

    @NotNull
    @Column(name = "model", nullable = false)
    private String model;

    @NotNull
    @Column(name = "category", nullable = false)
    private CarCategory category;

    @Transient
    private List<Link> links;

    public Car(String registration, String brand, String model, CarCategory category) {
        this.registration = registration;
        this.brand = brand;
        this.model = model;
        this.category = category;
    }

    public Car() {
    }

    public UUID getId() {
        return id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    public String getRegistration() {
        return registration;
    }

    public void setRegistration(String registration) {
        this.registration = registration;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public CarCategory getCategory() {
        return category;
    }

    public void setCategory(CarCategory category) {
        this.category = category;
    }

    public List<Link> getLinks() {
        return links;
    }

    public void setLinks(List<Link> links) {
        this.links = links;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o;
        return Objects.equals(registration, car.registration) &&
                Objects.equals(brand, car.brand) &&
                Objects.equals(model, car.model) &&
                category == car.category;
    }

    @Override
    public int hashCode() {
        return Objects.hash(registration, brand, model, category);
    }
}

➡️ Next, define the repository interface CarRepository under agency.persistence, as follows:

package com.stacklabs.micronaut.workshop.agency.persistence;

import com.stacklabs.micronaut.workshop.agency.domain.Car;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface CarRepository {
    List<Car> findAll();
    Optional<Car> findById(@NotNull UUID id);
    Optional<Car> findByRegistration(@NotNull String registration);
    Optional<Car> save(Car car);
    void deleteById(@NotNull UUID id);
    int deleteAll();
    Optional<Car> update(@NotNull UUID id, @NotBlank Car car);
}

➡️ Now, let's write the implementation using JPA.

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.persistence;

import com.stacklabs.micronaut.workshop.agency.domain.Car;
import io.micronaut.configuration.hibernate.jpa.scope.CurrentSession;
import io.micronaut.spring.tx.annotation.Transactional;

import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Singleton
public class CarRepositoryImpl implements CarRepository {
    @PersistenceContext
    EntityManager entityManager;
    //private EntityManager entityManager;



    public CarRepositoryImpl(@CurrentSession EntityManager entityManager) {
        //this.entityManager = entityManager;
    }

    @Transactional(readOnly = true)
    @Override
    public List<Car> findAll() {
        return entityManager
                .createQuery("SELECT g FROM Car as g", Car.class)
                .getResultList();
    }

    @Transactional(readOnly = true)
    @Override
    public Optional<Car> findById(@NotNull UUID id) {
        return Optional.ofNullable(entityManager.find(Car.class, id));
    }

    @Transactional(readOnly = true)
    @Override
    public Optional<Car> findByRegistration(@NotNull String registration) {
        return entityManager.createQuery("FROM Car where registration=:registration", Car.class)
                .setParameter("registration", registration)
                .getResultStream()
                .findFirst();
    }

    @Transactional
    @Override
    public Optional<Car> save(Car car) {
        entityManager.persist(car);
        return findByRegistration(car.getRegistration());
    }

    @Transactional
    @Override
    public void deleteById(@NotNull UUID id) {
        findById(id).ifPresent(car -> entityManager.remove(car));
    }

    @Transactional
    @Override
    public int deleteAll() {
        return entityManager
                .createQuery("DELETE FROM Car")
                .executeUpdate();
    }

    @Transactional
    @Override
    public Optional<Car> update(@NotNull UUID id, @NotBlank Car car) {
        return findById(id)
                .map(any -> entityManager.createQuery(
                        "UPDATE Car g SET brand = :brand, model = :model, category = :category, registration = :registration WHERE id = :id")
                        .setParameter("brand", car.getBrand())
                        .setParameter("model", car.getModel())
                        .setParameter("category", car.getCategory())
                        .setParameter("registration", car.getRegistration())
                        .setParameter("id", id)
                        .executeUpdate())
                .flatMap(any -> findById(id));
    }
}

➡️ Now, let's write a test for our car repository.

Tips:

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;

import static org.assertj.core.api.Assertions.assertThat;

@MicronautTest
class CarRepositoryImplTest {

    @Inject
    private CarRepository repository;

    private static final Car tesla = new Car("AA-123-BB", "Tesla", "Model S", CarCategory.COUPE);
    private static final Car clio = new Car("ZZ-666-NB", "Renault", "Clio", CarCategory.COMPACT_CAR);

    @Test
    void testCrudOperations() {
        assertThat(repository.findAll()).hasSize(0);

        repository.save(tesla);
        repository.save(clio);
        assertThat(repository.findAll())
                .hasSize(2)
                .containsExactly(tesla, clio);

        assertThat(repository.findById(tesla.getId())).hasValue(tesla);
        assertThat(repository.findByRegistration("ZZ-666-NB")).hasValue(clio);

        repository.deleteById(tesla.getId());
        assertThat(repository.findAll()).hasSize(1);
    }
}

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.15.0</version>
    <scope>test</scope>
</dependency>

➡️ We can now run our new test:

./mvnw test

Now that we have our CRUD repository, let's expose some endpoints through a REST API.

Creating REST endpoints

➡️ In this exercice we will add endpoints to the CarsController we created earlier. You can remove the first Get endpoint we created, we will not need it anymore. Now your mission is to implement the following endpoints :

Hints:

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.agency.aop.Logged;
import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.*;
import io.micronaut.http.annotation.Error;
import io.micronaut.http.hateoas.JsonError;
import io.micronaut.http.hateoas.Link;

import javax.persistence.PersistenceException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static java.util.Collections.emptyList;

@Controller("/cars")
public class CarsController {
    private CarRepository repository;

    public CarsController(CarRepository repository) {
        this.repository = repository;
    }

    @Logged
    @Get("/{?registration}")
    public List<Car> findAll(@QueryValue("registration") Optional<String> registration) {
        return registration
                .map(reg -> repository.findByRegistration(reg)
                        .map(Collections::singletonList)
                        .orElse(emptyList()))
                .orElseGet(() -> repository.findAll());
    }

    @Logged
    @Get("/{id}")
    public Optional<Car> findById(@PathVariable UUID id) {
        return repository.findById(id);
    }


    @Logged
    @Post("/")
    @Status(HttpStatus.CREATED)
    public Car add(@Body Car car) {
        return repository.save(car)
                .orElseThrow(() -> new RuntimeException("Unable to retrieve saved car..."));
    }

    @Logged
    @Delete("/{id}")
    @Status(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable UUID id) {
        repository.deleteById(id);
    }

    @Logged
    @Put("/{id}")
    Optional<Car> update(@PathVariable UUID id, @Body Car car) {
        return repository.update(id, car);
    }
}

Manual testing with curl

➡️ Now that we have a brand new controller that allow us to save, update, delete and find cars from our database, let's launch our application and make sure our endpoints work as expected:

./mvnw exec:exec -Dskip.agency.run=false

➡️ Run the following curl commands below to your terminal and you should see results similar to the ones below (with other generated uuids of course):

# Find all cars
curl "http://localhost:8080/cars"
[]⏎                            

# Add a first car
curl -X POST "http://localhost:8080/cars" -H "Content-Type:application/json" -d '{"registration":"AA-123-ZZ","brand":"Tesla","model":"Model S","category":"COUPE"}' | jq
{
    "id": "edaded73-be91-4156-9a94-76897579e455",
    "registration": "AA-123-ZZ",
    "brand": "Tesla",
    "model": "Model S",
    "category": "COUPE"
}        

# Add a second car
curl -X POST "http://localhost:8080/cars" -H "Content-Type:application/json" -d '{"registration":"GH-987-PO","brand":"Ford","model":"Fiesta","category":"COMPACT_CAR"}' | jq
{
  "id": "e95d5197-36c3-4501-914a-9de3befac3a1",
  "registration": "GH-987-PO",
  "brand": "Ford",
  "model": "Fiesta",
  "category": "COMPACT_CAR"
}

# Find all cars
curl "http://localhost:8080/cars" | jq
[
  {
    "id": "edaded73-be91-4156-9a94-76897579e455",
    "registration": "AA-123-ZZ",
    "brand": "Tesla",
    "model": "Model S",
    "category": "COUPE"
  },
  {
    "id": "e95d5197-36c3-4501-914a-9de3befac3a1",
    "registration": "GH-987-PO",
    "brand": "Ford",
    "model": "Fiesta",
    "category": "COMPACT_CAR"
  }
]

# Get cars by registration id
curl "http://localhost:8080/cars?registration=AA-123-ZZ" | jq
[
  {
    "id": "edaded73-be91-4156-9a94-76897579e455",
    "registration": "AA-123-ZZ",
    "brand": "Tesla",
    "model": "Model S",
    "category": "COUPE"
  }
]

# Get a specific car by id
curl "http://localhost:8080/cars/edaded73-be91-4156-9a94-76897579e455" | jq          
{
  "id": "edaded73-be91-4156-9a94-76897579e455",
  "registration": "AA-123-ZZ",
  "brand": "Tesla",
  "model": "Model S",
  "category": "COUPE"
}

# Update the first car
curl -X PUT "http://localhost:8080/cars/edaded73-be91-4156-9a94-76897579e455" -H "Content-Type:application/json"  -d '{"registration":"YT-756-KL","brand":"Alfa Romeo","model":"Brera","category":"COUPE"}' | jq
{
  "id": "edaded73-be91-4156-9a94-76897579e455",
  "registration": "AA-123-ZZ",ar
  "brand": "Alfa Romeo",
  "model": "Brera",
  "category": "COUPE"
}

# Find all cars
curl http://localhost:8080/cars | jq
[
  {
    "id": "edaded73-be91-4156-9a94-76897579e455",
    "registration": "YT-756-KL",
    "brand": "Alfa Romeo",
    "model": "Brera",
    "category": "COUPE"
  },
  {
    "id": "e95d5197-36c3-4501-914a-9de3befac3a1",
    "registration": "GH-987-PO",
    "brand": "Ford",
    "model": "Fiesta",
    "category": "COMPACT_CAR"
  }
]

# Delete a specific car
curl -X DELETE "http://localhost:8080/cars/e95d5197-36c3-4501-914a-9de3befac3a1" -v
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /cars/e95d5197-36c3-4501-914a-9de3befac3a1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 204 No Content
< Date: Sun, 13 Oct 2019 11:41:33 GMT
< connection: keep-alive
<
* Connection #0 to host localhost left intact
* Closing connection 0

Adding HTTP logs with custom log level

So far we have been manually testing our server and hoped everything was okay, but what if it wasn't ? It could be useful to add some logs so we can see how routes are matched or why they are not.

➡️ Just like any other modern JVM framework, Micronaut supports logging configuration through a simple logback.xml file. The default logging level is set to info so we simply need to add the debug level for the package we're interesed in. Open the file src/main/resources/logback.xml and add the following line inside the <configuration> element:

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

➡️ Now reissue a command to your application such as curl http://localhost:8080/cars and check the logs of your application, you should now see debug logs such as the following:

14:24:31.629 [nioEventLoopGroup-1-4] DEBUG i.m.h.server.netty.NettyHttpServer - Server localhost:8080 Received Request: GET /cars
14:24:31.630 [nioEventLoopGroup-1-4] DEBUG i.m.h.s.netty.RoutingInBoundHandler - Matching route GET - /cars
14:24:31.630 [nioEventLoopGroup-1-4] DEBUG i.m.h.s.netty.RoutingInBoundHandler - Matched route GET - /cars to controller class com.stacklabs.micronaut.workshop.agency.v1.$CarsControllerDefinition$Intercepted
14:24:31.638 [pool-1-thread-10] DEBUG i.m.h.s.netty.RoutingInBoundHandler - Encoding emitted response object [[Car(id=01b0cce0-6755-4f81-b97f-8809dd36b996, registration=GH-987-PO, brand=Ford, model=Fiesta, category=COMPACT_CAR, links=[] using codec: io.micronaut.jackson.codec.JsonMediaTypeCodec@3b362f1

Error handling

So far we tested cases where everything was going well and we always received success http responses. But how do we handle failures ? Of course we could always return HttpResponse from all our controller methods and manually handle error cases as they happen. However you sometimes want to have a transverse way of handling errors that might appear at multiple places, and keep the controller methods clean and business oriented, not using lower-level HttpResponse.

➡️ Micronaut offers a way to do just that using the @Error annotation. For example we can define a method in our controller that will intercept all JPA PersistenceException and return a Conflict status code:

@Error
public MutableHttpResponse<Object> jsonError(HttpRequest<String> request, PersistenceException constraintViolationException) {
    JsonError error = new JsonError("Duplicate record : " + constraintViolationException.getMessage())
            .link(Link.SELF, Link.of(request.getUri()));

    return HttpResponse.status(HttpStatus.CONFLICT, "Given car already exists")
            .body(error);
}

For more information you can check the official documentation on error handling.

End-to-end test

Launching the application to do manual test is useful for debug purposes and can be enough for simple applications, but for real-world apps we usually want to have automated tests, so let's write one.

➡️ For this exercice we will open our CarsControllerTest test class and test each one of our REST endpoint (and their parameters variants) and make sure that they save or return appropriate cars records.

Hints:

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.json.JSONException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;

import java.util.AbstractMap.SimpleEntry;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class CarsControllerTest {
    private EmbeddedServer server;
    private HttpClient client;
    private CarRepository repository;

    @BeforeEach
    void setup() {
        server = ApplicationContext
                .build()
                .run(EmbeddedServer.class);
        client = server.getApplicationContext().createBean(HttpClient.class, server.getURL());
        repository = server.getApplicationContext().getBean(CarRepository.class);
        repository.deleteAll();
    }

    @AfterEach
    void tearDown() {
        repository.deleteAll();
        server.close();
    }

    @Test
    void addCar() throws JSONException {
        // Add a car
        HttpResponse<String> response = addCar(createCar("FIRST-CAR", "Lada", "Model1"));
        assertThat(response.getStatus().getCode()).isEqualTo(HttpStatus.CREATED.getCode());
        JSONAssert.assertEquals(
                "{" +
                        "        \"registration\":\"FIRST-CAR\"," +
                        "        \"brand\": \"Lada\"," +
                        "        \"model\": \"Model1\"," +
                        "        \"category\": \"COMPACT_CAR\"" +
                        "}",
                response.getBody().get(),
                JSONCompareMode.LENIENT
        );
    }

    @Test
    void addCar_shouldThrowAnException_whenCarAlreadyExist() {
        // Given
        repository.save(createCar("FIRST-CAR", "Lada", "Model1"));

        // When + Then
        assertThatThrownBy(() -> addCar(createCar("FIRST-CAR", "Even other brand should fail", "Another model")))
                .hasMessageContaining("Duplicate record");
    }

    @Test
    void updateCar() {
        // Given
        UUID carId = repository.save(createCar("FIRST-CAR", "Lada", "Model1")).get().getId();

        // When
        updateCar(carId.toString(), createCar("FIRST-CAR", "Lada", "Model2"));

        // Then
        assertThat(repository.findById(carId).get().getModel()).isEqualTo("Model2");
    }

    @Test
    void findAllCars_returnEmptyList_onStartup() {
        // Test initial empty list
        SimpleEntry<HttpStatus, String> response = getCars(null, null);
        assertThat(response.getKey().getCode()).isEqualTo(HttpStatus.OK.getCode());
        assertThat(response.getValue()).isEqualTo("[]");
    }

    @Test
    void findAllCars() throws JSONException {
        // Given
        repository.save(createCar("FIRST-CAR", "Lada", "Model1"));
        repository.save(createCar("SECOND-CAR", "Renault", "Capture"));

        // When
        String body = getCars(null, null).getValue();

        // Then
        JSONAssert.assertEquals(
                "[{" +
                        "        \"registration\":\"FIRST-CAR\"," +
                        "        \"brand\": \"Lada\"," +
                        "        \"model\": \"Model1\"," +
                        "        \"category\": \"COMPACT_CAR\"" +
                        "}," +
                        "{" +
                        "        \"registration\":\"SECOND-CAR\"," +
                        "        \"brand\": \"Renault\"," +
                        "        \"model\": \"Capture\"," +
                        "        \"category\": \"COMPACT_CAR\"" +
                        "}]",
                body,
                JSONCompareMode.LENIENT
        );
    }

    @Test
    void findCarById() throws JSONException {
        // Given
        UUID carId = repository.save(createCar("FIRST-CAR", "Lada", "Model1")).get().getId();

        // When
        String body = getCars(carId, null).getValue();

        // Then
        JSONAssert.assertEquals(
                "{" +
                        "        \"registration\":\"FIRST-CAR\"," +
                        "        \"brand\": \"Lada\"," +
                        "        \"model\": \"Model1\"," +
                        "        \"category\": \"COMPACT_CAR\"" +
                        "}",
                body,
                JSONCompareMode.LENIENT
        );
    }

    @Test
    void findCarById_withUnknownId_shouldReturn404() {
        assertThatThrownBy(() -> getCars(UUID.fromString("00000000-0000-0000-0000-000000000001"), null))
                .hasMessage("Page Not Found");
    }

    @Test
    void findCarByRegistration() throws JSONException {
        // Given
        repository.save(createCar("FIRST-CAR", "Lada", "Model1"));

        // When
        String body = getCars(null, "FIRST-CAR").getValue();

        // Then
        JSONAssert.assertEquals(
                "[{" +
                        "        \"registration\":\"FIRST-CAR\"," +
                        "        \"brand\": \"Lada\"," +
                        "        \"model\": \"Model1\"," +
                        "        \"category\": \"COMPACT_CAR\"" +
                        "}]",
                body,
                JSONCompareMode.LENIENT
        );
    }

    @Test
    void findCarByRegistration_withUnknownRegistration_shouldReturn404() {
        SimpleEntry<HttpStatus, String> response = getCars(null, "an-unknown-id");
        assertThat(response.getKey().getCode()).isEqualTo(HttpStatus.OK.getCode());
        assertThat(response.getValue()).isEqualTo("[]");
    }

    @Test
    void deleteCar() {
        // Given
        UUID carId = repository.save(createCar("FIRST-CAR", "Lada", "Model1")).get().getId();

        // When
        HttpResponse<String> deletedCarResponse = deleteCar(carId.toString());
        assertThat(deletedCarResponse.getStatus().getCode()).isEqualTo(HttpStatus.NO_CONTENT.getCode());
        String body = getCars(null, null).getValue();
        assertThat(body).isEqualTo("[]");
    }

    private HttpResponse<String> addCar(Car car) {
        return client.toBlocking()
                .exchange(HttpRequest.POST("/cars", car), String.class);
    }

    private HttpResponse<String> deleteCar(String carId) {
        return client.toBlocking()
                .exchange(HttpRequest.DELETE("/cars/" + carId), String.class);
    }

    private HttpResponse<String> updateCar(String carId, Car car) {
        return client.toBlocking()
                .exchange(HttpRequest.PUT("/cars/" + carId, car), String.class);
    }

    private SimpleEntry<HttpStatus, String> getCars(UUID id, String registration) {
        String idPath = id != null ? ("/" + id) : "";
        String registrationParam = registration != null ? ("?registration=" + registration) : "";
        HttpResponse<String> response = client.toBlocking().exchange(HttpRequest.GET("/cars" + idPath + registrationParam), String.class);
        return new SimpleEntry<>(response.getStatus(), response.getBody().get());
    }

    private static Car createCar(String registration, String brand, String model) {
        return new Car(registration, brand, model, CarCategory.COMPACT_CAR);
    }
}

So we now have a working application with REST endpoints and a repository implementation. Let's now say that we need to initialize the cars database with the known agency cars. Where should we put this "loader" code ? Should we put it Application main method after the application has started ?

Well actually Micronaut provides a nice and clean way to react to HTTP server events. What we need to do here is to add a @Singleton bean that implements ApplicationEventListener with the appropriate event we want to react to, ServiceStartedEvent in our case because we want to load our data after the application has started.

➡️ The exercice is to implement this DataLoader class. You can use the following initial list for repository insertion:

Arrays.asList(
    new Car("AA-982-BB", "Tesla", "Model S", CarCategory.COUPE),
    new Car("AA-123-BB", "Ford", "Fiesta", CarCategory.COMPACT_CAR)
);
Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.service;

import com.stacklabs.micronaut.workshop.agency.config.InitialCar;
import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.discovery.event.ServiceStartedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Singleton
public class DataLoader implements ApplicationEventListener<ServiceStartedEvent> {
    private static final Logger LOG = LoggerFactory.getLogger(DataLoader.class);

    private final CarRepository repository;

    public DataLoader(CarRepository repository) {
        this.repository = repository;
    }

    @Override
    public void onApplicationEvent(ServiceStartedEvent event) {
        Arrays.asList(
            new Car("AA-982-BB", "Tesla", "Model S", CarCategory.COUPE),
            new Car("AA-123-BB", "Ford", "Fiesta", CarCategory.COMPACT_CAR)
         ).stream()
          .peek(car -> LOG.info("Going to save car with registration {}...", car.getRegistration()))
          .forEach(car -> repository.save(car)
                  .ifPresentOrElse(
                          any -> LOG.info("Successfuly saved car with registration {}...", car.getRegistration()),
                          () -> LOG.warn("Could not save car with registration {}...", car.getRegistration()))
          );
    }
}

➡️ Now run the application and ask for the car list (GET http://localhost:8080/cars), what do you see ?

Note: It can take up to a few seconds to actually see the cars that have been inserted after the application startup.

➡️ Run the tests again, what do you observe ?

Our loader loads data at application startup so our tests will also have this data in the in-memory H2 database which means this data will interfere with our test data.

To avoid this problem we should tell Micronaut to conditionnaly load our DataLoader bean. As mentioned in the documentation, there are various ways to specify a conditional bean. We can for example only load a bean if a property is present (or missing), if we are using a specific version or based on the presence/absence of other beans/classes. However in our case we can use the environment to detect if we are currently in a test context.

➡️ Add this annotation to the DataLoader test class:

@Requires(notEnv = {Environment.TEST})

➡️ Run the tests again, what do you observe ?

➡️ We can now add a basic test to make sure that our DataLoader does load initial data into our repository. However as we excluded the bean from being created in test context, we need to manually instantiate the DataLoader in our test.

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.agency.config.InitialCar;
import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import com.stacklabs.micronaut.workshop.agency.service.DataLoader;
import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;

@MicronautTest
public class DataLoaderTest {

    @Inject
    private CarRepository carRepository;

    @Inject
    private List<InitialCar> initialCars;

    @BeforeEach
    void setup() {
        carRepository.deleteAll();
    }

    @AfterEach
    void tearDown() {
        carRepository.deleteAll();
    }

    @Test
    void testDataLoader() {
        // Given
        DataLoader dataLoader = new DataLoader(carRepository, initialCars);

        // When
        dataLoader.onApplicationEvent(null);

        // Then
        assertThat(carRepository.findAll()).hasSize(2);
    }
}

We are now able to load data at startup, however we have hard-coded the initial cars list which is not really suitable in a real-world example. What we usually want to do instead is to load the cars list from either a json file or from configuration. We will now see how to load data from configuration.

➡️ The exercice is now to add an InitialCar class that will reflect our initial configuration, then modify our DataLoader to take the list from the configuration instead of our hard-coded values. When the configuration is set you should be able to read the following configuration from application.yml:

app.initial-car:
  AA-123-VV:
    brand: Renault
    model: Capture
    category: SUV
  BB-456-UU:
    brand: Ford
    model: Focus
    category: SUV

Hints:

InitialCar config solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.config;

import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import io.micronaut.context.annotation.EachProperty;
import io.micronaut.context.annotation.Parameter;

@EachProperty("app.initial-car")
public class InitialCar {
    private String registration;
    private String brand;
    private String model;
    private CarCategory category;

    public InitialCar(@Parameter String registration) {
        this.registration = registration;
    }

    public String getRegistration() {
        return registration;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getModel() {
        return model;
    }

    public void setModel(String model) {
        this.model = model;
    }

    public CarCategory getCategory() {
        return category;
    }

    public void setCategory(CarCategory category) {
        this.category = category;
    }
}

DataLoader with injected config - solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.service;

import com.stacklabs.micronaut.workshop.agency.config.InitialCar;
import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.context.event.ApplicationEventListener;
import io.micronaut.discovery.event.ServiceStartedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Singleton
@Requires(notEnv = {Environment.TEST})
public class DataLoader implements ApplicationEventListener<ServiceStartedEvent> {
    private static final Logger LOG = LoggerFactory.getLogger(DataLoader.class);

    private final CarRepository repository;
    private final List<InitialCar> initialCars;

    public DataLoader(CarRepository repository, List<InitialCar> initialCars) {
        this.repository = repository;
        this.initialCars = initialCars;
    }

    @Override
    public void onApplicationEvent(ServiceStartedEvent event) {
        initialCars.stream()
                .map(c -> new Car(c.getRegistration(), c.getBrand(), c.getModel(), c.getCategory()))
                .peek(car -> LOG.info("Going to save car with registration {}...", car.getRegistration()))
                .forEach(car -> repository.save(car)
                        .ifPresentOrElse(
                                any -> LOG.info("Successfuly saved car with registration {}...", car.getRegistration()),
                                () -> LOG.warn("Could not save car with registration {}...", car.getRegistration()))
                );
    }
}

Don't forget to fill the application.yml with the configuration given above.

➡️ Run the application again and you should see the same results but with intial list coming from configuration instead of hard-coded values.

Obvisouly we need to also inject the configuration in our tests to reflect the DataLoader change. The great thing with Micronaut is that it makes it easy to inject a different test configuration by simply overriding properties in application-test.yml.

➡️ Create a new properties file under src/test/resources/application.yml and fill it with following configuration:

app.initial-car:
  test-0:
    brand: Brand0
    model: Model0
    category: COUPE
  test-1:
    brand: Brand1
    model: Model1
    category: CONVERTIBLE
  test-2:
    brand: Brand2
    model: Model2
    category: MINIVAN
  test-3:
    brand: Brand3
    model: Model3
    category: STATION_WAGON

➡️ Finally we can modify our existing DataLoaderTest to test initial data loading against our test configuration.

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.agency.config.InitialCar;
import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import com.stacklabs.micronaut.workshop.agency.service.DataLoader;
import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;

@MicronautTest
public class DataLoaderTest {

    @Inject
    private CarRepository carRepository;

    @Inject
    private List<InitialCar> initialCars;

    @BeforeEach
    void setup() {
        carRepository.deleteAll();
    }

    @AfterEach
    void tearDown() {
        carRepository.deleteAll();
    }

    @Test
    void testDataLoader() {
        // Given
        DataLoader dataLoader = new DataLoader(carRepository, initialCars);

        // When
        dataLoader.onApplicationEvent(null);

        // Then
        assertThat(carRepository.findAll()).hasSize(2);
        // Then
        List<Car> cars = carRepository.findAll();
        assertThat(cars)
                .filteredOn(car -> car.getRegistration().startsWith("test-"))
                .hasSize(4)
                .extracting("brand", "model", "category")
                .contains(
                        tuple("Brand0", "Model0", CarCategory.COUPE),
                        tuple("Brand1", "Model1", CarCategory.CONVERTIBLE),
                        tuple("Brand2", "Model2", CarCategory.MINIVAN),
                        tuple("Brand3", "Model3", CarCategory.STATION_WAGON));
    }
}

Suppose Micronaut http logger is not enough for us and we want to add a log with more context each time a method of our controller is called. How should we do that ?

Just like any serious JVM framework, Micronaut supports Aspect Oriented Programming (AOP). We can then create an Advice to decorate our methods and intercept each call.

➡️ The exercice is to create a new LoggingAdvice that implements MethodInterceptor. Add a log with request and parameters.

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.aop;

import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;

import static java.util.stream.Collectors.joining;

@Singleton
public class LoggingAdvice<T, V> implements MethodInterceptor<T, V> {
    private static final Logger LOG = LoggerFactory.getLogger(LoggingAdvice.class);

    @Override
    public V intercept(MethodInvocationContext<T, V> context) {
        String attributes = context.getAttributes()
                .asMap().entrySet()
                .stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(joining(",", "[", "]"));
        String parameters = context.getParameters()
                .entrySet()
                .stream()
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(joining(",", "[", "]"));
        LOG.info("In {}.{} with attributes {} and params {}...",
                context.getTargetMethod().getDeclaringClass().getSimpleName(),
                context.getMethodName(),
                attributes,
                parameters);

        return context.proceed();
    }
}

➡️ Now that we defined our AOP Advice, we need to use it on our controller methods. To simplify the use of our Advice we can create a custom annotation Logged.

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.aop;

import io.micronaut.aop.Around;
import io.micronaut.context.annotation.Type;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Retention(RUNTIME)
@Target(ElementType.METHOD)
@Around
@Type(LoggingAdvice.class)
public @interface Logged {

}

➡️ Finally, add the @Logged annotation to each controller method:

Solution. Click to expand!
@Logged
@Get("/{?registration}")
public List<Car> findAll(@QueryValue("registration") Optional<String> registration) {
    // Method implementation...
}

...

➡️ Remove the specific http logger from logback.xml, run the application and interact with its endpoints, then check the logs. You should outputs similar to the following one:

17:47:08.341 [pool-1-thread-4] INFO  c.s.m.w.agency.aop.LoggingAdvice - In CarsController.findAll with attributes [] and params [registration=Optional[AA-123-VV]]...

Let's say we now want to add a RentalsController that will expose a endpoint to calculate the price of a car rental for a specific car.

Here is the code for this RentalsController:

package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import com.stacklabs.micronaut.workshop.api.v1.model.RentalOptions;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.QueryValue;

import java.util.Optional;
import java.util.UUID;

@Controller("/cars/{id}/rentals")
public class RentalsController {

    private final CarRepository repository;
    private final PriceCalculatorClient calculator;

    public RentalsController(CarRepository repository, PriceCalculatorClient calculator) {
        this.repository = repository;
        this.calculator = calculator;
    }

    @Get("/_price{?nbDays,kilometers,driverAge,insurance}")
    public HttpResponse<String> calculateRentalPrice(
            @PathVariable("id") UUID carId,
            @QueryValue("nbDays") Optional<Integer> nbDays,
            @QueryValue("kilometers") Optional<Integer> kilometers,
            @QueryValue("driverAge") Optional<Integer> driverAge,
            @QueryValue("insurance") Optional<Boolean> insurance) {
        return nbDays
                .flatMap(_nbDays -> kilometers
                .flatMap(_kilometers -> driverAge
                .flatMap(_driverAge -> repository.findById(carId)
                .map(_car -> new RentalOptions(_car.getCategory(), _nbDays, _driverAge, _kilometers, insurance.orElse(false))))))
                .flatMap(options -> Optional.ofNullable(calculator.calculate(options).blockingGet()))
                .map(price -> HttpResponse.ok("This will cost you: " + price + "€"))
                .orElseGet(HttpResponse::notFound);
    }
}

As you can see, we need to inject a PriceCalculatorClient that you will now create. As per the documentation, we simply need to declare an interface annotated @Client and define the methods we will use to interact with the distant price-calculator service. As the methods are part of the contract between the client and server, they are already defined in the api module under the PriceCalculatorOperations interface.

➡️ Add the PriceCalculatorClient interface:

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.api.v1.PriceCalculatorOperations;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.retry.annotation.Retryable;

@Client("price-calculator")
public interface PriceCalculatorClient extends PriceCalculatorOperations {

}

There are many ways to define the actual url (or set of urls) of the distant server, but here we chose to simply declare the client name and define its distant server urls in the app configuration (in application.yml):

Solution. Click to expand!
micronaut:
  http:
    services:
      price-calculator:
        urls:
          - http://url-to-price-calculator-somewhere

Now add the RentalsControllerTest to make sure everything is working:

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.agency.domain.Car;
import com.stacklabs.micronaut.workshop.agency.persistence.CarRepository;
import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class RentalsControllerTest {
    private EmbeddedServer server;
    private HttpClient client;
    private CarRepository repository;

    @BeforeEach
    void setup() {
        server = ApplicationContext
                .build()
                .run(EmbeddedServer.class);
        client = server.getApplicationContext().createBean(HttpClient.class, server.getURL());
        repository = server.getApplicationContext().getBean(CarRepository.class);
        repository.deleteAll();
    }

    @AfterEach
    void tearDown() {
        repository.deleteAll();
        server.close();
    }

    @Test
    void getPrice() {
        // Given
        UUID carId = repository.save(createCar("FIRST-CAR", "Lada", "Model1")).get().getId();

        // When
        String response = client.toBlocking()
                .retrieve("/cars/" + carId + "/rentals/_price?nbDays=5&driverAge=35&kilometers=500&insurance=true");

        // Then
        assertThat(response).isEqualTo("This will cost you: 10.0€");
    }

    @Test
    void getPrice_forUnknownCar_return404() {
        // Given
        String carId = "00000000-0000-0000-0000-000000000000";

        // When + Then
        assertThatThrownBy(() ->
                client.toBlocking()
                        .retrieve("/cars/" + carId + "/rentals/_price?nbDays=5&driverAge=35&kilometers=500&insurance=true"))
                .isInstanceOf(Exception.class);
    }

    @Test
    void getPrice_withoutParameters_return404() {
        // Given
        UUID carId = repository.save(createCar("FIRST-CAR", "Lada", "Model1")).get().getId();

        // When + Then
        assertThatThrownBy(() -> client.toBlocking().retrieve("/cars/" + carId + "/rentals/_price?nbDays=5&insurance=true"))
                .isInstanceOf(Exception.class);
    }

    private Car createCar(String registration, String brand, String model) {
        return new Car(registration, brand, model, CarCategory.COMPACT_CAR);
    }
}

➡️ Run the tests, what do you observe ?

Of course the tests are failing because the server does not exist (yet) and anyway our test should not depend on an external server that might not be available in the development environement. Fortunately Micronaut provides an easy way to define client fallbacks that can either be used in production or in our case in tests context.

➡️ Can you add the appropriate to make the tests green ? Hint: you need to add retry properties to indicate how many times and how often to retry a failed request...

PriceCalculatorClient solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.api.v1.PriceCalculatorOperations;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.retry.annotation.Retryable;

@Client("price-calculator")
@Retryable(attempts = "${price-calculator.retry.attempts:3}", delay = "${price-calculator.retry.delay:1s}")
public interface PriceCalculatorClient extends PriceCalculatorOperations {

}

Also you will need to add an actual client fallback in your tests:

PriceCalculatorClientMock solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency;

import com.stacklabs.micronaut.workshop.api.v1.PriceCalculatorOperations;
import com.stacklabs.micronaut.workshop.api.v1.model.RentalOptions;
import io.micronaut.retry.annotation.Fallback;
import io.reactivex.Single;

@Fallback
public class PriceCalculatorClientMock implements PriceCalculatorOperations {
    @Override
    public Single<Float> calculate(RentalOptions options) {
        return Single.just(10F);
    }
}

Finally, define the actual values of the retry properties:

application-test.yml solution. Click to expand!
priceCalculator:
  retry:
    attempts: 1
    delay: 1ms

➡️ Run the tests again, what do you see now ?

It's time to test the real application, don't you think ?

➡️ First modify the application.yml and enter the real service url:

micronaut:
  http:
    services:
      price-calculator:
        urls:
          - http://REPLACE-WITH-REAL-CLOUD-RUN-URL

➡️ Launch the rental-agency app again and test that you can actually get the price of a rental:

curl "http://localhost:8080/cars"
==> Get one of the cars id
curl "http://localhost:8080/cars/{aCarId}/rentals/_price?nbDays=10&kilometers=1000&driverAge=45"
This will cost you: 300.0€

Congrats, you have fully tested your first microservice with a good number of interesting features !

Micronaut provides an easy way to manage and monitor our applications through both built-in and custom management endpoints. Each provided endpoint can be either deactivated, or allowed only to authenticated users of our security system.

Built-in endpoints

➡️ Let's test a few endpoints. Start by verifying that management & metrics dependencies have been added to rental-agency pom.xml:

<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-management</artifactId>
</dependency>
compile "io.micronaut.configuration:micronaut-micrometer-core"

➡️ Now run the application

./mvnw exec:exec -Dskip.agency.run=false

➡️ Send a curl request or open your browser and point it to http://localhost:8080/health, what do you see ?

The UP information is useful but lacks a little detail. Micronaut actually provides a more detailed view of which healthchecks have been run, but is restricted to authenticated (and authorized) users by default.

For the sake of this exercice, let's make the details of the health endpoint visible to everyone (in application.yml):

endpoints:
  health:
    enabled: true
    sensitive: false
    details-visible: ANONYMOUS

➡️ Run the application again and go back to health endpoint, you should now see a detailed view of application health.

➡️ Using the same application, point to http://localhost:8080/metrics and look at all the available metrics. An interesting thing to note here is that we have hikaricp.connections metrics coming from Micronaut jdbc driver which just "plugged" itself to build-in metrics, along with the default ones such as jvm.memory.used or system.cpu.usage.

➡️ Look at the http://localhost:8080/metrics/http.server.requests endpoint, how many requests have we made so far ? Let's have fun and generate more requests by executing generate-requests.sh script at the root of the project:

./generate-requests.sh

➡️ Now let's test have another look at the same endpoint, what do you see ?

We just saw two different endpoints that are enabled and open by default.

➡️ Use curl to test the beans endpoint:

curl -v http://localhost:8080/beans

What do you observe ?

Extending endpoints

Built-in endpoints are great but we often need to extend them in order to provide, for example, an additional check to existing healthcheck or other metrics that we need to report. Micronaut provides an easy way to do that by simply extending AbstractHealthIndicator class and modifying the status depending on anything we want.

➡️ The exercice is now to make sure that the global healthcheck also includes the state of our rental-calculator client by adding a new HealthCheck bean CalculatorHealthIndicator that will do a sample request to the calculator server.

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.agency.config;

import com.stacklabs.micronaut.workshop.agency.PriceCalculatorClient;
import com.stacklabs.micronaut.workshop.api.v1.model.CarCategory;
import com.stacklabs.micronaut.workshop.api.v1.model.RentalOptions;
import io.micronaut.health.HealthStatus;
import io.micronaut.management.health.indicator.AbstractHealthIndicator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;
import java.util.Collections;
import java.util.Map;

@Singleton
public class CalculatorHealthIndicator extends AbstractHealthIndicator<Map<String, String>> {

    private static final Logger LOG = LoggerFactory.getLogger(CalculatorHealthIndicator.class);
    private PriceCalculatorClient calculatorClient;

    public CalculatorHealthIndicator(PriceCalculatorClient calculatorClient) {
        this.calculatorClient = calculatorClient;
    }

    @Override
    protected Map<String, String> getHealthInformation() {
        long before = System.currentTimeMillis();
        sampleRequest();
        long duration = System.currentTimeMillis() - before;
        return Collections.singletonMap("responseTime", duration + " ms");
    }

    @Override
    protected String getName() {
        return "calculator-health";
    }

    private HealthStatus sampleRequest() {
        return calculatorClient
                .calculate(new RentalOptions(CarCategory.COMPACT_CAR, 1, 50, 1000, false))
                .map(any -> HealthStatus.UP)
                .doOnSuccess(any -> LOG.info("Successful request made do rental-calculator server..."))
                .doOnError(any -> healthStatus = HealthStatus.DOWN)
                .doOnError(any -> LOG.error("Unable to reach rental-calculator server !"))
                .blockingGet();
    }
}

➡️ Re-run the application and display app health:

curl -v http://localhost:8080/health

We can now see our custom health indicator !

Accessing secured endpoints

Some endpoints are, by default, only accessible to authenticated (and authorized users). Of course we could always modify the endpoint configuration to make it accessible to everyone, but usually we want to keep this protection so we need to setup authentication.

We will not see Security in this workshop. If you want to temporarily enable access to all endpoints such as /beans endpoint we tested so far (and was unauthorized), you can always activate the ugly "open-bar-for-all" in the configuration:

endpoints:
  all:
    enabled: true
    sensitive: false

👁 But hey, promise you won't do that in production ! 👮‍🔒

So far we have only be using blocking APIs such as database APIs, hence making all the application blocking. Micronaut however natively supports reactive programing as it is build on Netty which is "designed around an Event loop model and non-blocking I/O".

In this chapter we will create the module rental-registry responsible for storing and retrieving agencies to/from a MongoDB database.

Creating the reactive application

Let's now switch to rental-registry app to start coding a reactive app. This app has been generated using Micronaut cli with feature mongo-reactive.

➡️ First open rental-registry pom.xml and make sure we have the mongo-reactive dependency and an embedded mongo for tests :

dependency>
    <groupId>io.micronaut.configuration</groupId>
    <artifactId>micronaut-mongo-reactive</artifactId>
</dependency>
<dependency>
    <groupId>de.flapdoodle.embed</groupId>
    <artifactId>de.flapdoodle.embed.mongo</artifactId>
    <scope>test</scope>
</dependency>

➡️ Move to this project:

cd rental-registry

➡️ Just like we did for rental-agency module, let's add our AgencyController using Micronaut cli:

mn create-controller Agency
| Rendered template Controller.java to destination src/main/java/com/stacklabs/micronaut/workshop/registry/AgencyController.java
| Rendered template Test.java to destination src/test/java/com/stacklabs/micronaut/workshop/registry/AgencyControllerTest.java

Mongo configuration

➡️ Open src/main/resources/application.yml and notice that Micronaut already configured the connection uri. We will modify this uri to make it point to our local dockerized MongoDB instance, and we will also add a custom configuration for database and collection names. Modify the file to reflect the following:

micronaut:
  application:
    name: rental-registry
  server:
    port: 8082
---
mongodb:
  uri: "mongodb://${MONGO_HOST:localhost}:${MONGO_PORT:27017}"
---
app:
  agency:
    databaseName: micronaut
    collectionName: agencies

➡️ Let's add the corresponding AgencyConfiguration class:

package com.stacklabs.micronaut.workshop.registry.config;

import io.micronaut.context.annotation.ConfigurationProperties;

import javax.validation.constraints.NotBlank;

@ConfigurationProperties("app.agency")
public interface AgencyConfiguration {
    @NotBlank
    String getDatabaseName();

    @NotBlank
    String getCollectionName();
}

➡️ As you can see we are relying on a local Mongo instance, so please launch one in the background:

docker run --name mongo -p 27017:27017 -d mongo:latest

Adding Mongo reactive service

➡️ Your job is now to add a reactive MongoService bean. This bean will inject the AgencyConfiguration and a MongoClient (provided by Micronaut mongo-reactive module) and should implement at least the following methods :

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.registry.service;

import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.stacklabs.micronaut.workshop.api.v1.AgencyOperations;
import com.stacklabs.micronaut.workshop.api.v1.model.Agency;
import com.stacklabs.micronaut.workshop.registry.config.AgencyConfiguration;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.Single;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Singleton;

import java.util.UUID;

import static com.mongodb.client.model.Filters.eq;

@Singleton
public class MongoService implements AgencyOperations {
    private static final Logger LOG = LoggerFactory.getLogger(MongoService.class);

    private final AgencyConfiguration agencyConfiguration;
    private final MongoClient mongo;

    public MongoService(AgencyConfiguration agencyConfiguration, MongoClient mongo) {
        this.agencyConfiguration = agencyConfiguration;
        this.mongo = mongo;
    }

    public Maybe<Agency> getById(String id) {
        return Flowable.fromPublisher(
                getCollection()
                        .find(eq("uuid", id))
                        .limit(1)
        ).firstElement();
    }

    public Flowable<Agency> list() {
        return Flowable.fromPublisher(getCollection().find());
    }

    public Single<Agency> save(Agency agency) {
        LOG.info("Saving {} agency in Mongo...", agency);
        return Single.just(agency)
                .map(a -> new Agency(UUID.randomUUID().toString(), a.getName()))
                .flatMap(a -> Single.fromPublisher(getCollection().insertOne(a))
                        .doOnSuccess(any -> LOG.info("Successfuly saved {} agency to Mongo...", a))
                        .map(any -> a));
    }

    public Single<String> deleteAll() {
        LOG.info("Deleting entire {} collection from Mongo...", agencyConfiguration.getCollectionName());
        return Single.fromPublisher(getCollection().drop())
                .map(success -> "Successfuly deleted entire $ from Mongo...");
    }

    private MongoCollection<Agency> getCollection() {
        return mongo
                .getDatabase(agencyConfiguration.getDatabaseName())
                .getCollection(agencyConfiguration.getCollectionName(), Agency.class);
    }
}

Adding a non-blocking controller

➡️ Now that we have our MongoService, implement the AgencyController that we previously created with Micronaut command line. This controller should implement AgencyOperations from api module and should inject the MongoService. It should be straight-forward as the only thing it needs to do is to call lower MongoService corresponding methods.

Solution. Click to expand!
package com.stacklabs.micronaut.workshop.registry;

import com.stacklabs.micronaut.workshop.api.v1.AgencyOperations;
import com.stacklabs.micronaut.workshop.api.v1.model.Agency;
import com.stacklabs.micronaut.workshop.registry.service.MongoService;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.Single;

import java.util.concurrent.TimeUnit;

@Controller(value = "/agencies")
class AgencyController implements AgencyOperations {

    private MongoService mongoService;

    public AgencyController(MongoService mongoService) {
        this.mongoService = mongoService;
    }

    public Maybe<Agency> getById(String id) {
        return mongoService.getById(id);
    }

    public Flowable<Agency> list() {
        return mongoService.list();
    }

    public Single<Agency> save(Agency agency) {
        return mongoService.save(agency);
    }
}

Running the app

➡️ Its time to run the app and test it !

# From workshop repository root 
./mvnw exec:exec -Dskip.registry.run=false
curl http://localhost:8082/agencies
curl -X POST "http://localhost:8082/agencies" -H "Content-Type:application/json" -d '{"name": "stack-labs-agency"}' | jq
curl -X POST "http://localhost:8082/agencies" -H "Content-Type:application/json" -d '{"name": "olivier-agency"}' | jq
curl http://localhost:8082/agencies | jq

What do you see ?

Of course output seems very similar to the blocking one because browser waits for the end of the flowable and then closes the connection. In this case Mongo request is pretty fast so we don't see much.

➡️ Add this code snippet to your controller to emit the same flowable with a 1 second delay between each record:

@Get("/listWithDelay")
public Flowable<Agency> listOneSecondEach() {
    return Flowable.zip(
            mongoService.list(),
            Flowable.interval(1, TimeUnit.SECONDS),
            (agency, aLong) -> agency
    );
}

➡️ Let's run the app and test our new endpoint:

curl --raw http://localhost:8082/agencies/listWithDelay

Better, isn't it ?

"Classical" JVM compilation

Simple Jar package

Until then applications were compiled with the JDK and usually packaged as a (fat) jar that was running on the JVM.

We've done that many times during this workshop with our rental-agency application, that's what ./mvnw exec:exec -Dskip.agency.run=false was doing for us behind the scenes.

➡️ If we wanted to do that in a more formal way to deliver the jar somewhere, we would instead package our app using Maven:

./mvnw package -pl rental-agency -am
ls rental-agency/target
original-rental-agency-0.1.jar rental-agency-0.1.jar

➡️ This jar could later be executed with java command:

java -jar rental-agency/target/rental-agency-0.1.jar

Docker packaging

Generating a jar is great but we usually generate container images nowadays, and that's cool because Micronaut already ships a Dockerfile in our application when created with mn cli.

➡️ Let's simply modify rental-registry/Dockerfile to expose the port we use in this application:

FROM adoptopenjdk/openjdk11-openj9:jdk-11.0.1.13-alpine-slim
COPY target/rental-registry-*.jar rental-registry.jar
EXPOSE 8082
CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar rental-registry.jar

➡️ We can now build our image and run it:

./mvnw package -pl rental-registry -am
docker build -t rental-registry rental-registry/
docker run -p 8082:8082 rental-registry

GraalVM nativeimage compilation

Micronaut supports GraalVM which is a new universal virtual machine from Oracle that supports a polyglot runtime environment and the ability to compile Java applications down to native machine code.

To generate a native image of our application we will use GraalVM's nativeimage tool. Compiling to native code requires that we do not have reflection, which Micronaut guarantees for micronaut-core, however some third-party libraries could be incompatible with native compilation.

For more information on using Micronaut with GraalVM please refer to the documentation. In our case we will simply use Micronaut-provided Dockerfile to compile our rental-calculator app (which is just a simple http-server) for which we declared using the graal-native-image feature. This feature is important as it provides a few useful files for building native images.

➡️ Have a look at the multi-stages Dockerfile of rental-calculator application:

#FROM oracle/graalvm-ce:19.3.1-java8 as graalvm
FROM oracle/graalvm-ce:19.3.1-java11 as graalvm
RUN gu install native-image

COPY . /home/app/rental-calculator
WORKDIR /home/app/rental-calculator

RUN native-image --no-server --static -cp target/rental-calculator-*.jar

FROM scratch
EXPOSE 8080
COPY --from=graalvm /home/app/rental-calculator/rental-calculator /app/rental-calculator
ENTRYPOINT ["/app/rental-calculator", "-Djava.library.path=/app"]

➡️ Some modules need an extra-configuration to explicitely avoid using reflection. This is the case for jackson for which we specifically need to tell to use bean-introspection-module by adding this configuration to rental-calculator/src/main/resources/application.yml:

jackson:
  bean-introspection-module: true

➡️ We can now build our image and run it:

./mvnw package -pl rental-calculator -am
cd rental-calculator
./docker-build.sh

You should see an output similar to this one:

Sending build context to Docker daemon  13.31MB
Step 1/9 : FROM oracle/graalvm-ce:19.3.1-java11 as graalvm
 ---> 1515e8e21105
Step 2/9 : RUN gu install native-image
 ---> Using cache
 ---> b6563574167c
Step 3/9 : COPY . /home/app/rental-calculator
 ---> 4b6a3fa98b6f
Step 4/9 : WORKDIR /home/app/rental-calculator
 ---> Running in 474d0ecc88cb
Removing intermediate container 474d0ecc88cb
 ---> 5cb08d324413
Step 5/9 : RUN native-image --no-server --static -cp target/rental-calculator-*.jar
 ---> Running in 2e31dc818431
[rental-calculator:19]    classlist:  16,373.81 ms
[rental-calculator:19]        (cap):   2,447.53 ms
[rental-calculator:19]        setup:   6,566.57 ms
[rental-calculator:19]   (typeflow): 115,996.67 ms
[rental-calculator:19]    (objects):  62,873.89 ms
[rental-calculator:19]   (features):   3,834.40 ms
[rental-calculator:19]     analysis: 188,041.75 ms

[rental-calculator:19]     (clinit):   2,250.67 ms
[rental-calculator:19]     universe:   5,060.14 ms
[rental-calculator:19]      (parse):  15,492.16 ms
[rental-calculator:19]     (inline):  18,358.25 ms
[rental-calculator:19]    (compile):  91,946.78 ms
[rental-calculator:19]      compile: 129,173.84 ms
[rental-calculator:19]        image:   4,947.52 ms
[rental-calculator:19]        write:   2,697.31 ms
[rental-calculator:19]      [total]: 353,763.39 ms
Removing intermediate container ce8f8e7aaba6
 ---> e3b62e1e6029
Step 6/9 : FROM frolvlad/alpine-glibc
 ---> 914aed4f321e
Step 7/9 : EXPOSE 8080
 ---> Running in a08bbaf3629b
Removing intermediate container a08bbaf3629b
 ---> 33471eb2f4df
Step 8/9 : COPY --from=graalvm /home/app/rental-calculator .
 ---> 3d6c44361487
Step 9/9 : ENTRYPOINT ["./rental-calculator"]
 ---> Running in 6c891b8623c3
Removing intermediate container 6c891b8623c3
 ---> ad2fa3c7a8a3
Successfully built ad2fa3c7a8a3
Successfully tagged rental-calculator:latest

To run the docker container execute:
    $ docker run -p 8080:8080 rental-calculator

Yes, native compilation takes a long time.

➡️ Once our image is (finally) built we can simply run it:

docker run -p 8080:8080 rental-calculator

Notice how fast the server is starting: that is the great advantage of native images !

18:37:30.593 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 23ms. Server Running: http://0634e4a6b68a:8080

The great thing about Micronaut is that it offers many ways to build, package and deploy applications to a variety of platforms.

If we take Google Cloud Platform (GCP) as an example, we could imagine:

In the end of this workshop I'll be simply showing how simply it is to combine the GraalVM native image support of Micronaut and its deployment to Google Cloud Run.

Just as a reminder, to build a container with our app compiled to native code (i.e. no JVM needed to run the binary), we just executed a Maven build and Micronaut-provided Docker build, as follows:

./mvnw package -pl rental-calculator -am
cd rental-calculator
./docker-build.sh

Now if I want to deploy to cloud run I simply need to send my container image to Google Container Registry:

docker tag rental-calculator-graal eu.gcr.io/micronaut/rental-calculator:0.1
docker push eu.gcr.io/micronaut/rental-calculator:0.1

Now that we have uploaded our image to Google Container Registry, we simply need to deploy a cloud run revision:

gcloud beta run deploy --image eu.gcr.io/micronaut/rental-calculator:0.1 --platform managed

Service name (rental-calculator):
Deploying container to Cloud Run service [rental-calculator] in project [micronaut] region [europe-west1]
✓ Deploying... Done.
  ✓ Creating Revision...
  ✓ Routing traffic...
Done.
Service [rental-calculator] revision [rental-calculator-4ntgt] has been deployed and is serving 100 percent of traffic at https://rental-calculator-mypersonalid-ew.a.run.app

And voilaaa !

Of course we can test our newly deployed app:

curl -X POST "https://rental-calculator-mypersonalid-ew.a.run.app/calculate" -H "Content-Type:application/json" -d '{"carCategory":"COMPACT_CAR","nbDays":10,"driverAge":30,"kilometers":520,"insurance":false}'
228.00002

Pretty easy, right ?