Fork me on GitHub

Introductory workshop about Micronaut.

Software Requirements

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

  • Linux or MacOS with shell access, and the following installed:

    • curl.

    • wget.

    • unzip.

    • git.

  • JDK 8.

  • Docker. Please pull the following images before attending the workshop:

    • consul.

    • mongo.

Micronaut CLI

  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.0.0.M3
    | JVM Version: 1.8.0_131

Clone this repository

Once done, you can clone this repo:

git clone https://github.com/alvarosanchez/micronaut-workshop.git
You will find each exercise’s template files on each exNN folder. Solution is always inside a solution folder. To highlight the actions you actually need to perform, an icon is used:

Application architecture

Throughout this workshop, we will be creating a football (soccer) management system.

football diagram
  • clubs is the microservice responsible for managing clubs. It uses GORM for Hibernate as a data access layer.

  • fixtures manages all game fixtures, storing its data in MongoDB. For the teams playing in a game, it doesn’t store their full details, but rather their ID. It has a service-discovery-enabled HTTP client to fetch club details from the clubs microservice.

1. Getting started with the Micronaut CLI (25 minutes)

Change to the ex01 directory to work on this exercise

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 typically 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.

1.1. Listing profiles (3 minutes)

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

$ mn list-profiles
| Available Profiles
--------------------
* function-aws - The function profile for AWS Lambda
* function - The function profile
* federation - The federation profile
* service - The service profile
* base - The base profile
The Micronaut team is actively working on new profiles, and eventually they will be available.

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.

1.2. Getting information about a profile (2 minutes)

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
  help               Prints help information for a specific command

| Provided Features:
--------------------
  annotation-api    Adds Java annotation API
  config-consul     Adds support for Distributed Configuration with Consul (https://www.consul.io)
  discovery-consul  Adds support for Service Discovery with Consul (https://www.consul.io)
  discovery-eureka  Adds support for Service Discovery with Eureka
  groovy            Creates a Groovy application
  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
  jrebel            Adds support for class reloading with JRebel (requires separate JRebel installation)
  junit             Adds support for the JUnit testing framework
  kafka             Adds support for Kafka
  kotlin            Creates a Kotlin application
  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
  picocli           Adds support for command line parsing (http://picocli.info)
  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 framewokr
  spock             Adds support for the Spock testing framework
  springloaded      Adds support for class reloading with Spring-Loaded
  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)

1.3. Creating and running a hello galaxy (15 minutes)

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

Table 1. Create-App Flags
Flag Description Example

build

Build tool (one of gradle, maven - default is gradle)

--build maven

profile

Profile to use for the project (default is service)

--profile function-aws

features

Features to use for the project, comma-separated

--features security-jwt,mongo-gorm

inplace

If present, generates the project in the current directory (project name is optional if this flag is set)

--inplace

Let’s create a hello galaxy project:

$ mn create-app hello-galaxy --features groovy
| Application created at /private/tmp/hello-galaxy

Now, move into the generated hello-galaxy folder and let’s create a controller:

$ mn create-controller hello
| Rendered template Controller.groovy to destination src/main/groovy/hello/galaxy/HelloController.groovy
| Rendered template ControllerSpec.groovy to destination src/test/groovy/hello/galaxy/HelloControllerSpec.groovy

Open the generated HelloController.groovy with your favourite IDE and make it return "Hello Micronauts!":

@Get("/")
String index() {
    return "Hello Galaxy!"
}

Now, run the application:

$ MICRONAUT_SERVER_PORT=8080 ./gradlew run
Micronaut by default runs on a random port. This helps running multiple instances of a service. However, the port can be easily fixed by setting a configuration variable, or simply by exposing an environment variable as we did with MICRONAUT_SERVER_PORT=8080

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, on another shell, make a request to your service:

$ curl 0:8080/hello
Hello Galaxy!

1.4. Write an automated test (5 minutes)

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

Change the generated src/test/groovy/hello/galaxy/HelloControllerSpec.groovy to look like this:

class HelloControllerSpec extends Specification {

    @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) (1)
    @Shared @AutoCleanup RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) (2)

    void "test index"() {
        given:
        String response = client.toBlocking().retrieve("/hello") (3)

        expect:
        response == "Hello Galaxy!"
    }
}
1 Running an embedded server
2 Obtaining a reactive HTTP client attached to our embedded server
3 As the client is non-blocking by default, we want to block in tests to make sure we get a result before the test finishes. Also, the retrieve method returns the response as a String.

Then, run the tests:

./gradlew test

Once finished, you should see an output similar to:

BUILD SUCCESSFUL in 6s

2. Creating the Clubs microservice (70 minutes)

Change to the ex02/clubs directory to work on this exercise. The project has been already created for you, no need to run mn create-app this time.

In this exercise we are creating the clubs microservice.

2.1. GORM layer (15 minutes)

Let’s define first a Club domain class under src/main/groovy/clubs/domain/Club.groovy with 2 string attributes: name (mandatory) and stadium (optional).

Unlike Grails, when using GORM in Micronaut you need to annotate your entities with grails.gorm.annotation.Entity, as in Micronaut there is no conventional folder such as grails-app/domain.

Next, define a GORM data service named ClubService as an interface with the following operations:

int count()
Club save(@NotBlank String name, @NotBlank String stadium)
List<Club> findAll()
Club find(@NotNull Long id)
GORM Data Services are annotated with grails.gorm.services.Service, taking as an argument the entity they operate with. In this case, it would be @Service(Club).

Now, let’s test our service:

@Rollback (1)
class ClubServiceSpec extends Specification {

    @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
    @Shared ClubService service = embeddedServer.applicationContext.getBean(ClubService) (2)

    void "it can work with clubs"() {
        expect:
        service.count() == 0

        when:
        service.save("Real Madrid CF", "Bernabeu")

        then:
        service.count() == 1

        when:
        List<Club> clubs = service.findAll()

        then:
        clubs.size() == 1
    }

    void "it can show a club"() {
        given:
        Club club = service.save("CD Leganes", "Butarque")

        when:
        service.find(club.id)

        then:
        club.name == "CD Leganes"
        club.stadium == "Butarque"
    }
}
1 grails.gorm.transactions.Rollback applies a transaction that always rolls back.
2 Instead of any kind of injection, we simply get the bean from the application context.

2.2. REST API (30 minutes)

Micronaut helps you writing both the client and server sides of a REST API. In this service, we are going to create the following:

clubs diagram

Create the ClubsApi interface, annotating its methods with io.micronaut.http.annotation.Get as described in the diagram.

Then, create ClubsClient by simply extending from ClubsApi. Annotate the interface with io.micronaut.http.client.Client("/").

Finally, implement the controller ClubController. Annotate the class with io.micronaut.http.annotation.Controller("/"), matching the path specified on ClubsClient. Use ClubService to implement the actions by declaring a constructor dependency on it.

The controller actions need to be annotated with io.micronaut.http.annotation.Get again.

Finally, configure logback.xml to see some relevant output

<configuration>

    <!-- Default settings ... -->

    <logger name="clubs" level="DEBUG"/> (1)
    <logger name="io.micronaut.http.client" level="TRACE"/> (2)

</configuration>
1 Debug level for our code
2 This allows to see the HTTP request and responses from the HTTP clients.

Once you have it, write a test for everything:

class ClubControllerSpec extends Specification {

    @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
    @Shared ClubsClient client = embeddedServer.applicationContext.getBean(ClubsClient)
    @Shared ClubService service = embeddedServer.applicationContext.getBean(ClubService)
    @Shared Long id

    @Transactional (1)
    void setupSpec() {
        id = service.save("Real Madrid CF", "Santiago Bernabeu").id
        service.save("FC Barcelona", "Camp Nou")
    }

    @Transactional (1)
    void cleanupSpec() {
        Club.list()*.delete()
    }

    void "test index"() {
        when:
        List<Club> response = client.listClubs()

        then:
        response.size() == 2
    }

    void "test find one"() {
        when:
        Club club = client.show(id)

        then:
        club.name == 'Real Madrid CF'
        club.stadium == 'Santiago Bernabeu'
    }
}
1 Wrap write operations with grails.gorm.transactions.Transactional

2.3. Load some data for production (15 minutes)

During our tests, we have been seeding test data on demand, as it is a good practise to isolate test data from test to test. However, for production, we want some data loaded

Let’s create a bean to load some data. Run:

mn create-bean dataLoader

Change it to look like:

@Slf4j
@CompileStatic
@Singleton (1)
@Requires(notEnv = Environment.TEST) (2)
class DataLoader implements ApplicationEventListener<ServerStartupEvent> { (3)

    final ClubService clubService

    DataLoader(ClubService clubService) {
        this.clubService = clubService
    }

    @Override
    void onApplicationEvent(ServerStartupEvent event) { (4)
        if (!clubService.count()) {
            log.debug "Loading sample data"
            clubService.save("Real Madrid CF", "Santiago Bernabeu")
            clubService.save("FC Barcelona", "Camp Nou")
            clubService.save("CD Leganes", "Butarque")
            clubService.save("Getafe CF", "Coliseum")
        }
    }
}
1 javax.inject.Singleton will tell Micronaut to manage a single instance in the application context.
2 With io.micronaut.context.annotation.Requires, we ensure this runs on production, which can be specified as not running under tests.
3 Make the bean a io.micronaut.context.event.ApplicationEventListener of an io.micronaut.runtime.server.event.ServerStartupEvent event.
4 Implement the method loading some sample data.

Now, run the application:

./gradlew run

You should see an output similar to:

03:05:56.704 [main] DEBUG clubs.init.DataLoader - Loading sample data

2.4. Register the service in Consul (10 minutes)

We want the clubs microservice to be discoverable by the fixtures service. So we will enable Micronaut’s Consul support for service discovery.

First, add the neccessary dependency in build.gradle:

compile "io.micronaut:discovery-client"

Then, change src/main/resources/application.yml to define the Consul configuration:

---
consul:
  client:
    registration:
      enabled: true
  defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

Finally, run a Consul instance with Docker:

$ docker run -d --name=dev-consul -e CONSUL_BIND_INTERFACE=eth0 -e CONSUL_UI_BETA=true -p 8500:8500 consul

Now, if you run the application, you will see it registers with Consul at startup:

$ ./gradlew run
...
04:20:09.501 [nioEventLoopGroup-1-3] INFO  i.m.d.registration.AutoRegistration - Registered service [clubs] with Consul
...

If you go the Consul UI, you can see it shows as registered:

consul

You can run yet another instance of clubs on a different shell, and see it registered. We will use them both with Micronaut’s load-balanced HTTP client in the next exercise.

3. Creating the Fixtures microservice (70 minutes)

Change to the ex03/fixtures directory to work on this exercise.

In this exercise we are creating the fixtures microservice.

3.1. Data layer (35 minutes)

First of all, run MongoDB with Docker:

$ docker run -d --name=dev-mongo -p 27017:27017 mongo

Then, create the Fixture domain class with the following properties:

ObjectId id
Long homeClubId
Long awayClubId

Short homeScore
Short awayScore

Date date

As you can see, we are only storing club’s ids. When rendering fixture details, we will use Micronaut’s HTTP client to fetch details from the clubs microservice.

The next thing we need is an HTTP client for the clubs microservice. Create one with:

$ mn create-client clubs

Before actually mapping any endpoint, we are going to create the following hierarchy:

clients diagram
  • ClubsApi is the interface that contains the client endpoint mappings.

  • ClubsClient is the production client, is annotated with @Client and simply extends from ClubsApi.

  • ClubsClientMock is a mocking client (resides in src/test/groovy), is annotated with @Fallback, and implements ClubsApi by returning hardcoded instances.

This is how ClubsApi looks like:

interface ClubsApi {
    @Get("/{id}")
    Maybe<Club> findTeam(Long id)
}

We are using a reactive type in the HTTP client response, so that is a hint for Micronaut to make it non-blocking.

Then, the production client:

@Client("clubs")    (1)
interface ClubsClient extends ClubsApi {}
1 "clubs" is the Consul name for the Clubs microservice (which registers itself with the micronaut.application.name property).

Finally, the mocking client:

@Fallback
class ClubsClientMock implements ClubsApi{

    @Override
    Maybe<Club> findTeam(Long id) {
        if (id == 1) {
            Maybe.just(new Club(name: "CD Leganes", stadium: "Butarque"))
        } else {
            Maybe.just(new Club(name: "Getafe CF", stadium: "Coliseum"))
        }
    }
}

We also need a Club POGO to capture the JSON response from clubs. Define it with 2 string fields: name and stadium.

Now let’s create a GORM Data Service for Fixture (named FixtureService). In this case, instead of an interface, we are using an abstract class, as we are going to implement our own custom method.

First, define the operations that we want GORM to implement automatically:

//GORM operations
abstract Fixture save(@Valid Fixture fixture)
abstract List<Fixture> findAll()
abstract Number count()

In this service, we need to transform Fixture instances into a data transfer object that contains club names and the stadium of the game.

Let’s call this DTO FixtureView and add the following fields:

String homeClubName
String awayClubName

String stadium

Short homeScore
Short awayScore

Date date

Then, in FixtureService we need to implement a method that takes a Fixture instance and converts it to a FixtureView instance. You first need to inject the ClubsClient we defined before:

@Inject
ClubsClient clubsClient

Then, implement the method:

Maybe<FixtureView> toView(Fixture fixture) {
    Maybe.zip(clubsClient.findTeam(fixture.homeClubId), clubsClient.findTeam(fixture.awayClubId)) { Club homeClub, Club awayClub ->
        return new FixtureView(
                date: fixture.date,
                homeClubName: homeClub.name,
                awayClubName: awayClub.name,
                stadium: homeClub.stadium,
                homeScore: fixture.homeScore,
                awayScore: fixture.awayScore
        )
    } as Maybe<FixtureView>
}

As the HTTP client is non-blocking, we can retrieve details about both clubs in parallel, and then compose our response once we get both HTTP responses back from the other microservice.

Now, let’s write a test for it:

package fixtures

import fixtures.domain.Fixture
import fixtures.service.FixtureService
import fixtures.view.FixtureView
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class FixtureServiceSpec extends Specification {

    @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
    @Shared FixtureService fixtureService = embeddedServer.applicationContext.getBean(FixtureService)

    void "it can get full details of a fixture"() {
        given:
        Date now = new Date()
        Fixture fixture = new Fixture(homeClubId: 1, homeScore: 5, awayClubId: 2, awayScore: 0, date: now)
        fixture = fixtureService.save(fixture)

        when:
        FixtureView view = fixtureService.toView(fixture).blockingGet()

        then:
        view.date == now
        view.homeClubName == "CD Leganes"
        view.homeScore == 5
        view.awayClubName == "Getafe CF"
        view.awayScore == 0
        view.stadium == "Butarque"

        cleanup:
        fixture.delete(flush: true)
    }

}

Make sure it passes.

3.2. REST API (35 minutes)

Let’s create a controller for displaying fixtures:

$ mn create-controller fixture

Declare a constructor dependency on FixtureService so that Micronaut knows it needs to be injected:

FixtureService fixtureService

FixtureController(FixtureService fixtureService) {
    this.fixtureService = fixtureService
}

Then, implement the action:

@Get("/")
List<FixtureView> findAll() {
    fixtureService.findAll().collect {
        fixtureService.toView(it).blockingGet()
    }
}

The method FixtureService.toView() is reactive as it returns a Maybe<FixtureView>. It could be possible to change the action implementation so that the return type would be Flowable<FixtureView>, but it would complicate this example. Mastering reactive programming is not the main purpose of this workshop, so for the sake of simplicity, we are introducing a blocking call.

Now, we need to test it:

package fixtures

import fixtures.client.FixtureClient
import fixtures.domain.Fixture
import fixtures.service.FixtureService
import fixtures.view.FixtureView;
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class FixtureControllerSpec extends Specification {

    @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
    @Shared FixtureClient client = embeddedServer.applicationContext.getBean(FixtureClient)
    @Shared FixtureService fixtureService = embeddedServer.applicationContext.getBean(FixtureService)

    void "test index"() {
        given:
        Fixture fixture = new Fixture(homeClubId: 1, homeScore: 5, awayClubId: 2, awayScore: 0, date: new Date())
        fixtureService.save(fixture)

        when:
        List<FixtureView> views = client.findAll()

        then:
        views.first().homeClubName == "CD Leganes"
        views.first().awayClubName == "Getafe CF"

        cleanup:
        fixture.delete(flush: true)
    }
}

Run the test to ensure it passes.

3.3. Load some data and run the application (10 minutes)

Similarly to the previous exercise, let’s seed the application with some data:

@Slf4j
@Singleton
@Requires(notEnv = Environment.TEST)
class DataLoader implements ApplicationEventListener<ServerStartupEvent> {

    final FixtureService fixtureService

    DataLoader(FixtureService fixtureService) {
        this.fixtureService = fixtureService
    }

    @Override
    void onApplicationEvent(ServerStartupEvent event) {
        if (!fixtureService.count()) {
            log.debug "Loading sample data"
            Fixture fixture = new Fixture(homeClubId: 1, homeScore: 5, awayClubId: 2, awayScore: 0, date: new Date())
            fixtureService.save(fixture)
        }
    }
}

Now, run the application:

./gradlew run

If you make a request to the default controller, and the clubs microservice is not running, you will see an error:

{"message":"Internal Server Error: No available services for ID: clubs"}

Now, run the clubs service on a different terminal, and try the request again.