Eureka and Micronaut - Microservices service discovery

Use Netflix Eureka service discovery to expose your Micronaut apps.

Authors: Sergio del Amo

Micronaut Version: 1.0.0.M4

1 Getting Started

In this guide, we are going to create three microservices and register them with Netflix Eureka service discovery. You will discover how Micronaut eases Eureka integration.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the App

Lets describe the microservices:

  • bookcatalogue - It returns a list of books. It uses a domain consisting of a book name and isbn.

  • bookinventory - It exposes an endpoint to check whether a book has sufficient stock to fullfil an order. I uses a domain consisting of a stock level and isbn.

  • bookrecommendation - It consumes previous services and exposes and endpoint which recommends book names which are in stock.

Initially we are going to hardcode the addresses where the different services are in the bookcatalogue service.

hardcoded

As shown in the previous image, the bookcatalogue hardcodes references to its collaborators.

In the second part of this tutorial we are going to use a discovery service.

The services register when they start up:

discovery service registration

When a service wants to do a request to other service, it uses the discovery service to retrieve the address.

discovery service flow

If you are using Java or Kotlin and IntelliJ IDEA make sure you have enabled annotation processing.

annotationprocessorsintellij

2.1 Catalogue Microservice

Create the bookcatalogue microservice:

mn create-app example.micronaut.bookcatalogue.bookcatalogue

The previous command creates a folder named bookcatalogue and a Micronaut app inside it with default package: example.micronaut.bookcatalogue.

bookcatalogue/src/main/kotlin/example/micronaut/bookcatalogue/BooksController.kt
package example.micronaut.bookcatalogue

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/books") (1)
class BooksController {

    @Get("/") (2)
    internal fun index(): List<Book> {
        return listOf(Book("1491950358", "Building Microservices"),
                Book("1680502395", "Release It!"),
                Book("0321601912", "Continuous Delivery:"))
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /books
2 The @Get annotation is used to map the index method to /books requests that use an HTTP GET.
3 You can return reactive types from your controller methods.

The previous controller responds a Flowable<Book>. Create the Book POJO:

bookcatalogue/src/main/kotlin/example/micronaut/bookcatalogue/Book.kt
package example.micronaut.bookcatalogue

data class Book(var isbn: String, val name: String)

Write a test:

bookcatalogue/src/test/kotlin/example/micronaut/bookcatalogue/BooksControllerSpec.kt
package example.micronaut.bookcatalogue

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.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import kotlin.test.assertEquals
import kotlin.test.assertTrue

object BooksControllerSpec: Spek({
    describe("BookController Suite") {
        var embeddedServer : EmbeddedServer = ApplicationContext.run(EmbeddedServer::class.java)
        var client : HttpClient  = HttpClient.create(embeddedServer.url)

        it("books can be retrieved") {
            val req = HttpRequest.GET<Any>("/books") (1)
            val books = client.toBlocking().retrieve(req, Argument.of(List::class.java, Book::class.java)) (2)
            assertEquals(expected = books.size, actual = 3)
            assertTrue(books.contains(Book("1491950358", "Building Microservices")))
            assertTrue(books.contains(Book("1680502395", "Release It!")))
        }

        afterGroup {
            client.close()
            embeddedServer.close()
        }
    }

})
1 For JUnit you can write methods to start and stop the server for the scope of the test
2 It is easy to create HTTP requests with a fluid API.
3 Parse easily JSON into Java objects.

Edit application.yml

bookcatalogue/src/main/resources/application.yml
micronaut:
    application:
        name: bookcatalogue (1)
    server:
        port: 8081 (2)
1 Configure the application name. The app name will be use by the discovery service.
2 Configure the app to listen at port 8081

Create a file named application-test.yml which is used in the test environment:

bookcatalogue/src/main/resources/application-test.yml
micronaut:
    server:
        port: -1 (1)
1 Start the micronaut microservice at a random port when running the tests.

2.2 Inventory Microservice

Create the bookinventory microservice:

mn create-app example.micronaut.bookinventory.bookinventory

Modify build.gradle to add validation capabilities.

bookinventory/build.gradle
    compile "io.micronaut:validation"
    compile "io.micronaut.configuration:hibernate-validator"

The previous command creates a folder named bookinventory and a Micronaut app inside it with default package: example.micronaut.bookinventory.

Create a Controller:

bookinventory/src/main/kotlin/example/micronaut/bookinventory/BooksController.kt
package example.micronaut.bookinventory

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.validation.Validated
import javax.validation.constraints.NotBlank
import java.util.Optional

@Validated (1)
@Controller("/books") (2)
open class BooksController {

    @Produces(MediaType.TEXT_PLAIN) (3)
    @Get("/stock/{isbn}") (4)
    open fun stock(@NotBlank isbn: String): Boolean? {
        return bookInventoryByIsbn(isbn).map { (_, stock) -> stock > 0 }.orElse(null)
    }

    private fun bookInventoryByIsbn(isbn: String): Optional<BookInventory> {
        if (isbn == "1491950358") {
            return Optional.of(BookInventory(isbn, 4))

        } else if (isbn == "1680502395") {
            return Optional.of(BookInventory(isbn, 0))
        }
        return Optional.empty()
    }
}
1 Define Validated annotation at the class level to signal any class that requires validation.
2 The class is defined as a controller with the @Controller annotation mapped to the path /books
3 By default a Micronaut’s response uses application/json as Content-Type. We are returning a String not a JSON object. Because of that, we set it to text/plain.
4 The @Get annotation is used to map the index method to /books/stock/{isbn} requests that use an HTTP GET.

The previous controller uses a POJO:

bookinventory/src/main/kotlin/example/micronaut/bookinventory/BookInventory.kt
package example.micronaut.bookinventory

data class BookInventory(var isbn: String, val stock: Int)

Write a test:

bookinventory/src/test/kotlin/example/micronaut/bookinventory/BooksControllerSpec.kt
package example.micronaut.bookinventory

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientException
import io.micronaut.runtime.server.EmbeddedServer
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertTrue

object BookControllerSpec: Spek({
    describe("BookController Suite") {
        var embeddedServer : EmbeddedServer = ApplicationContext.run(EmbeddedServer::class.java)
        var client : HttpClient  = HttpClient.create(embeddedServer.url)

        it("a book in stock returns true") {
            val req = HttpRequest.GET<Any>("/books/stock/1491950358")
            val hasStock = client.toBlocking().retrieve(req, Boolean::class.java)
            assertTrue(hasStock)
        }
        it("a book out of stock returns false") {
            val req = HttpRequest.GET<Any>("/books/stock/1680502395")
            val hasStock = client.toBlocking().retrieve(req, Boolean::class.java)
            assertFalse(hasStock)
        }

        it("non existing Isbn returns 404") {
            val ex = assertFailsWith<HttpClientException> {
                val req = HttpRequest.GET<Any>("/books/stock/XXXXX")
                val hasStock = client.toBlocking().retrieve(req, Boolean::class.java)
            }
            assertEquals(ex.message, "Page Not Found")
        }

        afterGroup {
            client.close()
            embeddedServer.close()
        }
    }

})
1 For JUnit you can write methods to start and stop the server for the scope of the test

Edit application.yml

bookinventory/src/main/resources/application.yml
micronaut:
    application:
        name: bookinventory (1)
    server:
        port: 8082 (2)
1 Configure the application name. The app name will be used later in the tutorial.
2 Configure the app to listen at port 8082

Create a file named application-test.yml which is used in the test environment:

bookinventory/src/main/resources/application-test.yml
micronaut:
    server:
        port: -1 (1)
1 Start the micronaut microservice at a random port when running the tests.

2.3 Recommendation Microservice

Create the bookrecommendation microservice:

mn create-app example.micronaut.bookrecommendation.bookrecommendation

The previous command creates a folder named bookrecommendation and a Micronaut app inside it with default package: example.micronaut.bookrecommendation.

Create an interface to map operations with bookcatalogue and a Micronaut Declarative HTTP Client to consume it.

bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BookCatalogueOperations.kt
package example.micronaut.bookrecommendation

import io.reactivex.Flowable

interface BookCatalogueOperations {
    fun findAll(): Flowable<Book>
}
bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BookCatalogueClient.kt
package example.micronaut.bookrecommendation

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.Client
import io.reactivex.Flowable


@Client("http://localhost:8081") (1)
interface BookCatalogueClient : BookCatalogueOperations {

    @Get("/books")
    override fun findAll(): Flowable<Book>
}
1 Use @Client to use declarative HTTP Clients

The client returns a POJO. Create it in the bookrecommendation:

bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/Book.kt
package example.micronaut.bookrecommendation

data class Book(var isbn: String, val name: String)

Create an interface to map operations with bookinventory and a Micronaut Declarative HTTP Client to consume it.

bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BookInventoryOperations.kt
package example.micronaut.bookrecommendation

import io.reactivex.Maybe

import javax.validation.constraints.NotBlank

interface BookInventoryOperations {
    fun stock(@NotBlank isbn: String): Maybe<Boolean>
}
bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BookInventoryClient.kt
package example.micronaut.bookrecommendation

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.Client
import io.reactivex.Maybe

import javax.validation.constraints.NotBlank


@Client("http://localhost:8082") (1)
interface BookInventoryClient : BookInventoryOperations {

    @Get("/books/stock/{isbn}")
    override fun stock(@NotBlank isbn: String): Maybe<Boolean>
}
1 Use @Client to use declarative HTTP Clients

Create a Controller which injects both clients.

bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BooksController.kt
package example.micronaut.bookrecommendation

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.reactivex.Flowable

@Controller("/books") (1)
class BooksController(private val bookCatalogueOperations: BookCatalogueOperations,
                     private val bookInventoryOperations: BookInventoryOperations) { (2)

    @Get("/") (3)
    fun index(): Flowable<BookRecommendation> {
        return bookCatalogueOperations.findAll()
                .flatMapMaybe { b ->
                    bookInventoryOperations.stock(b.isbn)
                            .filter { hasStock -> hasStock }
                            .map { _ -> b }
                }.map { (_, name) -> BookRecommendation(name) }
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /books
2 Constructor injection
3 The @Get annotation is used to map the index method to /books requests that use an HTTP GET.

The previous controller returns a Flowable<BookRecommendation>. Create the BookRecommendation POJO:

bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BookRecommendation.kt
package example.micronaut.bookrecommendation

data class BookRecommendation(val name: String)

BookCatalogueClient and BookInventoryClient will fail to consume the bookcatalogue and bookinventory during the tests phase.

Using the @Fallback annotation you can declare a fallback implementation of a client that will be picked up and used once all possible retries have been exhausted

Create @Fallback alternatives in the test classpath.

bookrecommendation/src/test/kotlin/example/micronaut/bookrecommendation/BookInventoryClientStub.kt
package example.micronaut.bookrecommendation

import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.retry.annotation.Fallback
import io.reactivex.Maybe
import javax.inject.Singleton
import javax.validation.constraints.NotBlank

@Requires(env = arrayOf(Environment.TEST))
@Fallback
@Singleton
class BookInventoryClientStub : BookInventoryOperations {

    override fun stock(@NotBlank isbn: String): Maybe<Boolean> {
        if (isbn == "1491950358") {
            return Maybe.just(java.lang.Boolean.TRUE)

        } else if (isbn == "1680502395") {
            return Maybe.just(java.lang.Boolean.FALSE)
        }
        return Maybe.empty()
    }
}
bookrecommendation/src/test/kotlin/example/micronaut/bookrecommendation/BookCatalogueClientStub.kt
package example.micronaut.bookrecommendation

import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.retry.annotation.Fallback
import io.reactivex.Flowable

import javax.inject.Singleton

@Requires(env = arrayOf(Environment.TEST))
@Fallback
@Singleton
class BookCatalogueClientStub : BookCatalogueOperations {

    override fun findAll(): Flowable<Book> {
        val buildingMicroservices = Book("1491950358", "Building Microservices")
        val releaseIt = Book("1680502395", "Release It!")
        return Flowable.just(buildingMicroservices, releaseIt)
    }
}

Write a test:

bookrecommendation/src/test/kotlin/example/micronaut/bookrecommendation/BookControllerSpec.kt
Unresolved directive in <stdin> - include::/home/travis/build/micronaut-guides/micronaut-microservices-services-discover-eureka-kotlin/complete/bookrecommendation/src/test/kotlin/example/micronaut/bookrecommendation/BookControllerSpec.kt[]
1 For JUnit you can write methods to start and stop the server for the scope of the test

Edit application.yml

bookrecommendation/src/main/resources/application.yml
micronaut:
    application:
        name: bookrecommendation (1)
    server:
        port: 8080 (2)
1 Configure the application name. The app name will be used later in the tutorial.
2 Configure the app to listen at port 8080

Create a file named application-test.yml which is used in the test environment:

bookrecommendation/src/main/resources/application-test.yml
micronaut:
    server:
        port: -1 (1)
1 Start the micronaut microservice at a random port when running the tests.

3 Running the app

Run bookcatalogue microservice:

bookcatalogue $ ./gradlew run
 ...
 > Task :bookcatalogue:run
 14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081

Run bookinventory microservice:

bookinventory $ ./gradlew run
 ...
 > Task :bookcatalogue:run
 14:31:13.104 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8082

Run bookrecommendation microservice:

bookrecommendation $ ./gradlew run
...
> Task :bookrecommendation:run
14:31:57.389 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080

You can run a cURL command to test the whole application:

$ curl http://localhost:8080/books
[{"name":"Building Microservices"}

4 Eureka and Micronaut

Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers.

4.1 Eureka Server

Spring-Cloud-Netflix provides a very neat way to bootstrap Eureka. To bring up Eureka server using Spring-Cloud-Netflix:

  • Clone the sample Eureka server application.

  • Run this project as a Spring Boot app (e.g. import into IDE and run main method, or use mvn spring-boot:run or gradle bootRun or ./gradlew bootRun). It will start up on port 8761 and serve the Eureka API from /eureka.

This tutorial solution contains a folder named eureka with the cloned sample Eureka server application.

4.2 Integrate Eureka

4.3 Book Catalogue

Modify build.gradle to add discovery-client dependency.

bookcatalogue/build.gradle
dependencies {
    ...
    ..
    .
    runtime "io.micronaut:discovery-client"
}

Append to bookcatalogue service application.yml the following snippet:

bookcatalogue/src/main/resources/application.yml
eureka:
  client:
    registration:
      enabled: true
    defaultZone: "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"

Previous configuration registers a Micronaut app with Eureka with minimal configuration. Discover a more complete list of Configuration options at EurekaConfiguration.

Disable Eureka registration in tests:

bookcatalogue/src/main/resources/application-test.yml
eureka:
    client:
        registration:
            enabled: false

4.4 Book Inventory

Modify build.gradle to add discovery-client dependency.

bookinventory/build.gradle
dependencies {
    ...
    ..
    .
    runtime "io.micronaut:discovery-client"
}

Also, append to bookinventory.application.yml the following snippet:

bookinventory/src/main/resources/application.yml
eureka:
  client:
    registration:
      enabled: true
    defaultZone: "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"

Disable Eureka registration in tests:

bookinventory/src/main/resources/application-test.yml
eureka:
    client:
        registration:
            enabled: false

4.5 Book Recommendation

Modify build.gradle to add discovery-client dependency.

bookrecommendation/build.gradle
dependencies {
    ...
    ..
    .
    runtime "io.micronaut:discovery-client"
}

Also, append to bookrecommendation.application.yml the following snippet:

bookrecommendation/src/main/resources/application.yml
eureka:
  client:
    registration:
      enabled: true
    defaultZone: "${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}"

Modify BookInventoryClient and BookCatalogueClient to use the service id instead of a harcoded ip.

bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BookCatalogueClient.kt
package example.micronaut.bookrecommendation

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.Client
import io.reactivex.Flowable


@Client(id = "bookcatalogue") (1)
interface BookCatalogueClient : BookCatalogueOperations {

    @Get("/books")
    override fun findAll(): Flowable<Book>
}
1 Use the configuration value micronaut.application.name used in bookcatalogue as service id.
bookrecommendation/src/main/kotlin/example/micronaut/bookrecommendation/BookInventoryClient.kt
package example.micronaut.bookrecommendation

import io.micronaut.http.annotation.Get
import io.micronaut.http.client.Client
import io.reactivex.Maybe

import javax.validation.constraints.NotBlank


@Client(id = "bookinventory") (1)
interface BookInventoryClient : BookInventoryOperations {

    @Get("/books/stock/{isbn}")
    override fun stock(@NotBlank isbn: String): Maybe<Boolean>
}
1 Use the configuration value micronaut.application.name used in bookinventory as service id.

Disable Eureka registration in tests:

bookrecommendation/src/main/resources/application-test.yml
eureka:
    client:
        registration:
            enabled: false

4.6 Running the App

Run the eureka server:

eureka $ ./gradle bootRun

Run bookcatalogue microservice:

bookcatalogue $ ./gradlew run
 ...

 > Task :bookcatalogue:run
 14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081

Run bookinventory microservice:

bookinventory $ ./gradlew run
 ...
 > Task :bookcatalogue:run
 14:31:13.104 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8082

Run bookrecommendation microservice:

bookrecommendation $ ./gradlew run
...
> Task :bookrecommendation:run
14:31:57.389 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 523ms. Server Running: http://localhost:8080

Open http://localhost:8761 in your browser.

You will see the services registered in Eureka:

eurekaui

You can run a cURL command to test the whole application:

$ curl http://localhost:8080/books
[{"name":"Building Microservices"}

5 Next Steps

Read more about Eureka Support inside Micronaut.