RABBITMQ_URI=amqp://production-server:5672
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: 4.6.3
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 (e.g. IntelliJ IDEA)
-
JDK 21 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.
-
Download and unzip the source
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.
5. Test Resources
When the application is started locally — either under test or by running the application — resolution of the rabbitmq.uri
property is detected and the Test Resources service will start a local RabbitMQ docker container, and inject the properties required to use this as the message broker.
When running under production, you should replace this property with the location of your production message broker via an environment variable.
For more information, see the RabbitMQ section of the Test Resources documentation.
5.1. 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=gradle --lang=java
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
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. |
5.1.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
:
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
public class ChannelPoolListener extends ChannelInitializer {
@Override
public void initialize(Channel channel, String name) throws IOException {
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.
|
5.1.2. Create consumer
Create a BookCatalogueService
class to handle incoming RPC requests into the bookcatalogue
microservice:
package example.micronaut;
import io.micronaut.rabbitmq.annotation.Queue;
import io.micronaut.rabbitmq.annotation.RabbitListener;
import java.util.Arrays;
import java.util.List;
@RabbitListener (1)
public class BookCatalogueService {
@Queue("catalogue") (2)
List<Book> listBooks() {
Book buildingMicroservices = new Book("1491950358", "Building Microservices");
Book releaseIt = new Book("1680502395", "Release It!");
Book cidelivery = new Book("0321601912", "Continuous Delivery:");
return Arrays.asList(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:
package example.micronaut;
import io.micronaut.serde.annotation.Serdeable;
import java.util.Objects;
@Serdeable
public class Book {
private final String isbn;
private final String name;
public Book(String isbn, String name) {
this.isbn = isbn;
this.name = name;
}
public String getIsbn() {
return isbn;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Book{" +
"isbn='" + isbn + '\'' +
", name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(isbn, book.isbn) &&
Objects.equals(name, book.name);
}
@Override
public int hashCode() {
return Objects.hash(isbn, name);
}
}
5.2. 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=gradle --lang=java
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
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
.
5.2.1. Create RabbitMQ exchange, queue and binding
Copy the ChannelPoolListener
class you created in the bookcatalogue
microservice to bookinventory/src/main/java/example/micronaut/bookcatalogue
.
5.2.2. Create consumer
Create a BookInventoryService
class to handle incoming RPC requests into the bookinventory
microservice:
package example.micronaut;
import io.micronaut.rabbitmq.annotation.Queue;
import io.micronaut.rabbitmq.annotation.RabbitListener;
import jakarta.validation.constraints.NotBlank;
import java.util.Optional;
@RabbitListener (1)
public class BookInventoryService {
@Queue("inventory") (2)
public Boolean stock(@NotBlank String isbn) {
return bookInventoryByIsbn(isbn).map(bi -> bi.getStock() > 0).orElse(null);
}
private Optional<BookInventory> bookInventoryByIsbn(String isbn) {
if (isbn.equals("1491950358")) {
return Optional.of(new BookInventory(isbn, 4));
}
if (isbn.equals("1680502395")) {
return Optional.of(new BookInventory(isbn, 0));
}
return Optional.empty();
}
}
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:
package example.micronaut;
import io.micronaut.serde.annotation.Serdeable;
import java.util.Objects;
@Serdeable
public class BookInventory {
private final String isbn;
private final Integer stock;
public BookInventory(String isbn, Integer stock) {
this.isbn = isbn;
this.stock = stock;
}
public String getIsbn() {
return isbn;
}
public Integer getStock() {
return stock;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BookInventory that = (BookInventory) o;
return Objects.equals(isbn, that.isbn) &&
Objects.equals(stock, that.stock);
}
@Override
public int hashCode() {
return Objects.hash(isbn, stock);
}
}
5.3. 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=gradle --lang=java
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
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. |
5.3.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
.
5.3.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
:
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;
import java.util.List;
@RabbitClient("micronaut") (1)
@RabbitProperty(name = "replyTo", value = "amq.rabbitmq.reply-to") (2)
public interface CatalogueClient {
@Binding("books.catalogue") (3)
Publisher<List<Book>> findAll(byte[] data); (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
:
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)
public interface InventoryClient {
@Binding("books.inventory") (3)
Mono<Boolean> stock(String isbn); (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. |
5.3.3. Create the controller
Create a Controller that injects both clients.
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)
public class BookController {
private final CatalogueClient catalogueClient; (2)
private final InventoryClient inventoryClient; (2)
public BookController(CatalogueClient catalogueClient, InventoryClient inventoryClient) { (2)
this.catalogueClient = catalogueClient;
this.inventoryClient = inventoryClient;
}
@Get (3)
public Publisher<BookRecommendation> index() {
return Flux.from(catalogueClient.findAll(null))
.flatMap(Flux::fromIterable)
.flatMap(book -> Flux.from(inventoryClient.stock(book.getIsbn()))
.filter(Boolean::booleanValue)
.map(response -> book))
.map(book -> new BookRecommendation(book.getName()));
}
}
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:
package example.micronaut;
import io.micronaut.serde.annotation.Serdeable;
import java.util.Objects;
@Serdeable
public class BookRecommendation {
private final String name;
public BookRecommendation(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BookRecommendation that = (BookRecommendation) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
6. RabbitMQ and the Micronaut Framework
As mentioned above a shared dockerized instance of RabbitMQ will be started automatically when you run the application, however if you want to start your own instance of RabbitMQ, you have a few options:
6.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.2. Alternative methods
Alternatively you can install and run a local RabbitMQ instance.
7. Running the Application
Configure bookinventory
to run on port 8082:
micronaut:
server:
port: 8082 (1)
Run bookinventory
microservice:
./gradlew 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:
micronaut:
server:
port: 8081 (1)
Run bookcatalogue
microservice:
./gradlew 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:
micronaut:
server:
port: 8080 (1)
8080 is the default port if you don’t specify micronaut.server.port property
|
Run bookrecommendation
microservice:
./gradlew 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"}]
8. Generate a Micronaut Application Native Executable with GraalVM
We will use GraalVM, an advanced JDK with ahead-of-time Native Image compilation, to generate a native executable of this Micronaut application.
Compiling Micronaut applications ahead of time with GraalVM significantly 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.
|
8.1. GraalVM Installation
sdk install java 21.0.5-graal
For installation on Windows, or for a manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
The previous command installs Oracle GraalVM, which is free to use in production and free to redistribute, at no cost, under the GraalVM Free Terms and Conditions.
Alternatively, you can use the GraalVM Community Edition:
sdk install java 21.0.2-graalce
8.2. Native Executable Generation
To generate a native executable using Gradle, run:
./gradlew nativeCompile
The native executable is created in build/native/nativeCompile
directory and can be run with build/native/nativeCompile/micronautguide
.
It is possible to customize the name of the native executable or pass additional parameters to GraalVM:
graalvmNative {
binaries {
main {
imageName.set('mn-graalvm-application') (1)
buildArgs.add('-Ob') (2)
}
}
}
1 | The native executable name will now be mn-graalvm-application |
2 | It is possible to pass extra build arguments to native-image . For example, -Ob enables the quick build mode. |
Start the native executables for the two microservices and run the same curl
request as before to check that everything works with GraalVM.
9. Next Steps
Read more about RabbitMQ RPC Support in the Micronaut framework.
10. License
All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…). |