BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Micronaut Tutorial: How to Build Microservices with This JVM-Based Framework

Micronaut Tutorial: How to Build Microservices with This JVM-Based Framework

Lire ce contenu en français

Key Takeaways

  • Micronaut is a modern, JVM based, full-stack framework for building modular, easily testable microservice applications.
  • Micronaut has 100% compile-time, reflection free, dependency injection and AOP. 
  • The framework development team is the same group behind Grails Framework. 
  • Micronaut integrates cloud technologies into the framework, and microservice patterns such as service discovery, distributed tracing, circuit breaker are baked into the framework.
  • During this tutorial you will create three microservices with different languages: Java, Kotlin and Groovy. You will also learn how easy it is to consume other microservices with Micronaut HTTP Client and how to create functional tests that run fast.
     

Unlike applications built using traditional JVM frameworks, Micronaut has 100% compile-time, reflection free, dependency injection and AOP. Thus, Micronaut applications are small and have a low memory footprint. With Micronaut, you can develop a big monolith or a small function which can be deployed AWS Lambda. You are not constrained by the framework.

Micronaut also integrates cloud technologies into the framework, and microservice patterns such as service discovery, distributed tracing, circuit breaker are baked into the framework.

Micronaut was released as open-source in May 2018, and is scheduled to release its 1.0.0 version by the end of 2018. You can try Micronaut today, as milestone and release candidate versions are available.

Micronaut development team is the same group behind Grails Framework. Grails, who recently reached its 10th year anniversary, continues to help developers to craft web applications with many productivity boosters. Grails 3 is built on top of Spring Boot. As you will soon discover, Micronaut has an easy learning curve for developers coming from both frameworks, Grails and Spring Boot.

Tutorial Overview

In this series of articles, we are going to create an application composed by several microservices:

  • books microservice; written in Groovy.
  • An inventory microservice; written in Kotlin.
  • gateway microservice; written in Java.

You will:

  • Write endpoints and use compiletime DI. 
  • Write functional tests.
  • Configure those micronaut applications to register with Consul.
  • Communicate between them with Micronaut  declarative HTTP Client.

The next diagram illustrates the app, you will build: 

Microservice #1 - a Groovy Microservice

The easiest way to create Micronaut apps is to use its command line interface (Micronaut CLI) which can be installed via effortlessly With SDKMan.

Micronaut applications can be written in Java, Kotlin and Groovy. Let us create a Groovy Micronaut app first:

mn create-app example.micronaut.books --lang groovy .

The previous command creates an app named books with a default package of example.micronaut.

Micronaut is test framework-agnostic. It selects a default testing framework based on the language you use. By default for Java, JUnit is used. If you select Groovy, by default, Spock is used. You can mix different languages and testing frameworks. For example, a Java Micronaut app tested with Spock.

Moreover, Micronaut is build tool agnostic. You can use Maven or Gradle. By default, Gradle will be used.

The generated app includes a non-blocking HTTP server based on Netty.

Create a controller to expose your first Micronaut endpoint:


books/src/main/groovy/example/micronaut/BooksController.groovy

package example.micronaut

import groovy.transform.CompileStatic
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@CompileStatic
@Controller("/api")
class BooksController {

    private final BooksRepository booksRepository

    BooksController(BooksRepository booksRepository) {
        this.booksRepository = booksRepository
    }

    @Get("/books")
    List<Book> list() {
        booksRepository.findAll()
    }
}

Several things are worth mention in the previous code.

  • The Controller exposes a route/api/books which could be invoked with a GET request.
  • The value of @Get and @Controller annotations is a RFC-6570 URI template.
  • Via constructor injection, Micronaut supplies a collaborator; BooksRepository.
  • Micronaut controllers consume and produce JSON by default.

The previous controller uses an interface and a POGO:

books/src/main/groovy/example/micronaut/BooksRepository.groovy

package example.micronaut

interface BooksRepository {
    List<Book> findAll()
}

books/src/main/groovy/example/micronaut/Book.groovy

package example.micronaut

import groovy.transform.CompileStatic
import groovy.transform.TupleConstructor

@CompileStatic
@TupleConstructor
class Book {
    String isbn
    String name
}

Micronaut wires up at compile time a bean implementing the BooksRepository interface.

For this app, we create a singleton, which we define with the javax.inject.Singleton annotation.

books/src/main/groovy/example/micronaut/BooksRepositoryImpl.groovy

package example.micronaut

import groovy.transform.CompileStatic
import javax.inject.Singleton

@CompileStatic
@Singleton
class BooksRepositoryImpl implements BooksRepository {

    @Override
    List<Book> findAll() {
        [
            new Book("1491950358", "Building Microservices"),
            new Book("1680502395", "Release It!"),
        ]
    }
}

Functional tests add the most value since they test the application in its entirety. However, with other frameworks, functional and integration tests are seldom used. Mostly, because since they involve the start of the whole application, they are slow.

However, writing functional tests in Micronaut is a joy. Because they are fast; really fast.

A functional test for the previous controller is listed below:

books/src/test/groovy/example/micronaut/BooksControllerSpec.groovy

package example.micronaut

import io.micronaut.context.ApplicationContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.RxHttpClient
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class BooksControllerSpec extends Specification {

    @Shared
    @AutoCleanup
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)

    @Shared @AutoCleanup RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL())

    void "test books retrieve"() { 
        when:
        HttpRequest request = HttpRequest.GET('/api/books')
        List<Book> books = client.toBlocking().retrieve(request, Argument.of(List, Book))

        then:
        books books.size() == 2
    }
}

Several things are worth mentioning from the previous test:

  • It is easy to run the application from a unit test with the EmbeddedServer interface.
  • You can easily create a HTTP Client bean to consume the embedded server.
  • Micronaut Http Client makes it easy to parse JSON into Java objects.

2nd Microservice. A Kotlin microservice

Run the next command to create another microservice named inventory . This time, we use Kotlin as the language.

mn create-app example.micronaut.inventory --lang kotlin

This new microservice controls the stock of each book.

Create a Kotlin Data Class to encapsulate the domain:

inventory/src/main/kotlin/example/micronaut/Book.kt

package example.micronaut

data class Book(val isbn: String, val stock: Int)

Create a Controller which returns a book’s stock.

inventory/src/main/kotlin/example/micronaut/BookController.kt

package example.micronaut

import io.micronaut.http.HttpResponse 
import io.micronaut.http.MediaType 
import io.micronaut.http.annotation.Controller 
import io.micronaut.http.annotation.Get 
import io.micronaut.http.annotation.Produces
import io.micronaut.security.annotation.Secured

@Controller("/api") 
class BooksController {

    @Produces(MediaType.TEXT_PLAIN) 
    @Get("/inventory/{isbn}") 
    fun inventory(isbn: String): HttpResponse<Int> {
        return when (isbn) { 
            "1491950358" -> HttpResponse.ok(2) 
            "1680502395" -> HttpResponse.ok(3) 
            else -> HttpResponse.notFound()
        }
    }
}

3rd Microservice. A Java microservice

Create a Java gateway app which consumes both books and inventory microservices.

mn create-app example.micronaut.gateway

Java is picked by default if you do not specify the lang flag.

Inside the gateway microservice, create one declarative HTTP Client to communicate with the books microservice.

First create an interface:

gateway/src/main/java/example/micronaut/BooksFetcher.java

package example.micronaut;

import io.reactivex.Flowable;

public interface BooksFetcher { 
    Flowable<Book> fetchBooks(); 
}

Then create a declarative HTTP Client; an interface annotated with @Client.

gateway/src/main/java/example/micronaut/BooksClient.java

package example.micronaut;

import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.micronaut.http.annotation.Get; 
import io.micronaut.http.client.annotation.Client; 
import io.reactivex.Flowable;

@Client("books") 

@Requires(notEnv = Environment.TEST) 

public interface BooksClient extends BooksFetcher {

    @Override @Get("/api/books") Flowable<Book> fetchBooks();

}

Micronaut declarative HTTP Client methods will be implemented for you at compile time, greatly simplifying the creation of HTTP clients.

Also, Micronaut supports the concept of application environment. In the previous code listing, you can see how easy is to disable the loading of some beans for a particular environment with @Requires annotation.

Moreover, as you can see from the previous code sample non blocking types are first class citizens in Micronaut. The BooksClient::fetchBooks() method returns Flowable<Book> where Book is a Java POJO:

gateway/src/main/java/example/micronaut/Book.java

package example.micronaut;

public class Book {
     private String isbn; 
     private String name; 
     private Integer stock;
     
     public Book() {}

     public Book(String isbn, String name) { 
         this.isbn = isbn; 
         this.name = name; 
     }

     public String getIsbn() { return isbn; }

     public void setIsbn(String isbn) { this.isbn = isbn; }

     public String getName() { return name; }

     public void setName(String name) { this.name = name; }
     
     public Integer getStock() { return stock; }

     public void setStock(Integer stock) { this.stock = stock; }
}

Create another declarative HTTP Client to communicate with the inventory microservice.

First create an interface:

gateway/src/main/java/example/micronaut/InventoryFetcher.java

package example.micronaut;

import io.reactivex.Maybe;

public interface InventoryFetcher { 
    Maybe<Integer> inventory(String isbn); 
}

Then, an HTTP declarative client:

gateway/src/main/java/example/micronaut/InventoryClient.java

package example.micronaut;

import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.micronaut.http.annotation.Get; 
import io.micronaut.http.client.Client; 
import io.reactivex.Maybe; 

@Client("inventory") 
@Requires(notEnv = Environment.TEST)
public interface InventoryClient extends InventoryFetcher {
    @Override 
    @Get("/api/inventory/{isbn}") 
    Maybe<Integer> inventory(String isbn);
}

Now, create a controller which injects both beans and creates a reactive response.

gateway/src/main/java/example/micronaut/BooksController.java

package example.micronaut;

import io.micronaut.http.annotation.Controller; 
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured; 
import io.reactivex.Flowable;
import java.util.List;

@Controller("/api") 
public class BooksController {

    private final BooksFetcher booksFetcher; 
    private final InventoryFetcher inventoryFetcher;

    public BooksController(BooksFetcher booksFetcher, InventoryFetcher inventoryFetcher) {
        this.booksFetcher = booksFetcher;
        this.inventoryFetcher = inventoryFetcher; 
    }

    @Get("/books") Flowable<Book> findAll() { 
        return booksFetcher.fetchBooks()
                   .flatMapMaybe(b -> inventoryFetcher.inventory(b.getIsbn())
                        .filter(stock -> stock > 0)
                        .map(stock -> { 
                            b.setStock(stock); 
                            return b; 
                        })
                    );

    }
}

Before we can create a functional test for the controller, we need to create bean implementations for ( BooksFetcher and InventoryFetcher) at the test environment.

Create a bean which conforms to BooksFetcher interface, only available for the test environment; see @Requires annotation.


gateway/src/test/java/example/micronaut/MockBooksClient.java

package example.micronaut;

import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.reactivex.Flowable;
import javax.inject.Singleton;

@Singleton 
@Requires(env = Environment.TEST) 
public class MockBooksClient implements BooksFetcher {
    @Override
    public Flowable<Book> fetchBooks() { 
        return Flowable.just(new Book("1491950358", "Building Microservices"), new Book("1680502395", "Release It!"), new Book("0321601912", "Continuous Delivery:"));
    } 
}

Create a bean which conforms to InventoryFetcher interface, only available for the test environment.

gateway/src/test/java/example/micronaut/MockInventoryClient.java

package example.micronaut;

import io.micronaut.context.annotation.Requires; 
import io.micronaut.context.env.Environment; 
import io.reactivex.Maybe;
import javax.inject.Singleton;

@Singleton 
@Requires(env = Environment.TEST) 
public class MockInventoryClient implements InventoryFetcher {

    @Override 
    public Maybe<Integer> inventory(String isbn) { 
        if (isbn.equals("1491950358")) { 
            return Maybe.just(2); 
        } 
        if (isbn.equals("1680502395")) { 
            return Maybe.just(0); 
        } 
        return Maybe.empty();
    } 
}

Create a functional test. In the Groovy microservice we wrote a Spock test, this time we write JUnit test instead.

gateway/src/test/java/example/micronaut/BooksControllerTest.java

package example.micronaut;

import io.micronaut.context.ApplicationContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.List;

public class BooksControllerTest {

    private static EmbeddedServer server; 
    private static HttpClient client;

    @BeforeClass 
    public static void setupServer() {
        server = ApplicationContext.run(EmbeddedServer.class); 
        client = server .getApplicationContext() .createBean(HttpClient.class, server.getURL());
    }

    @AfterClass 
    public static void stopServer() {
        if (server != null) { 
            server.stop();
        }
        if (client != null) { 
            client.stop();
        }
     }

     @Test 
     public void retrieveBooks() { 
         HttpRequest request = HttpRequest.GET("/api/books");         
         List<Book> books = client.toBlocking().retrieve(request, Argument.of(List.class, Book.class)); 
         assertNotNull(books); 
         assertEquals(1, books.size());
     } 
}

Discovery service

We are going to configure our micronaut microservice to register with Consul Service discovery.

Consul is a distributed service mesh to connect, secure, and configure services across any runtime platform and public or private cloud

Integrating Micronaut and Consul is simple.

First add to every microservice books, inventory and gateway the discovery-client dependency:

gateway/build.gradle

runtime "io.micronaut:micronaut-discovery-client"

books/build.gradle

runtime "io.micronaut:micronaut-discovery-client"

inventory/build.gradle

runtime "io.micronaut:micronaut-discovery-client"

We need to do some configuration changes to each app so that when the application starts, it registers with Consul.

gateway/src/main/resources/application.yml

micronaut:
    application:
        name: gateway 
    server:
        port: 8080
consul:
    client:
        registration: 
            enabled: true
        defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"



books/src/main/resources/application.yml
micronaut:
    application:
        name: books
    server:
        port: 8082
consul:
    client:
        registration: 
            enabled: true
        defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"



inventory/src/main/resources/application.yml
micronaut:
    application:
        name: inventory
    server:
        port: 8081
consul:
    client:
        registration: 
            enabled: true
        defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

Each service use the property micronaut.application.name as a service id when they register in Consul. That it is why we use those exact names in the previous @Client annotation.

The previous code listings illustrate another feature of Micronaut, environment variable interpolation with default values in configuration files. See:

defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"

Also, in Micronaut you can have environment specific configuration files. We are going to create a file named application-test.yml in each of the environments to consul registration in the test phase.

gateway/src/test/resources/application-test.yml
consul:
    client:
        registration: enabled: false


books/src/test/resources/application-test.yml
consul:
    client:
        registration: enabled: false


inventory/src/test/resources/application-test.yml
consul:
    client:
        registration: enabled: false

Run the app

The easiest way to start using Consul is via Docker. Now, run a Docker instance. 

docker run -p 8500:8500 consul

Create a multi-project build with Gradle. Create a settings.gradle file in the root folder:

settings.gradle
include 'books'

include 'inventory'

include 'gateway'

Now you can run each app in parallel. Gradle has a handy flag ( -parallel ) for that:

./gradlew -parallel run

Each microservice starts in the configured ports: 8080, 8081 and 8082.

Consul comes with a HTML UI. Open http://localhost:8500/ui in your browser you will see:

Each Micronaut micro service has registered with consul .

You can invoke the gateway microservice with the next cURL command:

$ curl http://localhost:8080/api/books [{"isbn":"1680502395","name":"Release It!","stock":3}, {"isbn":"1491950358","name":"Building Microservices","stock":2}]

Congratulations you have created your first Micronaut network of microservices!

Wrapping Up

During this tutorial you have created three microservices with different languages: Java, Kotlin and Groovy. You have also learned how easy it is to consume other microservices with the Micronaut HTTP Client, and how to create functional tests that run fast. Moreover, you created everything enjoying full reflection-free Dependency Injection and AOP.

Join me for part two of the series, coming soon. In the meantime, please feel free to ask questions via the comments section below.

About the Author

Sergio del Amo Caballero is a developer specialized in the development of mobile phone apps ( iOS, Android) powered by Grails / Micronaut backends. Since 2015, Sergio del Amo writes a newsletter, Groovy Calamari, around the Groovy Ecosystem and Microservices. Groovy, Grails, Micronaut, Gradle,...

Rate this Article

Adoption
Style

BT