Sending and receiving JSON messages with Spring Boot AMQP and RabbitMQ

Sending and receiving JSON messages with Spring Boot AMQP and RabbitMQ

Setting up a Hello-World Spring Boot application using AMQP with RabbitMQ is quite easy if you go for the basic setup, as many other guides do. However, the configuration is not so straightforward when you get into the serialization setup and want to make use of @RabbitListener annotations to produce and consume messages in JSON format. Therefore, I’m sharing with you a really simple but more serious approach that those hello-messaging apps, supported with a real-life practical example.

This article covers:

  • How to send/publish Java Objects as JSON messages using Spring Boot and RabbitMQ’s RabbitTemplate.
  • How to read/consume JSON messages as Java Objects using Spring Boot and RabbitMQ’s @RabbitListener annotation.
  • How to send and receive Java Objects through RabbitMQ using default Java serializer.
Table of Contents
Updated: The code examples now use Java 16 and Spring Boot 2.4.4

Video (without the update)

Do you prefer the Video version? It uses previous Java and Spring Boot versions, but it contains not only the explanation about how to set up the configuration to send and receive JSON messages but also some extra details about how the Message Converters work. The complete video is 29min, but here you have the shortcuts to the different sections:

If you prefer the most updated code (or you don’t like videos) just keep reading for the complete updated guide.

Introduction

As you probably know if you reached this article, AMQP is a specification, a protocol that defines messaging and patterns for communication between systems. RabbitMQ is a software that implements AMQP. If you want to know more about messaging systems and RabbitMQ in particular, check out my book since it dedicates a complete chapter to these concepts.

Goal

Within this article we’ll set up a Spring Boot application that uses RabbitMQ to send messages to a Topic exchange formatted as JSON, and read them using two different queues:

  • The first Queue will read the contents as a generic org.springframework.amqp.core.Message class.
  • The second Queue will transform the JSON message to a specific class created by us, CustomMessage.

See Figure 1 for some visual help.

Figure 1. Serializing a RabbitMQ message and deserializing in two Queues

I’ll also show you an alternative configuration to make this work without formatting the message to JSON, although nowadays this format is a good choice for intercommunication between applications and systems that may differ in programming languages, for instance.

I recommend you to use the code examples as you follow these instructions. You can do so by cloning the GitHub repository. Don’t forget to give it a star if you find it useful!

You can find all the source code on GitHub: Spring Boot JSON AMQP messaging. If you like it, give it a star!

Setting up the project

pom.xml

Creating a simple Spring Boot application is pretty straightforward, in my case I use Spring Boot 2.4 and Maven. You can go to the website start.spring.io and select the version and the included dependencies. The only thing you need to do to make use of AMQP with RabbitMQ and JSON in Spring Boot is to include the corresponding starters for AMQP and the Jackson libraries. See Listing 1.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.thepracticaldeveloper</groupId>
    <artifactId>spring-boot-amqp-messaging</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring Boot AMQP messaging</name>
    <description>Skeleton project for RabbitMQ JSON Configuration</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>16</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Listing 1. Project’s dependencies in pom.xml

The message

We’ll send a very simple message over RabbitMQ, but it’s good enough to get an idea of how the serialization looks like in different formats. Meet the CustomMessage record in Listing 2.

package com.thepracticaldeveloper.rabbitmqconfig;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonProperty;

public record CustomMessage(@JsonProperty("text") String text,
                            @JsonProperty("priority") int priority,
                            @JsonProperty("secret") boolean secret)
        implements Serializable {
}

Listing 2. The CustomMessage Java Record

Learn Microservices with Spring Boot - Second Edition

As you can see, it’s a simple object where we put some text, an integer named priority, and whether the message is secret or not. We won’t build any business logic on top of these three fields, yet is good to add them to work with content that is close to real-life scenarios. There are only two remarks here:

  • We need to use @JsonProperty in the declaration of our Java record to instruct the JSON libraries to use the corresponding field names when serializing and deserializing.
  • We need to mark our record as serializable, also for the Jackson libraries to work fine.

Sending messages

Our practical example will use a topic of type Exchange to send the CustomMessage objects. See Listing 3.

package com.thepracticaldeveloper.rabbitmqconfig;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class CustomMessageSender {

    private static final Logger log = LoggerFactory.getLogger(CustomMessageSender.class);

    private final RabbitTemplate rabbitTemplate;

    public CustomMessageSender(final RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @Scheduled(fixedDelay = 3000L)
    public void sendMessage() {
        final var message = new CustomMessage("Hello there!", new Random().nextInt(50), false);
        log.info("Sending message...");
        rabbitTemplate.convertAndSend(MessagingApplication.EXCHANGE_NAME, MessagingApplication.ROUTING_KEY, message);
    }
}

Listing 2. The CustomMessage Java Record

Some comments about this service:

  • The @Scheduled annotation is used to schedule a method to be executed at a given time, or with a given frequency. In this case, this method will be executed every 3 seconds (until the end of the world or until you stop the application, whatever happens earlier). Check the Spring docs if you want to know more about how this works.
  • RabbitTemplate is used to convert and send a message using RabbitMQ. It is a helper class, as many other Template classes existing in Spring (such as JdbcTemplate, RestTemplate, etc.). Spring Boot AMQP provides a default RabbitTemplate, but we will need to tune it a bit to use JSON to serialize the messages.
  • The exchange and the routing key are AMQP concepts. There are multiple types of exchange for different use cases, and routing keys help RabbitMQ know where to put the messages to be consumed. I recommend you to get my book if you want to dive deeper into these concepts and learn how RabbitMQ can support a microservices architecture. As a summary, we are sending messages through a given channel (exchange), and the routing key allows us to filter for who are those generated.

We’ll see later where to declare the Exchange topic, the Queue, and the binding that uses the Routing Key.

Receiving messages

We need to build the other side of the communication too. For this post, I’ve created the receivers in the same application for better logistics (having a single repo), but you shouldn’t have problems to replicate this configuration with two different apps if you want. Actually, you could copy this Spring Boot application twice, remove the producer in one of the copies and the consumers in the other one, and run both. See the RabbitMQ consumer in Listing 3.

package com.thepracticaldeveloper.rabbitmqconfig;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class CustomMessageListener {

    private static final Logger log = LoggerFactory.getLogger(CustomMessageListener.class);

    @RabbitListener(queues = MessagingApplication.QUEUE_GENERIC_NAME)
    public void receiveMessage(final Message message) {
        log.info("Received message as a generic AMQP 'Message' wrapper: {}", message.toString());
    }

    @RabbitListener(queues = MessagingApplication.QUEUE_SPECIFIC_NAME)
    public void receiveMessage(final CustomMessage customMessage) {
        log.info("Received message and deserialized to 'CustomMessage': {}", customMessage.toString());
    }
}

Listing 3. A RabbitMQ consumer using @RabbitListener annotations

Here you can see our goal: the idea is to show at the same time how the same message, produced by the CustomMessageSender class, can be received through two different queues in two different formats. First queue (GENERIC) receives the message as a generic AMQP Message class, so there is no implicit conversion. Spoiler alert: when we log the message we should see a JSON string in the payload since it has not been converted to a Java class.

On the other hand, the queue SPECIFIC declares a CustomMessage argument in the method annotated with RabbitListener. This will trigger a logic inside Spring to find a converter from JSON to that specific class. Even though this part should be easy to configure, sometimes is the part in which we the developers struggle most. In this case is easy to guess that the text logged should be the CustomMessage’s implementation of toString() (which is itself the default implementation for records).

Configuration

This is the most interesting part since it’s what makes everything work together. First, you will find some annotations in the MessagingApplication class as shown in Listing 4.

@SpringBootApplication
@EnableScheduling
public class MessagingApplication {

Listing 4. Annotations in the main Spring Boot class

Apart from the one that makes Spring Boot works (@SpringBootApplication), we need to add the @EnableScheduling annotation to use the @Scheduled annotated method to send messages we saw earlier. As a side note, you don’t need to add the Spring’s @EnableRabbit annotation since RabbitMQ will be enabled automatically just because you added the dependency. This is just how Spring Boot autoconfiguration works (also detailed in my book if you’re interested).

Below these annotations, you’ll find some constants and, next to them, the Exchange / Queue configuration. See Listing 5.

@Bean
public TopicExchange appExchange() {
    return new TopicExchange(EXCHANGE_NAME);
}

@Bean
public Queue appQueueGeneric() {
    return new Queue(QUEUE_GENERIC_NAME);
}

@Bean
public Queue appQueueSpecific() {
    return new Queue(QUEUE_SPECIFIC_NAME);
}

@Bean
public Binding declareBindingGeneric() {
    return BindingBuilder.bind(appQueueGeneric()).to(appExchange()).with(ROUTING_KEY);
}

@Bean
public Binding declareBindingSpecific() {
    return BindingBuilder.bind(appQueueSpecific()).to(appExchange()).with(ROUTING_KEY);
}

Listing 5. Declaration of RabbitMQ objects via bean declaration

These are the topic exchange, both queues, and the bindings between them. Both queues are bound to the same exchange with the same routing key, so they’ll get both a copy of each message that gets produced. This is just an example configuration, you could use many more depending on your needs.

At the bottom, in the same class, we can find the configuration related to producing and consuming JSON messages. See Listing 6.

@Bean
public RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory) {
    final var rabbitTemplate = new RabbitTemplate(connectionFactory);
    rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
    return rabbitTemplate;
}

@Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
    return new Jackson2JsonMessageConverter();
}

Listing 6. RabbitTemplate configuration for JSON serialization

Learn Microservices with Spring Boot - Second Edition

With that configuration, we override the default RabbitTemplate bean created by Spring Boot with another one that will use a message converter of type Jackson2JsonMessageConverter. Remember: this configuration only affects to messages sent via RabbitTemplate.

The bean Jackson2JsonMessageConverter will also take care of deserializing the JSON messages to Java classes, using a default Jackson’s ObjectMapper.

Note that, with this configuration, you don’t need to implement the interface RabbitListenerConfigurer nor use a MappingJackson2MessageConverter as it was needed in a previous version of this guide.

Running the application

First, you need to make sure that the RabbitMQ server is running. In the GitHub repository, there is a docker-compose.yml file that you can use if you have Docker installed. Just run:

docker-compose up -d

Also, if you want to use a configuration which is different from the default one to connect to RabbitMQ, make sure you include those changes within the application.properties file (port number, authentication, etc.). You don’t need to do this if you use the default settings as they are set in this sample codebase (unless your docker host IP is not localhost, then you might need to tweak the settings).

You can run the application class from your preferred IDE or executing mvn spring-boot:run from the command line (you can also use the embedded Maven wrapper for that). See Listing 7 for the console output.

INFO 23562 --- [   scheduling-1] c.t.rabbitmqconfig.CustomMessageSender   : Sending message...
INFO 23562 --- [ntContainer#0-1] c.t.r.CustomMessageListener              : Received message as a generic AMQP 'Message' wrapper: (Body:'{"text":"Hello there!","priority":26,"secret":false}' MessageProperties [headers={__TypeId__=com.thepracticaldeveloper.rabbitmqconfig.CustomMessage}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=appExchange, receivedRoutingKey=messages.key, deliveryTag=3, consumerTag=amq.ctag-Q2Y3DUjFV_YHDPjQPOLuiQ, consumerQueue=appGenericQueue])
INFO 23562 --- [ntContainer#1-1] c.t.r.CustomMessageListener              : Received message and deserialized to 'CustomMessage': CustomMessage[text=Hello there!, priority=26, secret=false]

Listing 7. Running the application with JSON serialization

It works as expected! Let’s describe what happens here.

  • The producer (CustomMessageSender) sends a message via the Exchange topic.
  • The listener method consuming generic Message objects gets all the RabbitMQ headers and the payload’s body, which gets deserialized as a string. Since the message got produced as JSON, we see that representation there.
  • The other listener declares a CustomMessage argument, so Spring simply takes the payload body and uses the JSON representation to deserialize it to Java. It succeeds, so we can work directly with the object. In our case, we print it as a String, and that’s what we see in the console. It’s the standard toString() format for a Java record.

Changing from JSON format the default serialization

You can comment the lines that set up the message converters. That’s the complete contents of Listing 6. What you will see in that case is the default configuration for Spring Boot, which uses a Java serialization to encode messages.

INFO 25142 --- [   scheduling-1] c.t.rabbitmqconfig.CustomMessageSender   : Sending message...
INFO 25142 --- [ntContainer#0-1] c.t.r.CustomMessageListener              : Received message as a generic AMQP 'Message' wrapper: (Body:'[[email protected](byte[143])' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=appExchange, receivedRoutingKey=messages.key, deliveryTag=7, consumerTag=amq.ctag-9WtY0LRLuKgRrIzv3NiAbg, consumerQueue=appGenericQueue])
INFO 25142 --- [ntContainer#1-1] c.t.r.CustomMessageListener              : Received message and deserialized to 'CustomMessage': CustomMessage[text=Hello there!, priority=2, secret=false]

Now, as you can see in the headers of the Message object, the format of the contents (contentType) is application/x-java-serialized-object. This is just another approach for serialization, which could turn out to be not the best one if you are using, for instance, different types of applications (non-Java) for receiving messages. On the other hand, the payload is smaller, which can lead to better performance if you transfer a lot of data.

In any case, the serialization and deserialization works perfectly. Normally, you would go for the second option and abstract the serialization format from your listeners, as in the CustomMessage listener, which behaves exactly the same as before.

More info

I hope you found this guide useful. If you are interested in using RabbitMQ in a Microservice Architecture to support an Event-Driven Architecture, and some other related topics like Service Discovery, Routing, Async vs. Sync communication, etc., do not miss my book Learn Microservices with Spring Boot. It will guide you step by step to get real experience, looking also at the disadvantages of such kind of setup.

Get the book on Amazon
Moisés Macero's Picture

About Moisés Macero

Software Developer, Architect, and Author.
Are you interested in my workshops?

Málaga, Spain https://thepracticaldeveloper.com

Comments