Micronaut vs Quarkus - Part II: Micronaut First Contact

This is the second part of the series in which we dive into the Micronaut framework and try to replicate our Spring Boot Service we created in the first part of the series.

Micronaut vs Quarkus - Part II: Micronaut First Contact

In the first part of the series we built a REST microservice using the well known Spring Boot framework. Long overdue - let’s blame it on the summer season and some procrastination by yours truly - we now proudly present the second part in our Micronaut vs. Quarkus series.

In this blog post we’ll take our first steps in the Micronaut framework. We’ll try to rebuild the service from our previous blog post, nothing more and nothing less, and see how easy (or hard) this is.

As always the final code can be found on GitHub here.

Micronaut CLI

Micronaut comes with a CLI for skeleton generation of apps, features and components. For those who ever worked with the Grails framework, this functionality should look familiar. This is no coincidence. The inventors of Grails are also the developers of Micronaut. Like its counterpart in Spring Boot - the Spring Initializr - the Micronaut CLI is extremely useful in setting up the basics of a new Micronaut service (Maven pom, property files, etc.) .

Now, before taking our first Micronaut steps, let’s first install the CLI. Detailed installation instructions can be found on the Micronaut Download page.

Installation on Ubuntu is done with a few simple steps using sdkman:

curl -s https://get.sdkman.io | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install micronaut

If all went well, the following command will work and gives you the version of Micronaut you just installed:

mn --version

Currently the version we’re using in this blog is 2.0.1. If ever in the future you need to upgrade to a newer version of Micronaut, issue:

sdk upgrade micronaut

First steps

For a good guide on getting your feet wet using Micronaut, check the Creating your first Micronaut App guide.

As already mentioned, we’re gonna use the CLI to create the skeleton of our project. Micronaut is a polyglot framework supporting Java, Groovy and Kotlin. For the sake of staying close to the SpringBoot application, we’ll stick with java for now. Gradle is the default Build Tool, but again, to mimic the original, we’ll use Maven.

Unfortunately Micronaut offers no MongoDB Object Relational Mapping in Java at the moment. Groovy’s GORM does offer this support - maybe in a future blog post we’ll check it out -, but for Java our way to go would be to use the mongo-reactive feature. More information on MongoDB support for Micronaut can be found here.

We also want to create a Swagger UI. Micronaut provides the openapi feature for this.

Keeping all the information above in mind, leads to the following CLI command:

mn create-app nl.terrax.tbrestmongodb.tb-rest-micronaut-mongodb --build=maven --features=mongo-reactive,openapi

Skeleton project

The most important pieces of the skeleton project are:

  • the pom.xml containing all the necessary Micronaut dependencies as well as the feature dependencies, i.e. mongo-reactive and openapi in our case;
  • the Application.java bootstrap class;
  • the application.yml file containing the mongodb.uri property as part of the mongo-reactive feature.

Building the REST service

Some good examples on building CRUD services with Micronaut can be found here and here. We’re going to build the same layered setup as the one we used for the Spring Boot service, i.e. there will be a model layer containing the domain objects - Beer and Brewery classes -, a repository layer taking care of the persistency in CRUD style - BeerRepository class -, a service layer for the orchestration of CRUD operations - BeerService class - and a controller layer - BeerController class - exposing the REST operations.

Micronaut supports JSR-330 (Dependency Injection for Java) so the repository and service classes will be Singleton annotated and the controller class will have a Controller annotation.

Let’s create the setup code for these layers with the cli as well:

mn create-bean nl.terrax.tbrestmongodb.repository.BeerRepository
mn create-bean nl.terrax.tbrestmongodb.service.BeerService
mn create-controller nl.terrax.tbrestmongodb.controller.BeerController

The create-bean cli gives you not much more than an empty class annotated as @Singleton. The create-controller cli gives you a little bit more and also adds a Micronaut controller unit test.

BeerRepository

As opposed to the Spring Boot service that brings the concept of Spring Data Repositories - adding repository functionality without having to add barely any code -, in Micronaut we have to write some code using the MongoDB Reactive Streams Java Driver to get the job done.

@Singleton
public class BeerRepositoryImpl implements BeerRepository {

    private final MongoClient mongoClient;

    BeerRepositoryImpl(MongoClient mongoClient) {
        this.mongoClient = mongoClient;
    }

    @Override
    public Single<Beer> create(@NotNull Beer beer) {
        return Single.fromPublisher(
            getCollection().insertOne(beer)
        ).map(success -> beer);
    }

    @Override
    public Single<List<Beer>> findAll() {
        return Flowable.fromPublisher(
            getCollection().find()
        ).toList();
    }

    @Override
    public Maybe<Beer> find(@NotEmpty String name) {
        return Flowable.fromPublisher(
            getCollection()
                .find(eq("name", name))
                .limit(1)
        ).firstElement();
    }

    @Override
    public Single<Beer> update(@NotNull Beer beer) {
        return Single.fromPublisher(
            getCollection().findOneAndReplace(eq("id", beer.getId()), beer)
        ).map(success -> beer);
    }

    @Override
    public Maybe<Beer> delete(ObjectId id) {
        return Flowable.fromPublisher(
            getCollection()
                .findOneAndDelete(eq("_id", id))
        ).firstElement();
    }

    private MongoCollection<Beer> getCollection() {
        return mongoClient
            .getDatabase("terraxbeer")
            .getCollection("beers", Beer.class);
    }

}

Some pointers:

  • The mongoClient doing the actual work is dependency-injected into the repository;
  • Accessing the Beer collection has been extracted into its own private method to avoid repetition. With this method the database and the data collection is configured.

BeerService

The Beerservice code is merely a 1-on-1 mapping to the BeerRepository, so for brevity we won’t show the code here. The service uses transaction annotations though and for them to work we have to to add a dependency to our pom (unfortunately there’s no feature you could have used in the cli to get this one for free):

<dependency>
    <groupId>io.micronaut.data</groupId>
    <artifactId>micronaut-data-processor</artifactId>
    <scope>compile</scope>
</dependency>

BeerController

This is the class where we actually have to use some micronaut specific code.

@Controller("/beers/1.0")
public class BeerController {

    private final BeerService beerService;

    public BeerController(BeerService beerService) {
        this.beerService = beerService;
    }

    @Get()
    @Produces(MediaType.APPLICATION_JSON)
    public List<Beer> getAllBeers() {
        return beerService.findAll();
    }

    @Get("/{beerName}")
    @Produces(MediaType.APPLICATION_JSON)
    public Beer getBeerByName(@PathVariable("beerName") String name) {
        return beerService.findByName(name);
    }

    @Post()
    @Produces(MediaType.TEXT_PLAIN)
    public HttpResponse<String> saveOrUpdateBeer(@Body Beer beer) {
        beerService.saveBeer(beer);
        return HttpResponse.ok("Beer added successfully");
    }

    @Delete("/{beerName}")
    @Produces(MediaType.TEXT_PLAIN)
    public HttpResponse<String> deleteBeer(@PathVariable("beerName") String name) {
        beerService.deleteBeer(beerService.findByName(name).getId());
        return HttpResponse.ok("Beer deleted successfully");
    }
}

Some pointers:

  • The BeerService is dependency injected into the controller and operations are delegated to it (similar to the way we built it with Spring Boot)t;
  • Annotations like @Get and @Post are similar to the Spring variants we used in the previous blog post;
  • The io.micronaut.http.HttpResponse serves a similar purpose as the org.springframework.http.ResponseEntity.

OpenAPI

For more information on setting up the OpenApi UI for a Micronaut project, check out the guide here.

The feature=openapi clause we added upon generating the project already added the right dependencies for us. We need to add a bit more to get the actual UI in place:

  • The @OpenAPIDefinition annotation that has been added to the Application class can be refined further to set some OpenAPI metadata;
  • An openapi.properties file needs to be added to the project root to enable the UI. The following property should be included in there:
swagger-ui.enabled=true
  • The following lines have to be added to the application.yml file
micronaut:
    router:
        static-resources:
            swagger:
                paths: classpath:META-INF/swagger
                mapping: /swagger/**
  • This would be enough to get the OpenAPI page when maven is used to build the project. When, like me, you’re running it from IntelliJ, you need to add the following line to the maven pom.xml under the compilerArgs section of the maven-compiler-plugin configuration:
<arg>-Amicronaut.openapi.config.file=${project.basedir}/openapi.properties</arg>

After startup the OpenAPI docs are now available at http://localhost:8080/swagger/tb-rest-micronaut-mongodb-1.0.yml and the OpenAPI UI should be available at http://localhost:8080/swagger/views/swagger-ui/index.html.

Unit Testing

Alright, so how do we unit test the Micronaut controller beasty?!

We’ll use the client present in Micronaut’s arsenal for programmatically calling restful apis over http, i.e. the RxHttpClient. See this guide for more information.

More information on building unit tests in Java can be found in this guide.

We’ll also be using the mockito library for mocking stuff and the json-path library for json validation. The following dependencies need to be added to the pom:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>3.4.6</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.0</version>
    <scope>test</scope>
</dependency>

Now, for the actual unit test, see the code below. For brevity we’ll only show the saveOrUpdate test, the other tests use similar techniques.

@MicronautTest
class BeerControllerTest {

    @Inject
    @Client("/")
    RxHttpClient client;

    @Inject
    private BeerService beerServiceMock;

    @Test
    void saveOrUpdateBeer() {
        final String beerName = "Terra10 Gold";
        final String breweryName = "Terrax Micro-Brewery Inc.";
        final String breweryCountry = "The Netherlands";
        final Beer beer = newTestBeer(beerName, breweryName, breweryCountry);

        final HttpResponse<String> response = client.exchange(POST("/beers/1.0", beer), String.class).blockingFirst();

        assertEquals(HttpStatus.OK, response.status());
        assertTrue(response.getContentType().isPresent());
        assertEquals(MediaType.TEXT_PLAIN, response.getContentType().get().toString());

        assertTrue(response.getBody().isPresent());
        String result = response.getBody().get();
        assertEquals("Beer added successfully", result);

        final ArgumentCaptor<Beer> beerArgumentCaptor = ArgumentCaptor.forClass(Beer.class);
        verify(beerServiceMock, times(1)).saveBeer(beerArgumentCaptor.capture());
        verifyNoMoreInteractions(beerServiceMock);

        final Beer beerArgument = beerArgumentCaptor.getValue();
        assertNull(beerArgument.getId());
        assertEquals(beerName, beerArgument.getName());
        assertEquals(breweryName, beerArgument.getBrewery().getName());
        assertEquals(breweryCountry, beerArgument.getBrewery().getCountry());
    }

    @MockBean(BeerService.class)
    BeerService mockBeerService() {
        return mock(BeerService.class);
    }
}

Some pointers here:

  • To indicate there is some Micronaut code to be tested, the @MicronautTest annotation is added to the test class;
  • The Micronaut RxHttpClient is used to call the rest controller and needs to be injected in the unit test and annotated with @Client;
  • Setting up a mock requires a bit more code when you compare it to the Spring Boot tests. You need a method returning a @MockBean and a corresponding dependency injected instance variable.

The rest of the unit test code hopefully speaks for itself. It’s just calling the rest operation, verifying the response and verifying the argument that went to the underlying BeerService.

Test Driving

After starting MongoDB as a Docker container like we did when testing the Spring Boot service, we can use the OpenAPI UI to test our new Micronaut service:

And there you have it, a running REST service built with the Micronaut framework!

Summary

In this blog post we rebuilt the Spring Boot service of our previous post in Micronaut. In a future blog we will dive a bit deeper into the possibilities and advantages Micronaut gives us.

Looking at it from a coding perspective, I have to say that Micronaut code feels a bit more convoluted than its Spring Boot counterpart.

Biggest omission was the MongoDB CRUD repository functionality that Spring Boot does offer. Micronaut only has that functionality for relational databases at the moment. Hopefully MongoDB will be high on their list of upcoming features. Micronaut does offer GORM but that would mean switching from Java to Groovy.

Unit testing a controller also feels a bit clumsier. I really miss the fluent MockMVC class here.

Furthermore the maven pom has a bit more complexity compared to its Spring Boot twin, but here the Micronaut CLI can help out a lot.

Having said that, the Micronaut CLI is really helpful and could very well prove to be a killer feature of Micronaut in the future. And speaking of killer features, the startup time of a Micronaut app is amazing. The service built in this blog starts up in well under a second.

And as far as documentation goes: from what I’ve seen so far, the documentation is pretty complete and very helpful in getting you up to speed.

That’s it for now! See you in the next Micronaut blog. We’ll dive a bit deeper in the Micronaut framework then, maybe run it on GraalVM, build it in Groovy, who knows? Oh, and Quarkus will be coming up very soon as well.

Until then: keep calm and have a beer!

References

Code

Micronaut service

OpenApi

Unit testing