Introductory workshop about Micronaut.
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
.
-
-
Install SDKMAN! if you haven’t done so already.
-
Install Micronaut CLI:
$ sdk install micronaut
-
Ensure the CLI is installed properly:
$ mn --version | Micronaut Version: 1.0.0.M3 | JVM Version: 1.8.0_131
Once done, you can clone this repo:
git clone https://github.com/alvarosanchez/micronaut-workshop.git
Note
|
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: [hand o right]
|
Throughout this workshop, we will be creating a football (soccer) management system.
-
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 theclubs
microservice.
Tip
|
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.
[hand o right] 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
Tip
|
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.
[hand o right] 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)
As explained avobe, the create-app
command can be used to create new projects.
It accepts some flags:
Flag | Description | Example |
---|---|---|
|
Build tool (one of |
|
|
Profile to use for the project (default is |
|
|
Features to use for the project, comma-separated |
|
|
If present, generates the project in the current directory (project name is optional if this flag is set) |
|
[hand o right] Let’s create a hello galaxy project:
$ mn create-app hello-galaxy --features groovy | Application created at /private/tmp/hello-galaxy
[hand o right] 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
[hand o right] Open the generated HelloController.groovy
with your favourite
IDE and make it return "Hello Micronauts!":
link:./ex01/solution/hello-galaxy/src/main/groovy/hello/galaxy/HelloController.groovy[role=include]
[hand o right] Now, run the application:
$ MICRONAUT_SERVER_PORT=8080 ./gradlew run
Tip
|
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
[hand o right] Then, on another shell, make a request to your service:
$ curl 0:8080/hello Hello Galaxy!
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!
[hand o right] Change the generated src/test/groovy/hello/galaxy/HelloControllerSpec.groovy
to look like this:
link:./ex01/solution/hello-galaxy/src/test/groovy/hello/galaxy/HelloControllerSpec.groovy[role=include]
-
Running an embedded server
-
Obtaining a reactive HTTP client attached to our embedded server
-
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.
[hand o right] Then, run the tests:
./gradlew test
Once finished, you should see an output similar to:
BUILD SUCCESSFUL in 6s
Tip
|
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.
[hand o right] 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).
Warning
|
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 .
|
[hand o right] Next, define a
GORM data service
named ClubService
as an interface with the following operations:
link:./ex02/solution/clubs/src/main/groovy/clubs/service/ClubService.groovy[role=include]
Tip
|
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) .
|
[hand o right] Now, let’s test our service:
link:./ex02/solution/clubs/src/test/groovy/clubs/ClubServiceSpec.groovy[role=include]
-
grails.gorm.transactions.Rollback
applies a transaction that always rolls back. -
Instead of any kind of injection, we simply get the bean from the application context.
Micronaut helps you writing both the client and server sides of a REST API. In this service, we are going to create the following:
[hand o right] Create the ClubsApi
interface, annotating its methods with
io.micronaut.http.annotation.Get
as described in the diagram.
[hand o right] Then, create ClubsClient
by simply extending from ClubsApi
.
Annotate the interface with io.micronaut.http.client.Client("/")
.
[hand o right] 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.
Warning
|
The controller actions need to be annotated with io.micronaut.http.annotation.Get again.
|
[hand o right] Finally, configure logback.xml
to see some relevant output
link:./ex02/solution/clubs/src/main/resources/logback.xml[role=include]
-
Debug level for our code
-
This allows to see the HTTP request and responses from the HTTP clients.
[hand o right] Once you have it, write a test for everything:
link:./ex02/solution/clubs/src/test/groovy/clubs/ClubControllerSpec.groovy[role=include]
-
Wrap write operations with
grails.gorm.transactions.Transactional
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
[hand o right] Let’s create a bean to load some data. Run:
mn create-bean dataLoader
[hand o right] Change it to look like:
link:./ex02/solution/clubs/src/main/groovy/clubs/init/DataLoader.groovy[role=include]
-
javax.inject.Singleton
will tell Micronaut to manage a single instance in the application context. -
With
io.micronaut.context.annotation.Requires
, we ensure this runs on production, which can be specified as not running under tests. -
Make the bean a
io.micronaut.context.event.ApplicationEventListener
of anio.micronaut.runtime.server.event.ServerStartupEvent
event. -
Implement the method loading some sample data.
[hand o right] 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
We want the clubs
microservice to be discoverable by the fixtures
service.
So we will enable Micronaut’s Consul support for service discovery.
[hand o right] First, add the neccessary dependency in build.gradle
:
link:./ex02/solution/clubs/build.gradle[role=include]
[hand o right] Then, change src/main/resources/application.yml
to define
the Consul configuration:
link:./ex02/solution/clubs/src/main/resources/application.yml[role=include]
[hand o right] 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
[hand o right] 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 ...
[hand o right] If you go the Consul UI, you can see it shows as registered:
[hand o right] 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.
Tip
|
Change to the ex03/fixtures directory to work on this exercise.
|
In this exercise we are creating the fixtures
microservice.
[hand o right] First of all, run MongoDB with Docker:
$ docker run -d --name=dev-mongo -p 27017:27017 mongo
[hand o right] Then, create the Fixture
domain class with the following properties:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/domain/Fixture.groovy[role=include]
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.
[hand o right] 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:
-
ClubsApi
is the interface that contains the client endpoint mappings. -
ClubsClient
is the production client, is annotated with@Client
and simply extends fromClubsApi
. -
ClubsClientMock
is a mocking client (resides insrc/test/groovy
), is annotated with@Fallback
, and implementsClubsApi
by returning hardcoded instances.
This is how ClubsApi
looks like:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/clubs/ClubsApi.groovy[role=include]
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:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/clubs/ClubsClient.groovy[role=include]
-
"clubs"
is the Consul name for the Clubs microservice (which registers itself with themicronaut.application.name
property).
Finally, the mocking client:
link:./ex03/solution/fixtures/src/test/groovy/fixtures/ClubsClientMock.groovy[role=include]
[hand o right] We also need a Club
POGO to capture the JSON response from clubs
. Define
it with 2 string fields: name
and stadium
.
[hand o right] 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:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/service/FixtureService.groovy[role=include]
In this service, we need to transform Fixture
instances into a data transfer
object that contains club names and the stadium of the game.
[hand o right] Let’s call this DTO FixtureView
and add the following fields:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/view/FixtureView.groovy[role=include]
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:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/service/FixtureService.groovy[role=include]
Then, implement the method:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/service/FixtureService.groovy[role=include]
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.
[hand o right] Now, let’s write a test for it:
link:./ex03/solution/fixtures/src/test/groovy/fixtures/FixtureServiceSpec.groovy[role=include]
Make sure it passes.
[hand o right] 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:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/controller/FixtureController.groovy[role=include]
Then, implement the action:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/controller/FixtureController.groovy[role=include]
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.
[hand o right] Now, we need to test it:
link:./ex03/solution/fixtures/src/test/groovy/fixtures/FixtureControllerSpec.groovy[role=include]
Run the test to ensure it passes.
[hand o right] Similarly to the previous exercise, let’s seed the application with some data:
link:./ex03/solution/fixtures/src/main/groovy/fixtures/init/DataLoader.groovy[role=include]
[hand o right] 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"}
[hand o right] Now, run the clubs
service on a different terminal, and try the request again.