RabbitMQ RPC and the Micronaut Framework

Use RabbitMQ RPC to use request-reply pattern in your Micronaut applications.

Authors: Iván López

Micronaut Version: 3.2.7

1. Getting Started

In this guide, we will create three microservices that will communicate with each other with RabbitMQ using the request-response pattern with RPC (Remote Procedure Call).

RabbitMQ is an open-source message-broker software that originally implemented the Advanced Message Queuing Protocol (AMQP) and has since been extended with a plug-in architecture to support Streaming Text Oriented Messaging Protocol (STOMP), Message Queuing Telemetry Transport (MQTT), and other protocols.

2. 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

3. Solution

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

4. Writing the Application

Let’s describe the microservices you will build through the guide.

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

  • bookinventory - This exposes an endpoint to check whether a book has sufficient stock to fulfill an order. It uses a domain consisting of a stock level and an ISBN.

  • bookrecommendation - This consumes previous services and exposes an endpoint that recommends book names that are in stock.

4.1. Enable annotation Processing

If you use Java or Kotlin and IntelliJ IDEA, make sure to enable annotation processing.

annotationprocessorsintellij

4.2. Catalogue microservice

Create the bookcatalogue microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app --features=rabbitmq,graalvm example.micronaut.bookcatalogue --build=maven --lang=kotlin
If you don’t specify the --build argument, Gradle is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.

If you use Micronaut Launch, select Micronaut Application as application type and add the rabbitmq and graalvm features.

The previous command creates a directory named bookcatalogue and a Micronaut application inside it with default package example.micronaut.

If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features and apply those changes to your application.

By default, a Micronaut application will connect to a RabbitMQ instance running on localhost, so it is not necessary to add anything to application.yml. In case you want to change the configuration, add the following:

bookcatalogue/src/main/resources/application.yml
rabbitmq:
  uri: amqp://localhost:5672

4.2.1. Create RabbitMQ exchange, queue, and binding

Before being able to send and receive messages using RabbitMQ, it is necessary to define the exchange, queue, and binding. One option is to create them directly in the RabbitMQ Admin UI available on http://localhost:15672. Use guest for both username and password.

Another option is to create them programmatically. Create the class ChannelPoolListener.java:

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

import com.rabbitmq.client.BuiltinExchangeType
import com.rabbitmq.client.Channel
import io.micronaut.rabbitmq.connect.ChannelInitializer
import jakarta.inject.Singleton
import java.io.IOException

@Singleton
class ChannelPoolListener : ChannelInitializer() {

    @Throws(IOException::class)
    override fun initialize(channel: Channel) {
        channel.exchangeDeclare("micronaut", BuiltinExchangeType.DIRECT, true) (1)
        channel.queueDeclare("inventory", true, false, false, null) (2)
        channel.queueBind("inventory", "micronaut", "books.inventory") (3)
        channel.queueDeclare("catalogue", true, false, false, null) (4)
        channel.queueBind("catalogue", "micronaut", "books.catalogue") (5)
    }
}
1 Define an exchange named micronaut. From the producer point of view, everything is sent to the exchange with the appropriate routing key.
2 Define a queue named inventory. The consumer will listen for messages in that queue.
3 Define a binding between the exchange and the queue using the routing key books.inventory.
4 Define a queue named catalogue. The consumer will listen for messages in that queue.
5 Define a binding between the exchange and the queue using the routing key books.catalogue.
In this Catalogue Microservice, the only necessary element is the catalogue queue, but it is a good practice to define all the elements in the same file and share the file between all the projects.

4.2.2. Create consumer

Create a BookCatalogueService class to handle incoming RPC requests into the bookcatalogue microservice:

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

import io.micronaut.rabbitmq.annotation.Queue
import io.micronaut.rabbitmq.annotation.RabbitListener

@RabbitListener (1)
class BookCatalogueService {

    @Queue("catalogue") (2)
    fun listBooks(): List<Book> {
        val buildingMicroservices = Book("1491950358", "Building Microservices")
        val releaseIt = Book("1680502395", "Release It!")
        val cidelivery = Book("0321601912", "Continuous Delivery:")

        return listOf(buildingMicroservices, releaseIt, cidelivery)
    }
}
1 Annotate the class with @RabbitListener to indicate that this bean will consume messages from RabbitMQ.
2 Annotate the method with @Queue. This listener will listen to messages in the catalogue queue.

The previous service responds a List<Book>. Create the Book POJO:

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

import io.micronaut.core.annotation.Introspected

@Introspected
data class Book(val isbn: String, val name: String)

4.3. Inventory microservice

Create the bookinventory microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app --features=rabbitmq,graalvm example.micronaut.bookinventory --build=maven --lang=kotlin
If you don’t specify the --build argument, Gradle is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.

If you use Micronaut Launch, select Micronaut Application as application type and add the rabbitmq and graalvm features.

The previous command creates a directory named bookinventory and a Micronaut application inside it with default package example.micronaut.

If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features and apply those changes to your application.

The previous command creates a directory named bookinventory and a Micronaut application inside it with default package example.micronaut.

4.3.1. Create RabbitMQ exchange, queue and binding

Copy the ChannelPoolListener class you created in the bookcatalogue microservice to bookinventory/src/main/kotlin/example/micronaut/bookcatalogue.

4.3.2. Create consumer

Create a BookInventoryService class to handle incoming RPC requests into the bookinventory microservice:

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

import io.micronaut.rabbitmq.annotation.Queue
import io.micronaut.rabbitmq.annotation.RabbitListener
import java.util.Optional
import javax.validation.constraints.NotBlank

@RabbitListener (1)
class BookInventoryService {

    @Queue("inventory") (2)
    fun stock(isbn: @NotBlank String?): Boolean? =
        bookInventoryByIsbn(isbn).map { (_, stock): BookInventory -> stock > 0 }.orElse(null)

    private fun bookInventoryByIsbn(isbn: String?): Optional<BookInventory> =
        if (isbn.equals("1491950358")) {
            Optional.of(BookInventory(isbn!!, 4))
        } else if (isbn.equals("1680502395")) {
            Optional.of(BookInventory(isbn!!, 0))
        } else {
            Optional.empty<Any>() as Optional<BookInventory>
        }
}
1 Annotate the class with @RabbitListener to indicate that this bean will consume messages from RabbitMQ.
2 Annotate the method with @Queue. This listener will listen to messages in inventory queue.

The previous service uses BookInventory POJO. Create it:

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

import io.micronaut.core.annotation.Introspected

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

4.4. Recommendation microservice

Create the bookrecommendation microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app --features=rabbitmq,reactor,graalvm example.micronaut.bookrecommendation --build=maven --lang=kotlin
If you don’t specify the --build argument, Gradle is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.

If you use Micronaut Launch, select Micronaut Application as application type and add the rabbitmq, reactor, and graalvm features.

The previous command creates a directory named bookrecommendation and a Micronaut application inside it with default package example.micronaut.

If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features and apply those changes to your application.

4.4.1. Create RabbitMQ exchange, queue and binding

Copy the ChannelPoolListener class you created in the bookcatalogue microservice to bookrecommendation/src/main/java/example/micronaut/bookcatalogue.

4.4.2. Create clients

Let’s create two interfaces to send messages to RabbitMQ. The Micronaut framework will implement the interfaces at compilation time. Create CatalogueClient:

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

import io.micronaut.rabbitmq.annotation.Binding
import io.micronaut.rabbitmq.annotation.RabbitClient
import io.micronaut.rabbitmq.annotation.RabbitProperty
import org.reactivestreams.Publisher

@RabbitClient("micronaut") (1)
@RabbitProperty(name = "replyTo", value = "amq.rabbitmq.reply-to") (2)
interface CatalogueClient {

    @Binding("books.catalogue") (3)
    fun findAll(data: ByteArray?): Publisher<List<Book>> (4)
}
1 Send the messages to exchange micronaut.
2 Set the replyTo property to amq.rabbitmq.reply-to. This is a special queue that always exists and does not need to be created. That is why we did not create the queue in the ChannelInitializer. RabbitMQ uses that queue in a special way, and setting the value of the property replyTo to that queue will enable this call as an RPC one. RabbitMQ will create a temporary queue for the callback.
3 Set the routing key.
4 Define the method that will "mirror" the one in the consumer. Keep in mind that in the consumer, it is not possible to return a reactive type, but on the client side it is. Also, it is necessary to send something, even if it’s not used in the consumer.

Create InventoryClient.java:

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

import io.micronaut.rabbitmq.annotation.Binding
import io.micronaut.rabbitmq.annotation.RabbitClient
import io.micronaut.rabbitmq.annotation.RabbitProperty
import reactor.core.publisher.Mono

@RabbitClient("micronaut") (1)
@RabbitProperty(name = "replyTo", value = "amq.rabbitmq.reply-to") (2)
interface InventoryClient {

    @Binding("books.inventory")(3)
    fun stock(isbn: String): Mono<Boolean?> (4)
}
1 Send the messages to exchange micronaut.
2 Set the replyTo property to amq.rabbitmq.reply-to.
3 Set the routing key.
4 Define the method that will "mirror" the one in the consumer. As we did with CatalogueClient, we use a reactive type to wrap the result.

4.4.3. Create the controller

Create a Controller that injects both clients.

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

import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux

@Controller("/books") (1)
class BookController(
        private val catalogueClient: CatalogueClient, (2)
        private val inventoryClient: InventoryClient) {

    @Get (3)
    fun index(): Publisher<BookRecommendation> =
        Flux.from(catalogueClient.findAll(null))
                .flatMap { Flux.fromIterable(it) }
                .flatMap { book: Book ->
                    Flux.from(inventoryClient.stock(book.isbn))
                            .filter { obj: Boolean? -> obj == true }
                            .map { book }
                }
                .map { (_, name): Book -> BookRecommendation(name) }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /books
2 Clients are injected via constructor injection
3 The @Get annotation maps the index method to an HTTP GET request on /books

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

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

import io.micronaut.core.annotation.Introspected

@Introspected
class BookRecommendation(val name: String)

5. RabbitMQ and the Micronaut Framework

5.1. Install RabbitMQ via Docker

The fastest way to start using RabbitMQ is via Docker:

docker run --rm -it \
        -p 5672:5672 \
        -p 15672:15672 \
        rabbitmq:3.8.12-management

6. Running the Application

Configure bookinventory to run on port 8082:

bookinventory/src/main/resources/application.yml
micronaut:
  server:
    port: 8082 (1)

Run bookinventory microservice:

bookinventory
./mvnw mn:run
13:30:22.426 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 742ms. Server Running: 1 active message listeners.

Configure bookcatalogue to run on port 8081:

bookcatalogue/src/main/resources/application.yml
micronaut:
  server:
    port: 8081 (1)

Run bookcatalogue microservice:

bookcatalogue
./mvnw mn:run
13:31:19.887 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 949ms. Server Running: 1 active message listeners.

Configure bookrecommendation to run on port 8080:

bookrecommendation/src/main/resources/application.yml
micronaut:
  server:
    port: 8080 (1)
8080 is the default port if you don’t specify micronaut.server.port property

Run bookrecommendation microservice:

bookrecommendation
./mvnw mn:run
13:32:06.045 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 959ms. Server Running: http://localhost:8080

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

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

7. Generate a Micronaut Application Native Image with GraalVM

We will use GraalVM, the polyglot embeddable virtual machine, to generate a native image of our Micronaut application.

Compiling native images ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.

Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.

7.1. Native image generation

The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.

Java 11
$ sdk install java 22.0.0.2.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM.
Java 17
$ sdk install java 22.0.0.2.r17-grl

For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.

After installing GraalVM, install the native-image component, which is not installed by default:

gu install native-image

To generate a native image using Maven, run:

./mvnw package -Dpackaging=native-image

The native image is created in the target directory and can be run with target/application.

Start the native images for the two microservices and run the same curl request as before to check that everything works with GraalVM.

8. Next Steps

Read more about RabbitMQ RPC Support in the Micronaut framework.