Spring Boot + RabbitMQ (2023) - Error Handling Example
In this tutorial we will be implementing a RabbitMQ Error Handling with the help of example.Rabbitmq provides a way by which failure of message can be handled in a very smooth manner. This feature of the RabbitMQ is known as Retry and Error Handling feature. Whenever any data in the message is passed, that is not acceptable by the Receiver or there may be cases that Message is sent to the queue that is not present. The message is retried and sent again upto a certain attempts. Still if the message is not received by the receiver but sent from the sender side.
Now In such cases, the message queue is declared as undeliverable message queue or deadLetter queue.
The flow can be expressed in the following manner-
In this example, if the age passed in the RequestParam is not satisfying the condition, then the retry and error handling feature will come into picture and the message queue is declared as deadLetter qeue.
Project Structure
Sender
The sender is responsible for sending the message and put the message in the rabbitmq queue.-
This will be the standard directory layout for maven project structure-
We need to start by creating a Maven pom.xml(Project Object Model) file. The pom.xml file contains the project configuration details.<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.1</version> <relativePath /> <!-- lookup parent from repository --> </parent> <groupId>com.codeusingjava</groupId> <artifactId>RabbitMqErrorHandling</artifactId> <version>0.0.1-SNAPSHOT</version> <name>RabbitMqErrorHandling</name> <description>Spring boot Rabbitmq Error Handling</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Create the model class for the student.
We have used lombok dependency which generates the getter and setter only by adding dependency.package com.codeusingjava.model; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.ObjectIdGenerators; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Setter @ToString @Getter @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id", scope = Student.class) public class Student { private String name; private String hobby; private int age; }
The configuration class for the sender can be defined in the following manner-
For the configuration of a dead letter, we need to use x-dead-letter-exchange and x-dead-letter-routing-key. It tells the broker to use the default exchange.
The binding of dlq is done with the deadLetterExchange and codeusingjava with the codeusingjavaExchange.
The Jackson2JsonMessageConverter converts the object to JSON and then JSON to object of java.package com.codeusingjava.config; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQSenderConfig { @Bean DirectExchange deadLetterExchange() { return new DirectExchange("deadLetterExchange"); } @Bean DirectExchange exchange() { return new DirectExchange("codeusingjavaExchange"); } @Bean Queue dlq() { return QueueBuilder.durable("deadLetter.queue").build(); } @Bean Queue queue() { return QueueBuilder.durable("codeusingjava.queue").withArgument("x-dead-letter-exchange", "deadLetterExchange") .withArgument("x-dead-letter-routing-key", "deadLetter").build(); } @Bean Binding DLQbinding() { return BindingBuilder.bind(dlq()).to(deadLetterExchange()).with("deadLetter"); } @Bean Binding binding() { return BindingBuilder.bind(queue()).to(exchange()).with("codeusingjava"); } @Bean public MessageConverter jsonMessageConverter() { return new Jackson2JsonMessageConverter(); } public AmqpTemplate rabbitTemplate(ConnectionFactory connectionFactory) { final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMessageConverter(jsonMessageConverter()); return rabbitTemplate; } }
This class contains the mapping which will be executed for sending the message.
The convertAndSend method first converts the java object to an amqp message and then this message is sent to the exchange using the routing key.package com.codeusingjava.controller; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.codeusingjava.model.Student; @RestController public class RabbitMQSenderController { @Autowired private AmqpTemplate amqpTemplate; @GetMapping(value = "/sendmsg") public String producer(@RequestParam("name") String name,@RequestParam("hobby") String hobby,@RequestParam("age") int age) { Student student=new Student(); student.setName(name); student.setHobby(hobby); student.setAge(age); amqpTemplate.convertAndSend("codeusingjavaExchange", "codeusingjava", student); return "Message sent to RabbitMQ server"; } }
The main class for the Rabbitmq can bve defined as follows-package com.codeusingjava; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RabbitMqSenderErrorHandlingApplication { public static void main(String[] args) { SpringApplication.run(RabbitMqSenderErrorHandlingApplication.class, args); } }
Receiver
The Receiver class is the one who receives the message sent by the Sender. The project structure for the RabbitmqReceiver can be as follows-
The pom.xml file for the Receiver class is as follows-<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.1</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.codeusingjava</groupId> <artifactId>RabbitMqErrorHandlingConsumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>RabbitMqErrorHandlingConsumer</name> <description>Spring boot Rabbitmq Error Handling</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
The application.yml class is defined as follows-
Here we have defined the max-attempts for retrying to send the message, initial as well as max-interval.
After exceeding these limits, Exception is thrown and the message is declared as deadLetter queue.spring: rabbitmq: listener: simple: retry: enabled: true initial-interval: 3s max-attempts: 6 max-interval: 10s multiplier: 2 server: port: 8081
The model class Student can be defined as follows- @JsonIdentityInfo helps in serialising the instance of an object having the id of the object.package com.codeusingjava.model; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.ObjectIdGenerators; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Setter @ToString @Getter @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@id", scope = Student.class) public class Student { private String name; private String hobby; private int age; }
We define an exception class named as InvalidAgeException class.
An exception will be thrown if any invalid age is passed in the parameter.package com.codeusingjava.exception; public class InvalidAgeException extends Exception { private static final long serialVersionUID = -3154618962130084535L; }
The service class for the Receiver is as follows-
It helps the message to filter on the basis of age of the Student. If the age of Student is less then 0 or greater then 25, then the message will be rejected.
The RabbitListener listens the queue for any incoming message.package com.codeusingjava.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import com.codeusingjava.exception.InvalidAgeException; import com.codeusingjava.model.Student; @Component public class ReceiverService { private static final Logger logger = LoggerFactory.getLogger(ReceiverService.class); @RabbitListener(queues = "codeusingjava.queue") public void recievedMessage(Student student) throws InvalidAgeException { logger.info("Recieved Message From RabbitMQ: " + student); if (student.getAge() < 0 || student.getAge() > 25 ) { throw new InvalidAgeException(); } } }
The main class for the RabbitmqReceiver application is as follows-
The Jackson2JsonMessageConverter converts the object to JSON and then JSON to object of java.package com.codeusingjava; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class RabbitMqErrorHandlingReceiver { public static void main(String[] args) { SpringApplication.run(RabbitMqErrorHandlingReceiver.class, args); } @Bean public Jackson2JsonMessageConverter jackson2JsonMessageConverter() { return new Jackson2JsonMessageConverter(); } }
Run the application
Firstly we need to run both the sender and the Receiver as Spring boot application. Run the folllowing url in the browser:-http://localhost:8080/sendmsg?name=student1&hobby=dance&age=-5
As we can see, the age passed in the RequestParam is negative which is invalid and not acceptable by the Receiver.
Hence after several attempt, the InvalidAgeException is thrown and the message is declared as deadLetter queue.
The output displayed is as follows-
Go to browser and type the following url-
http://localhost:15672/#/queues
the output would be as follows- The message declared as deadLetter in the rabbitmqconsole