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
.
-
-
-
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.
-
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:
-
Using the Micronaut Launch web application.
-
Using the Micronaut
mn
CLI. -
Using
curl
against thelaunch.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
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:
-
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 extendsCrudRepository
so that it inherits the basic CRUD operations. It is annotated with@JdbcRepository
so that it is recognized by Micronaut Data JDBC. Thedialect
attribute is set toDialect.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:
-
ClubApi
is an API contract with REST operations. It will be implemented byClubController
, and aClubClient
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 inHttpResponse
when we need to customise the response (header, response codes, etc). -
ClubController
is a Micronaut controller. It is annotated with@Controller
, and usesClubRepository
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.