Fork me on GitHub

Introductory workshop about Micronaut.

Software Requirements

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

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

    • curl.

    • wget.

    • unzip.

    • git.

  • GraalVM for JDK 17.

    • Recommended to be installed with SDKMAN!: sdk install java 17.0.7-graalce.

  • A valid Docker environment with the following image pulled: postgres:latest.

  • Ensure that the current JDK is GraalVM for Java 17:

    java -version
    openjdk version "17.0.7" 2023-04-18
    OpenJDK Runtime Environment GraalVM CE 17.0.7+7.1 (build 17.0.7+7-jvmci-23.0-b12)
    OpenJDK 64-Bit Server VM GraalVM CE 17.0.7+7.1 (build 17.0.7+7-jvmci-23.0-b12, mixed mode, sharing)

Clone this repository

Once done, you can clone this repo:

git clone https://github.com/alvarosanchez/micronaut-workshop.git
You will find each lab’s template files on each labNN 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, you will be creating a football (soccer) management system.

football diagram
  • clubs is the microservice responsible for managing clubs. It uses Micronaut Data JPA as a data access layer.

1. Getting started with Micronaut and GraalVM (20 minutes)

Change to the lab01 directory to work on this exercise

There are multiple ways to create a new Micronaut application:

  1. Using the Micronaut Launch web application.

  2. Using the Micronaut mn CLI.

  3. Using curl against the launch.micronaut.io API to download a ZIP.

1.1. Using Micronaut Launch to create a new application (3 minutes)

We will use the third option, so that you can copy/paste the commands.

To see the options available, run:

curl https://launch.micronaut.io

Micronaut Launch uses features as building blocks to create a new application. Features can contribute to the application in different ways, such as adding build dependencies, configuration properties, sample code, etc.

Let’s create a new application with the following command:

curl 'https://launch.micronaut.io/create/default/com.example.micronaut.hello?lang=JAVA&build=MAVEN&test=JUNIT&javaVersion=JDK_17&features=graalvm' --output hello.zip

You can now unzip the file and open the resulting folder in your IDE.

1.2. Running the application with the Micronaut Maven Plugin (3 minutes)

The Micronaut Maven Plugin provides a convenient way to run your application from the command line.

To run the application, execute:

./mvnw mn:run

You can leave the application running while you work on the rest of the exercise, since the plugin will automatically restart the application when it detects changes in the source code.

Micronaut applications can also be run from the IDE. Refer to the IDE Setup section of the Micronaut documentation for more details.

1.3. Creating a Hello World controller (3 minutes)

Create a new controller with a single endpoint /hello/{name} that returns a greeting message using the provided path variable.

Click to expand
@Controller (1)
public class HelloController {

    @Get("/hello/{name}") (2)
    public String sayHello(String name) { (3)
        return "Hello " + name;

    }
}
1 The @Controller annotation marks this class as a controller on the default / route.
2 The @Get annotation marks this method as a GET endpoint on the /hello/{name} route
3 The name parameter matches the path variable in the route. A typo in either of them will result in a compilation error.

1.4. Testing the application (5 minutes)

Since the application is still running, you can test it using curl:

Open a new terminal and run:

curl http://localhost:8080/hello/World

While manual testing is useful, writing an automated functional test is even better. Micronaut provides a super-fast and convenient way to write functional tests using the Micronaut Test library.

Generated applications include a sample test that asserts that the application startups successfully.

Open src/test/java/com/example/micronaut/HelloTest.java and add a new test that asserts that the /hello/{name} endpoint returns a greeting message.

Click to expand
@MicronautTest
class HelloTest {

    @Inject
    EmbeddedApplication<?> application;

    @Inject
    @Client("/") (1)
    HttpClient client;

    @Test
    void testItWorks() {
        Assertions.assertTrue(application.isRunning());
    }

    @Test
    void testSayHello() {
        String response = client.toBlocking().retrieve("/hello/World");
        assertEquals("Hello World", response);
    }
}
1 The @Client("/") annotation will inject an HTTP client connected to the embedded server random port.

Before running the test, let’s add a Logback logger to see the details of the HTTP client.

Open src/main/resources/logback.xml and add the following logger:

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

Now you can run the test from your IDE or from the command line:

./mvnw test

1.5. Generating a native executable (5 minutes)

Micronaut applications can be compiled to native executables using GraalVM. This allows you to create a single self-contained executable that can be run without a JVM.

To generate a native executable, run:

./mvnw package -Dpackaging=native-image

On modern hardware, this should take about one minute.

Once finished, you can run the executable:

./target/hello

You will see that Micronaut starts in a few milliseconds.

Test the application again with curl:

curl http://localhost:8080/hello/World

2. Implementing a data access layer with Micronaut Data JDBC (60 minutes)

Change to the lab02 directory to work on this exercise

In this exercise, you will create a data access layer using Micronaut Data JDBC. This library provides a convenient way to create repositories that can be used to perform CRUD operations on a database.

For development and testing on a real database, we are going to use PostgreSQL via Micronaut Test Resources.

2.1. Preparing the project (15 minutes)

Before going any further, make sure you have a recent version of postgres:latest pulled in your Docker environment.

Execute the following command

docker pull postgres:latest

Now, create a new application:

curl 'https://launch.micronaut.io/create/default/com.example.micronaut.clubs?lang=JAVA&build=MAVEN&test=JUNIT&javaVersion=JDK_17&features=graalvm&features=data-jdbc&features=postgres&features=flyway' --output clubs.zip

Unzip clubs.zip and import the project in your IDE.

Also, to set up the schema and add some sample data, we are going to use Flyway via its Micronaut Flyway integration.

Create the following migration files:

Click to expand
src/main/resources/db/migration/V1__schema.sql
create table club(id SERIAL PRIMARY KEY, name TEXT, stadium TEXT);
src/main/resources/db/migration/V2__data.sql
insert into club(name, stadium) values ('Real Madrid', 'Santiago Bernabéu');
insert into club(name, stadium) values ('FC Barcelona', 'Camp Nou');
insert into club(name, stadium) values ('Manchester United', 'Old Trafford');
insert into club(name, stadium) values ('Chelsea', 'Stamford Bridge');
insert into club(name, stadium) values ('Paris Saint-Germain', 'Parc des Princes');
insert into club(name, stadium) values ('Olympique de Marseille', 'Stade VĂ©lodrome');
insert into club(name, stadium) values ('Bayern Munich', 'Allianz Arena');
insert into club(name, stadium) values ('Borussia Dortmund', 'Signal Iduna Park');
insert into club(name, stadium) values ('Juventus', 'Juventus Stadium');
insert into club(name, stadium) values ('Inter Milan', 'Giuseppe Meazza');

At this point, you should be able to run the application and see if the migrations are applied. When using the Micronaut Maven Plugin’s mn:run goal, a Test Resources service will be started automatically, so that required containers are run before the application is running.

Execute the following command

./mvnw mn:run

You should see the following output:

...
[info] Starting Micronaut Test Resources service, version 2.0.0
[test-resources-service] 08:10:56.830 [ForkJoinPool.commonPool-worker-9] INFO  i.m.t.e.TestResourcesResolverLoader - Loaded 2 test resources resolvers: io.micronaut.testresources.postgres.PostgreSQLTestResourceProvider, io.micronaut.testresources.testcontainers.GenericTestContainerProvider
[test-resources-service] 08:10:57.048 [main] INFO  i.m.t.server.TestResourcesService - A Micronaut Test Resources server is listening on port 60231, started in 360ms
...
[test-resources-service] 08:10:57.803 [default-nioEventLoopGroup-1-2] INFO  i.m.t.testcontainers.TestContainers - Starting test container postgres
...
INFO: Migrating schema "public" to version "1 - schema"
INFO: Migrating schema "public" to version "2 - data"
INFO: Successfully applied 2 migrations to schema "public", now at version v2 (execution time 00:00.016s)
...

Now, hit Ctrl+C to stop the application, since going forward it is going to be tested with functional tests.

2.2. Implementing the data access layer (15 minutes)

For the data access layer, we are going to use the Micronaut Data library in its JDBC flavour. In the source code, we will use standard JPA annotations.

Add the following dependency:

<dependency>
  <groupId>jakarta.persistence</groupId>
  <artifactId>jakarta.persistence-api</artifactId>
  <scope>provided</scope> (1)
</dependency>
1 It is in provided scope so that is available during compilation, so that the Micronaut annotation processor can generate the corresponding beans, but it is not included in the final JAR since at runtime we will not use JPA but Micronaut Data JDBC.

Create the following entity model:

entity model
  • Club is a JPA entity, so it is annotated with @Entity. It is also annotated with @Serdeable so that it can be serialized and deserialized by Micronaut Serialization.

  • ClubRepository is a Micronaut Data JDBC repository. It extends CrudRepository so that it inherits the basic CRUD operations. It is annotated with @JdbcRepository so that it is recognized by Micronaut Data JDBC. The dialect attribute is set to Dialect.POSTGRES so that the queries generated at compile time work with PostgreSQL.

We can write a simple test to make sure everything is working as expected.

Create the following test:

Click to expand
@MicronautTest
class ClubRepositoryTest {

    @Test
    void testItWorks(ClubRepository clubRepository) { (1)
        long clubCount = clubRepository.count();
        assertEquals(10, clubCount);
    }
}
1 Micronaut can inject dependencies in test methods.

2.3. Implementing the REST API (15 minutes)

Create the following REST API:

rest api
  • ClubApi is an API contract with REST operations. It will be implemented by ClubController, and a ClubClient we will create later. For each method annotated with @Get, @Post, @Put, @Delete, etc., Micronaut will figure out how to render the corresponding JSON response based on the return type, which can be POJOs or collections of POJOs, or wrapped in HttpResponse when we need to customize the response (header, response codes, etc).

  • ClubController is a Micronaut controller. It is annotated with @Controller, and uses ClubRepository to implement the operations.

We can now write a functional test for this REST API. In this case, instead of using the low-level HTTP client, we are going to use a declarative client. For this, we are going to leverage the ClubApi interface we have just created.

Create the following test:

Click to expand
@MicronautTest
class ClubControllerTest {

    @Inject
    ClubClient client; (2)

    @Test
    void testItCanListClubs() {
        Iterable<Club> clubs = client.list();

        assertEquals(10, ((Collection<Club>)clubs).size());
    }

    @Test
    void testItCanGetAClub() {
        HttpResponse<Club> response = client.get(1L);

        assertEquals(200, response.code());

        Club club = response.body();
        assertNotNull(club);
        assertEquals("Real Madrid", club.name());
    }

    @Test
    void itReturnsNotFoundForUnknownClub() {
        assertEquals(404, client.get(100L).code());
    }

    @Client("/clubs") (1)
    interface ClubClient extends ClubApi {}
}
1 The @Client annotation is mapped to the same path as the controller. The implementation of this interface will be generated by Micronaut at compile time.
2 The @Inject annotation is used to inject an instance of the generated client.

In order to see the HTTP requests and responses, and also the SQL queries, we can declare the following loggers:

src/main/resources/logback.xml
<logger name="io.micronaut.data.query" level="trace" />
<logger name="io.micronaut.http.client" level="trace" />

2.4. Completing the REST API (15 minutes)

Using the knowledge acquired in the previous exercises, you can now complete the REST API by implementing the following endpoints:

  • POST /clubs: creates a new club.

  • PUT /clubs/{id}: updates an existing club.

  • DELETE /clubs/{id}: deletes an existing club.

Augment the functional test to cover the new endpoints. Test also negative cases such as providing non-existing IDs, or invalid payloads.