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 and shell access, with the following installed:

    • curl.

    • wget.

    • unzip.

    • git.

  • Oracle GraalVM for JDK 21.

    • Recommended to be installed with SDKMAN!: sdk install java 21.0.3-graal.

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

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

    $ java -version
    java version "21.0.3" 2024-04-16 LTS
    Java(TM) SE Runtime Environment Oracle GraalVM 21.0.3+7.1 (build 21.0.3+7-LTS-jvmci-23.1-b37)
    Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.3+7.1 (build 21.0.3+7-LTS-jvmci-23.1-b37, 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 (35 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 (5 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_21&features=graalvm&features=control-panel' --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 (5 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 (5 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. Packaging the application into different formats (10 minutes)

The Micronaut Maven Plugin provides a convenient way to package your application into different formats, such as a fat JAR, a Docker image, or a native executable. For more details, refer to the Packaging section of the Micronaut Maven Plugin documentation.

To create a fat JAR, run:

./mvnw package

Once finished, you can run the JAR:

java -jar target/hello-0.1.jar

To create a Docker image, run:

./mvnw package -Dpackaging=docker

Once finished, you can run the Docker image:

docker run -p 8080:8080 hello

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

1.6. Exploring the Micronaut Control Panel (5 minutes)

The Micronaut Control Panel module provides a web UI that allows you to view and manage the state of your Micronaut application, typically in a development environment.

The control-panel feature was added when we created the application using Micronaut Launch, so all you need to do is run the application and open a browser and navigate to http://localhost:8080/control-panel

control panel
Check the Micronaut Control Panel documentation for more details.

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_21&features=graalvm&features=data-jdbc&features=postgres' --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 Micronaut Test’s @Sql annotation.

Create the following migration files:

Click to expand
src/main/resources/create.sql
create table club(id SERIAL PRIMARY KEY, name TEXT, stadium TEXT);
src/main/resources/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');
src/main/resources/clean.sql
drop table club;

Also, to prevent Micronaut Data’s built-in schema generation to conflict with the schema we are going to create, you need to remove the following line from application.properties:

datasources.default.schema-generate=CREATE_DROP

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 to make it 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.

In a real-world application, you should not expose your JPA entities directly to the outside world. Instead, you should create DTOs (Data Transfer Objects) that represent the data you want to expose. This is a good practice to avoid leaking implementation details and to have more control over the data you expose. In this exercise, we are exposing the JPA entities directly for simplicity.

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

Create the following test:

Click to expand
@MicronautTest
@Sql({"classpath:create.sql", "classpath:data.sql"}) (1)
@Sql(scripts = "classpath:clean.sql", phase = Sql.Phase.AFTER_ALL)
class ClubRepositoryTest {

    @Test
    void testItWorks(ClubRepository clubRepository) { (2)
        long clubCount = clubRepository.count();
        assertEquals(10, clubCount);
    }
}
1 The @Sql annotation is used to execute the SQL scripts before and after the test.
2 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 customise the response (header, response codes, etc).

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

In a real-world scenario, a controller should not interact directly with a repository. Instead, it should use a service layer that encapsulates the business logic. This is a good practice to separate concerns and make the code more testable. In this exercise, we are using the repository directly for simplicity.

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
@Sql({"classpath:create.sql", "classpath:data.sql"})
@Sql(scripts = "classpath:clean.sql", phase = Sql.Phase.AFTER_ALL)
class ClubControllerTest {

    @Inject
    ClubClient client; (2)

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

        assertFalse(((Collection<?>) clubs).isEmpty());
    }

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