Spring AMQP RabbitMQ Object sent as one type gets converted to Map in Listener - spring-boot

In my application, the RabbitTemplate sends an object (EventMessage - code below) to the queue. However in the RabbitListener and RabbitHandler the EmailMessage object that EventMessage contains gets converted as LinkedHashmap during deserialization
However the MessageProperties shows that the type is EventMessage
2020-09-23 18:39:47.712 WARN 16676 --- [ntContainer#0-1] o.s.a.r.r.RejectAndDontRequeueRecoverer : Retries exhausted for message (Body:'{"type":101,"params":{"emailMessage":{"hasTo":true,"hasCc":false,"hasBcc":false,"template":null,"templateParams":null,"html":false,"toAddresses":["test#test.com"],"ccAddresses":null,"bccAddresses":null,"fromAddress":"test#mhserver.com","subject":"Test Subject","message":"An email has been sent to your registered email to reset your password","isHtml":false}}}' MessageProperties [headers={__TypeId__=in.teamnexus.mq.EventMessage}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=eventExchange, receivedRoutingKey=route.key.email, deliveryTag=1, consumerTag=amq.ctag-eCfxhUrcjiCF4lA7x8ZLNg, consumerQueue=queue.email])
Caused by: java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class in.teamnexus.email.EmailMessage (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; in.teamnexus.email.EmailMessage is in unnamed module of loader org.springframework.boot.devtools.restart.classloader.RestartClassLoader #ac49aa4)
at in.teamnexus.mq.EmailQueueListener.handleEvent(EmailQueueListener.java:34) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
Following is the RabbitConfig
#Configuration
#EnableRabbit
#PropertySource("classpath:custom.properties")
public class RabbitConfig
{
#Bean
public ConnectionFactory rabbitConnectionFactory()
{
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(rabbitMQhost);
connectionFactory.setUsername(rabbitMQUsername);
connectionFactory.setPassword(rabbitMQPassword);
connectionFactory.setVirtualHost(rabbitMQVirtualHost);
connectionFactory.setPort(rabbitMQPort);
return connectionFactory;
}
#Bean
public MessageConverter messageConverter()
{
Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter(new ObjectMapper());
return messageConverter;
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory()
{
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setMessageConverter(messageConverter());
factory.setConnectionFactory(rabbitConnectionFactory());
factory.setConcurrentConsumers(20);
factory.setPrefetchCount(1);
factory.setMaxConcurrentConsumers(100);
factory.setAdviceChain(retryInterceptor());
return factory;
}
#Bean
public RetryOperationsInterceptor retryInterceptor()
{
return RetryInterceptorBuilder.stateless().maxAttempts(5).backOffOptions(1000, 2.0, 10000)
.recoverer(new RejectAndDontRequeueRecoverer()).build();
}
#Bean
public Queue mailQueue()
{
return new Queue("queue.email", true);
}
#Bean
public RabbitTemplate rabbitTemplate()
{
RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory());
rabbitTemplate.setMessageConverter(messageConverter());
return rabbitTemplate;
}
#Bean
public AmqpAdmin amqpAdmin()
{
RabbitAdmin rabbitAdmin = new RabbitAdmin(rabbitConnectionFactory());
return rabbitAdmin;
}
#Bean
public Exchange exchange()
{
AmqpAdmin rabbitAdmin = amqpAdmin();
DirectExchange dirExchange = new DirectExchange("eventExchange", true, false);
rabbitAdmin.declareExchange(dirExchange);
rabbitAdmin.declareQueue(mailQueue());
Binding emailBinding = BindingBuilder.bind(mailQueue()).to(dirExchange).with(Constants.ROUTE_KEY_EMAIL);
rabbitAdmin.declareBinding(emailBinding);
rabbitAdmin.declareBinding(retryBinding);
return dirExchange;
}
#Bean
Publisher publisher()
{
PublisherImpl publisher = new PublisherImpl();
return publisher;
}
#Bean
EmailQueueListener emailQueueListener()
{
return new EmailQueueListener();
}
}
The EventMessage class
public class EventMessage<T> implements Serializable
{
public static final int TYPE_EMAIL = 101;
/**
*
*/
private static final long serialVersionUID = 1846120191276045453L;
#JsonProperty("type")
private int type;
#JsonProperty("params")
private Map<String, T> params;
public EventMessage()
{
}
public EventMessage(int type, Map<String, T> params)
{
this.type = type;
this.params = params;
}
// Getters Setters...
}
The PublisherImpl class
public class PublisherImpl implements Publisher
{
private Logger logger = LoggerFactory.getLogger(getClass());
#Autowired
private RabbitTemplate rabbitTemplate;
#Override
public <T> EventResponse publishMessage(EventMessage<T> message, boolean async)
{
EventResponse response = new EventResponse();
if (async)
{
int resp = doPublish(message);
if (resp == EventResponse.EVENT_SUCCESS)
{
response.setStatus(EventResponse.EVENT_SUCCESS);
response.setMessage("Event Successfully published to the queue");
}
else
{
response.setStatus(EventResponse.EVENT_FAILURE);
response.setMessage("Failed to publish Event to the queue");
}
}
else
{
doSyncOp(message)
}
return response;
}
private <T> int doPublish(EventMessage<T> message)
{
String routingKey = Constants.ROUTE_KEY_EVENT;
int retVal = EventResponse.EVENT_SUCCESS;
try
{
switch (message.getType())
{
case EventMessage.TYPE_EMAIL:
{
routingKey = Constants.ROUTE_KEY_EMAIL;
this.rabbitTemplate.convertAndSend("eventExchange", routingKey, message);
break;
}
}
}
catch (AmqpException e)
{
retVal = EventResponse.EVENT_FAILURE;
logger.debug("Unable to push to the queue", e);
}
return retVal;
}
// Getters/Setters
}
The EmailQueueListener class
#RabbitListener(containerFactory = "rabbitListenerContainerFactory", queues = "queue.email")
public class EmailQueueListener
{
Logger logger = LoggerFactory.getLogger(getClass());
#Autowired
private EmailSender emailSender;
#RabbitHandler
void handleEvent(EventMessage<EmailMessage> message)
{
logger.debug("### RabbitHandler Email: Receiving in listener:" + message);
Map<String, EmailMessage> params = message.getParams();
logger.debug("### emailMessage: " + params.get("emailMessage") + " class:" + params.get("emailMessage").getClass());
EmailMessage email = (EmailMessage) params.get("emailMessage");
emailSender.sendEmail(email);
}
// Getters/Setters
}
At the line where params.get("emailMessage") is called is where I get the exception and the configured amount of retries. I am not sure if I am doing something wrong.
EDIT
Here is the code that publishes the message
public class EmailHelper
{
#Autowired
private Publisher publisher;
public void sendEmail(String to, String cc, String bcc, String subject, String text, boolean isHtml)
{
EmailMessage emailMessage = new EmailMessage();
emailMessage.setToAddresses(new String[] { to });
if (cc != null && !cc.isEmpty())
{
emailMessage.setCcAddresses(new String[] { cc });
}
if (bcc != null && !bcc.isEmpty())
{
emailMessage.setBccAddresses(new String[] { bcc });
}
emailMessage.setFromAddress("test#mhserver.com");
emailMessage.setSubject(subject);
emailMessage.setMessage(text);
Map<String, EmailMessage> params = new HashMap<>();
params.put("emailMessage", emailMessage);
EventMessage<EmailMessage> evtMsg = new EventMessage<>(EventMessage.TYPE_EMAIL, params);
publisher.<EmailMessage>publishMessage(evtMsg, true);
}

It's because EventMessage is a generic type; the default type mapper in the message converter can't handle arbitrary generic types.
It will work if the #RabbitListener is defined at the method level instead of the class level because we can infer the generic type from the listener method parameter.
Otherwise, you will need to create a custom type mapper for the message converter.

Related

How to use error-channel for catching exception in Spring Integration?

What I am trying to do? : I am new to Spring Integration and already have read many similar questions regarding error handling but I don't understand how to catch exceptions using error-channel?
What I have done so far:
#EnableIntegration
#IntegrationComponentScan
#Configuration
public class TcpClientConfig implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
private final ConnectionProperty connectionProperty;
#Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
TcpClientConfig(ConnectionProperty connectionProperty) {
this.connectionProperty = connectionProperty;
}
#Bean
public AbstractClientConnectionFactory clientConnectionFactory() {
TcpNioClientConnectionFactory tcpNioClientConnectionFactory =
getTcpNioClientConnectionFactoryOf(
connectionProperty.getPrimaryHSMServerIpAddress(),
connectionProperty.getPrimaryHSMServerPort());
final List<AbstractClientConnectionFactory> fallBackConnections = getFallBackConnections();
fallBackConnections.add(tcpNioClientConnectionFactory);
final FailoverClientConnectionFactory failoverClientConnectionFactory =
new FailoverClientConnectionFactory(fallBackConnections);
return new CachingClientConnectionFactory(
failoverClientConnectionFactory, connectionProperty.getConnectionPoolSize());
}
#Bean
DefaultTcpNioSSLConnectionSupport connectionSupport() {
final DefaultTcpSSLContextSupport defaultTcpSSLContextSupport =
new DefaultTcpSSLContextSupport(
connectionProperty.getKeystorePath(),
connectionProperty.getTrustStorePath(),
connectionProperty.getKeystorePassword(),
connectionProperty.getTruststorePassword());
final String protocol = "TLSv1.2";
defaultTcpSSLContextSupport.setProtocol(protocol);
return new DefaultTcpNioSSLConnectionSupport(defaultTcpSSLContextSupport, false);
}
#Bean
public MessageChannel outboundChannel() {
return new DirectChannel();
}
#Bean
#ServiceActivator(inputChannel = "outboundChannel")
public MessageHandler outboundGateway(AbstractClientConnectionFactory clientConnectionFactory) {
TcpOutboundGateway tcpOutboundGateway = new TcpOutboundGateway();
tcpOutboundGateway.setConnectionFactory(clientConnectionFactory);
return tcpOutboundGateway;
}
#Bean
#ServiceActivator(inputChannel = "error-channel")
public void handleError(ErrorMessage em) {
throw new RuntimeException(String.valueOf(em));
}
private List<AbstractClientConnectionFactory> getFallBackConnections() {
final int size = connectionProperty.getAdditionalHSMServersConfig().size();
List<AbstractClientConnectionFactory> collector = new ArrayList<>(size);
for (final Map.Entry<String, Integer> server :
connectionProperty.getAdditionalHSMServersConfig().entrySet()) {
collector.add(getTcpNioClientConnectionFactoryOf(server.getKey(), server.getValue()));
}
return collector;
}
private TcpNioClientConnectionFactory getTcpNioClientConnectionFactoryOf(
final String ipAddress, final int port) {
TcpNioClientConnectionFactory tcpNioClientConnectionFactory =
new TcpNioClientConnectionFactory(ipAddress, port);
tcpNioClientConnectionFactory.setUsingDirectBuffers(true);
tcpNioClientConnectionFactory.setDeserializer(new CustomDeserializer());
tcpNioClientConnectionFactory.setApplicationEventPublisher(applicationEventPublisher);
tcpNioClientConnectionFactory.setSoKeepAlive(true);
tcpNioClientConnectionFactory.setConnectTimeout(connectionProperty.getConnectionTimeout());
tcpNioClientConnectionFactory.setSoTcpNoDelay(true);
tcpNioClientConnectionFactory.setTcpNioConnectionSupport(connectionSupport());
return tcpNioClientConnectionFactory;
}
}
Gateway
#Component
#MessagingGateway(defaultRequestChannel = "outboundChannel",errorChannel ="error-channel" )
public interface TcpClientGateway {
String send(String message);
}
Also currently, I am facing
required a bean of type org.springframework.messaging.support.ErrorMessage that could not be found
I need some assistance!
Thanking you in advance,
EDIT
#AllArgsConstructor
#Service
public class AsyncNonBlockingClient implements Connector {
TcpClientGateway tcpClientGateway;
#Override
public String send(final String payload) {
return tcpClientGateway.send(payload);
}
}
See documentation about messaging annotation:
Your problem is here: https://docs.spring.io/spring-integration/docs/current/reference/html/configuration.html#annotations_on_beans
#Bean
#ServiceActivator(inputChannel = "error-channel")
public void handleError(ErrorMessage em) {
This is a plain POJO method, therefore it cannot be marked with a #Bean. You use a #Bean really for beans to expose. Then you decide if that has to be a #ServiceActivator or not. So, just remove #Bean from this method and your error-channel consumer should be OK.

Listener not getting message in REDIS PubS/ub with Spring Boot

I am relatively new to Redis Pub/Sub. I have integrated this recently in my Spring Boot application.
Redis Pub/Sub configuration is as follows:
#Configuration
public class RedisPubSubConfiguration {
#Bean
public RedisMessageListenerContainer messageListenerContainer(RedisConnectionFactory
connectionFactory,
#Qualifier("topicAdapterPair")
List<Pair
<Topic,
MessageListenerAdapter>>
channelAdaperPairList) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
for (Pair<Topic, MessageListenerAdapter> chanelAdapterPair : channelAdaperPairList) {
container.addMessageListener(chanelAdapterPair.getValue(),
chanelAdapterPair.getKey());
}
container.setConnectionFactory(connectionFactory);
return container;
}
#Bean("msg-listener-adptr-1")
public MessageListenerAdapter messageListnerAdapter1(
#Qualifier("message-listener-1")
MessageListener listener) {
return new MessageListenerAdapter(listener, REDIS_RECEIVER_METHOD_NAME);
}
#Bean("message-listener-1")
public MessageListener messageListener1(ManagerProxy managerProxy) {
return new MessageListener1(managerProxy);
}
#Bean("message-sender-1")
public MessageSender messageSender1(RedisTemplate redisTemplate,
#Value("${chnlTopicName1}")
String channelTopicName) {
return new MessageSender1(redisTemplate, channelTopicName);
}
#Bean
#Qualifier("topicAdapterPair")
public Pair<Topic, MessageListenerAdapter> getTopicListenerAdapterpair1(
#Value("${chnlTopicName1}") String channelTopicName,
#Qualifier("msg-listener-adptr-1")
MessageListenerAdapter messageListenerAdapter) {
return Pair.of(new ChannelTopic(channelTopicName), messageListenerAdapter);
}
#Bean("msg-listener-adptr-2")
public MessageListenerAdapter messageListnerAdapter2(
#Qualifier("message-listener-2")
MessageListener listener) {
return new MessageListenerAdapter(listener, REDIS_RECEIVER_METHOD_NAME);
}
#Bean("message-listener-2")
public MessageListener messageListener2(NotificationServiceImpl notificationService) {
return new MessageListener2(notificationService);
}
#Bean("message-sender-2")
public MessageSender messageSender2(RedisTemplate redisTemplate,
#Value("${chnlTopicName2}")
String channelTopicName) {
return new MessageSender2(redisTemplate, channelTopicName);
}
#Bean
#Qualifier("topicAdapterPair")
public Pair<Topic, MessageListenerAdapter> getTopicListenerAdapterPair2(
#Value("${chnlTopicName2}") String channelTopicName,
#Qualifier("msg-listener-adptr-2")
MessageListenerAdapter messageListenerAdapter) {
return Pair.of(new ChannelTopic(channelTopicName), messageListenerAdapter);
}
}
MessageSender2 is as follows:
public class MessageSender2 implements MessageSender<MyDTO> {
private final RedisTemplate<String, Object> redisTemplate;
private final String chanelName;
public MessageSender2(
RedisTemplate<String, Object> redisTemplate,
String chanelName) {
this.redisTemplate = redisTemplate;
this.chanelName = chanelName;
}
#Override
public void send(MyDTO myDTO) {
redisTemplate.convertAndSend(chanelName, myDTO);
}
}
MessageListener2 is as follows:
public class MessageListener2 implements MessageListener<EventDTO> {
private static final Logger LOGGER = LoggerFactory
.getLogger(MessageListener2.class);
private final NotificationService notificationService;
public MessageListener1(NotificationServiceImpl notificationService) {
this.notificationService = notificationService;
}
#Override
public void receiveMessage(MyDTO message) {
LOGGER.info("Received message : {} ", message); <--HERE MESSAGE IS NOT COMING EVEN AFTER PUBLISHING MESSAGE TO THE ASSOCIATED TOPIC FROM PUBLISHER
Type type = message.getType();
...
}
}
MessageSender1 is as follows:
public class MessageSender1 implements MessageSender<String> {
private final RedisTemplate<String, Object> redisTemplate;
private final String chanelName;
public MessageSender1(
RedisTemplate<String, Object> redisTemplate,
String chanelName) {
this.redisTemplate = redisTemplate;
this.chanelName = chanelName;
}
#Override
public void send(String message) {
redisTemplate.convertAndSend(chanelName, message);
}
}
Associated listener is follows:
public class MessageListener1 implements MessageListener<String> {
private static final Logger LOGGER = LoggerFactory
.getLogger(MessageListener1.class);
private final ManagerProxy managerProxy;
public MessageListener1(ManagerProxy managerProxy) {
this.managerProxy = managerProxy;
}
public void receiveMessage(String message) {
LOGGER.info("Received message : {} ", message);
managerProxy.refresh();
}
}
Here though MessageSender1 and associated message listener are working fine, I don't understand what I did with MessageSender2 and associated listener, because of which I am not able to receive message in the listener.

Spring-boot 2 rabbitmq MessageConverter not working as excepted

I am using the springboot and rabitmq. I have kind of similar scenario here.
I would like to map my message to custom java object
I also want pass delivery tag which is message proprieties
I wann to pass Channel as well beacuse I need to manual ack messages
My code is like this
#RunWith(SpringRunner.class)
public class EPPQ2SubscriberTest {
#Autowired
private RabbitListenerEndpointRegistry registry;
public final String sampleMessage = "{" + "\"header\": {" + "\"RETRY_COUNT\":0," + "\"PUBLISH_EVENT_TYPE\":\"AUTH\""
+ "}," + "\"payLoad\":{" + "\"MTI\": \"120\"," + "\"MTI_REQUEST\": \"120\","
+ "\"PAN\": \"6011000000000000\"" + "}" + "}";
#Test
public void message_converter_test() throws Exception {
SimpleMessageListenerContainer container = (SimpleMessageListenerContainer) this.registry
.getListenerContainer("messageListener");
ChannelAwareMessageListener listener = (ChannelAwareMessageListener) container.getMessageListener();
Message message = MessageBuilder.withBody(sampleMessage.getBytes())
.andProperties(MessagePropertiesBuilder.newInstance().setContentType("application/json").build())
.build();
listener.onMessage(message, mock(Channel.class));
}
#Configuration
#EnableRabbit
public static class config {
#Bean
public ConnectionFactory mockConnectionFactory() {
return mock(ConnectionFactory.class);
}
#Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
/*
* DefaultClassMapper classMapper = new DefaultClassMapper();
* classMapper.setDefaultType(com.discover.dftp.scrubber.domain.Message.class);
* messageConverter.setClassMapper(classMapper);
*/
return messageConverter;
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(mockConnectionFactory());
factory.setMessageConverter(messageConverter());
factory.setAutoStartup(false);
return factory;
}
#Bean
public EPPQ2Subscriber messageListener() {
return new EPPQ2Subscriber();
}
}
}
#Component
public class EPPQ2Subscriber {
private static final Logger LOGGER = LoggerFactory.getLogger(EPPQ2Subscriber.class);
// #RabbitListener(queues = "#{queue.getName()}") #TODO I wann to use this in
// later point in time.. !
#RabbitListener(id = "messageListener", queues = "TestQueue")
public void receiveMessage(Message message, Channel channel/* ,#Header(AmqpHeaders.DELIVERY_TAG) long tag */) {
LOGGER.info("Method receiveMessage invoked");
message.getMessageProperties().getDeliveryTag();
LOGGER.info("Result:" + message.getClass() + ":" + message.toString());
}
}
public class Message implements Serializable {
private static final long serialVersionUID = 1L;
private Map<String, Object> header;
private Map<String, Object> payLoad;
public Map<String, Object> getHeader() {
return header;
}
public void setHeader(Map<String, Object> header) {
this.header = header;
}
public Map<String, Object> getPayLoad() {
return payLoad;
}
public void setPayLoad(Map<String, Object> payLoad) {
this.payLoad = payLoad;
}
#Override
public String toString() {
return "Header [header=" + this.header + ", payLoad=" + this.payLoad + "]";
}
}
#RabbitListener(id = "messageListener", queues = "TestQueue")
public void receiveMessage(Message message, Channel channel/* ,#Header(AmqpHeaders.DELIVERY_TAG) long tag */) {
LOGGER.info("Method receiveMessage invoked");
message.getMessageProperties().getDeliveryTag();
LOGGER.info("Result:" + message.getClass() + ":" + message.toString());
}
I looks like that method receives the raw (unconverted) Spring AMQP Message (you are importing the wrong Message class in EPPQ2Subscriber).

Spring boot JMS using different messages class

I'm using spring boot.
I want to use different models at both sender and receiver so that they don't depend to the same model (receiver doesn't need to add model of sender to classpath).
1) Should I do that?
2) And how can I do that?
Sender:
AccountEvent accountEvent = new com.dspc.account.domain.dto.AccountEvent(createdAccount.getId(), EventType.CREATED);
jmsMessagingTemplate.convertAndSend(new ActiveMQTopic("VirtualTopic.ACCOUNT-EVENT-TOPIC"), accountEvent);
Receiver:
#JmsListener(destination = "Consumer.AgentGenerator.VirtualTopic.ACCOUNT-EVENT-TOPIC")
public void receive(com.dspc.devicemgmt.domain.dto.AccountEvent accountEvent) {
System.out.println(accountEvent);
}
JMS config of both sender and receiver:
#Bean // Serialize message content to json using TextMessage
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
Get exception when receiving message:
[com.dspc.account.domain.dto.AccountEvent]; nested exception is
java.lang.ClassNotFoundException:
com.dspc.account.domain.dto.AccountEvent
Note that there are 2 different packages:
- com.dspc.account.domain.dto.AccountEvent
- com.dspc.devicemgmt.domain.dto.AccountEvent
I'm thinking about creating a common message model. How do you think?
public class DspcCommonMessage {
private Map<String, String> properties;
private Optional<byte[]> payLoad = Optional.empty();
public Map<String, String> getProperties() {
return properties;
}
public void setProperties(Map<String, String> properties) {
this.properties = properties;
}
public Optional<byte[]> getPayLoad() {
return payLoad;
}
public void setPayLoad(Optional<byte[]> payLoad) {
this.payLoad = payLoad;
}
}
Sender and receiver:
public void publishMessage(com.dspc.account.domain.dto.AccountEvent accountEvent) {
ObjectMapper objectMapper = new ObjectMapper();
String messageAsString = objectMapper.writeValueAsString(accountEvent);
DspcCommonMessage dspcMessage = new DspcCommonMessage();
dspcMessage.setPayLoad(Optional.of(messageAsString.getBytes()));
jmsMessagingTemplate.convertAndSend(new ActiveMQTopic("VirtualTopic.ACCOUNT-EVENT-TOPIC"), dspcMessage);
}
#JmsListener(destination = "Consumer.AgentGenerator.VirtualTopic.ACCOUNT-EVENT-TOPIC")
public void receive(com.dspc.common.domain.DspcCommonMessage dspcCommonMessage) {
String jsonBody = new String(dspcCommonMessage.getPayload());
ObjectMapper objectMapper = new ObjectMapper();
com.dspc.devicemgmt.domain.dto.AccountEvent accountEvent = objectMapper.readValue(jsonBody,
com.dspc.devicemgmt.domain.dto.AccountEvent accountEvent.class);
System.out.println(accountEvent);
}

Spring-boot-starter RabbitMQ global error handling

I am using spring-boot-starter-amqp 1.4.2.Producer and consumer working fine but sometimes the incoming JSON messages have an incorrect syntax. This results in the following (correct) exception:
org.springframework.amqp.rabbit.listener.ListenerExecutionFailedException: Listener threw exception
Caused by: org.springframework.amqp.support.converter.MessageConversionException: Failed to convert Message content
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_ARRAY token...
In future i may face lot more exceptions. So i want to configure a global error handler so that if there is any exception in any one the consumer i can handle it globally.
Note : In this case message is not at all reached consumer. I want to handle these kind of exceptions globally across the consumer.
Please find the below code :
RabbitConfiguration.java
#Configuration
#EnableRabbit
public class RabbitMqConfiguration {
#Autowired
private CachingConnectionFactory cachingConnectionFactory;
#Bean
public MessageConverter jsonMessageConverter()
{
return new Jackson2JsonMessageConverter();
}
#Bean
#Primary
public RabbitTemplate rabbitTemplate()
{
RabbitTemplate template = new RabbitTemplate(cachingConnectionFactory);
template.setMessageConverter(jsonMessageConverter());
return template;
}
}
Consumer
#RabbitListener(
id = "book_queue",
bindings = #QueueBinding(
value = #Queue(value = "book.queue", durable = "true"),
exchange = #Exchange(value = "book.exchange", durable = "true", delayed = "true"),
key = "book.queue"
)
)
public void handle(Message message) {
//Business Logic
}
Could anyone please assist me to handle the error handler globally.Your help should be appreciable.
Updated question as per Gary comment
I can able to run your example and getting the expected output as you said, I just want to try few more negative cases based on your example, but i couldn't understand few things,
this.template.convertAndSend(queue().getName(), new Foo("bar"));
output
Received: Foo [foo=bar]
The above code is working fine.Now instead of "Foo" i am sending some other bean
this.template.convertAndSend(queue().getName(), new Differ("snack","Hihi","how are you"));
output
Received: Foo [foo=null]
The consumer shouldn't accept this message because it is completely a different bean(Differ.class not Foo.class) so i am expecting it should go to "ConditionalRejectingErrorHandler".Why it is accepting wrong payload and printing as null ? Please correct me if i am wrong.
Edit 1 :
Gary, As you said i have set the header "TypeId" while sending the message but still consumer can able to convert wrong messages and it is not throwing any error...please find the code below, I have used your code samples and just did the following modifications,
1) Added "__TypeId__" while sending the message,
this.template.convertAndSend(queue().getName(), new Differ("snack","hihi","how are you"),m -> {
m.getMessageProperties().setHeader("__TypeId__","foo");
return m;
});
2) Added "DefaultClassMapper" in the "Jackson2JsonMessageConverter"
#Bean
public MessageConverter jsonConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
DefaultClassMapper mapper = new DefaultClassMapper();
mapper.setDefaultType(Foo.class);
converter.setClassMapper(mapper);
return new Jackson2JsonMessageConverter();
}
Override Boot's listener container factory bean, as described in Enable Listener Endpoint Annotations.
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setErrorHandler(myErrorHandler());
...
return factory;
}
You can inject a custom implementation of ErrorHandler which will be added to each listener container the factory creates.
void handleError(Throwable t);
The throwable will be a ListenerExecutionFailedException which, starting with version 1.6.7 (boot 1.4.4), has the raw inbound message in its failedMessage property.
The default error handler considers causes such as MessageConversionException to be fatal (they will not be requeued).
If you wish to retain that behavior (normal for such problems), you should throw an AmqpRejectAndDontRequeueException after handling the error.
By the way, you don't need that RabbitTemplate bean; if you have just one MessageConverter bean in the application, boot will auto-wire it into the containers and template.
Since you will be overriding boot's factory, you will have to wire in the converter there.
EDIT
You could use the default ConditionalRejectingErrorHandler, but inject it with a custom implementation of FatalExceptionStrategy. In fact, you could subclass its DefaultExceptionStrategy and override isFatal(Throwable t), then, after handing the error, return super.isFatal(t).
EDIT2
Full example; sends 1 good message and 1 bad one:
package com.example;
import org.slf4j.Logger;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler;
import org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.util.ErrorHandler;
#SpringBootApplication
public class So42215050Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(So42215050Application.class, args);
context.getBean(So42215050Application.class).runDemo();
context.close();
}
#Autowired
private RabbitTemplate template;
private void runDemo() throws Exception {
this.template.convertAndSend(queue().getName(), new Foo("bar"));
this.template.convertAndSend(queue().getName(), new Foo("bar"), m -> {
return new Message("some bad json".getBytes(), m.getMessageProperties());
});
Thread.sleep(5000);
}
#RabbitListener(queues = "So42215050")
public void handle(Foo in) {
System.out.println("Received: " + in);
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jsonConverter());
factory.setErrorHandler(errorHandler());
return factory;
}
#Bean
public ErrorHandler errorHandler() {
return new ConditionalRejectingErrorHandler(new MyFatalExceptionStrategy());
}
#Bean
public Queue queue() {
return new Queue("So42215050", false, false, true);
}
#Bean
public MessageConverter jsonConverter() {
return new Jackson2JsonMessageConverter();
}
public static class MyFatalExceptionStrategy extends ConditionalRejectingErrorHandler.DefaultExceptionStrategy {
private final Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
#Override
public boolean isFatal(Throwable t) {
if (t instanceof ListenerExecutionFailedException) {
ListenerExecutionFailedException lefe = (ListenerExecutionFailedException) t;
logger.error("Failed to process inbound message from queue "
+ lefe.getFailedMessage().getMessageProperties().getConsumerQueue()
+ "; failed message: " + lefe.getFailedMessage(), t);
}
return super.isFatal(t);
}
}
public static class Foo {
private String foo;
public Foo() {
super();
}
public Foo(String foo) {
this.foo = foo;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
#Override
public String toString() {
return "Foo [foo=" + this.foo + "]";
}
}
}
Result:
Received: Foo [foo=bar]
2017-02-14 09:42:50.972 ERROR 44868 --- [cTaskExecutor-1] 5050Application$MyFatalExceptionStrategy : Failed to process inbound message from queue So42215050; failed message: (Body:'some bad json' MessageProperties [headers={TypeId=com.example.So42215050Application$Foo}, timestamp=null, messageId=null, userId=null, receivedUserId=null, appId=null, clusterId=null, type=null, correlationId=null, correlationIdString=null, replyTo=null, contentType=application/json, contentEncoding=UTF-8, contentLength=0, deliveryMode=null, receivedDeliveryMode=PERSISTENT, expiration=null, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=So42215050, receivedDelay=null, deliveryTag=2, messageCount=0, consumerTag=amq.ctag-P2QqY0PMD1ppX5NnkUPhFA, consumerQueue=So42215050])
EDIT3
JSON does not convey any type information. By default, the type to convert to will be inferred from the method parameter type. If you wish to reject anything that can't be converted to that type, you need to configure the message converter appropriately.
For example:
#Bean
public MessageConverter jsonConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
DefaultClassMapper mapper = new DefaultClassMapper();
mapper.setDefaultType(Foo.class);
converter.setClassMapper(mapper);
return converter;
}
Now, when I change my example to send a Bar instead of a Foo...
public static class Bar {
...
}
and
this.template.convertAndSend(queue().getName(), new Bar("baz"));
I get...
Caused by: org.springframework.amqp.support.converter.MessageConversionException: Cannot handle message
... 13 common frames omitted
Caused by: org.springframework.messaging.converter.MessageConversionException: Cannot convert from [com.example.So42215050Application$Bar] to [com.example.So42215050Application$Foo] for GenericMessage [payload=Bar [foo=baz], headers={amqp_receivedDeliveryMode=PERSISTENT, amqp_receivedRoutingKey=So42215050, amqp_contentEncoding=UTF-8, amqp_deliveryTag=3, amqp_consumerQueue=So42215050, amqp_redelivered=false, id=6d7e23a3-c2a7-2417-49c9-69e3335aa485, amqp_consumerTag=amq.ctag-6JIGkpmkrTKaG32KVpf8HQ, contentType=application/json, __TypeId__=com.example.So42215050Application$Bar, timestamp=1488489538017}]
But this only works if the sender sets the __TypeId__ header (which the template does if it's configured with the same adapter).
EDIT4
#SpringBootApplication
public class So42215050Application {
private final Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(So42215050Application.class, args);
context.getBean(So42215050Application.class).runDemo();
context.close();
}
#Autowired
private RabbitTemplate template;
private void runDemo() throws Exception {
this.template.convertAndSend(queue().getName(), new Foo("bar")); // good - converter sets up type
this.template.convertAndSend(queue().getName(), new Foo("bar"), m -> {
return new Message("some bad json".getBytes(), m.getMessageProperties()); // fail bad json
});
Message message = MessageBuilder
.withBody("{\"foo\":\"bar\"}".getBytes())
.andProperties(
MessagePropertiesBuilder
.newInstance()
.setContentType("application/json")
.build())
.build();
this.template.send(queue().getName(), message); // Success - default Foo class when no header
message.getMessageProperties().setHeader("__TypeId__", "foo");
this.template.send(queue().getName(), message); // Success - foo is mapped to Foo
message.getMessageProperties().setHeader("__TypeId__", "bar");
this.template.send(queue().getName(), message); // fail - mapped to a Map
Thread.sleep(5000);
}
#RabbitListener(queues = "So42215050")
public void handle(Foo in) {
logger.info("Received: " + in);
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jsonConverter());
factory.setErrorHandler(errorHandler());
return factory;
}
#Bean
public ErrorHandler errorHandler() {
return new ConditionalRejectingErrorHandler(new MyFatalExceptionStrategy());
}
#Bean
public Queue queue() {
return new Queue("So42215050", false, false, true);
}
#Bean
public MessageConverter jsonConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
DefaultClassMapper mapper = new DefaultClassMapper();
mapper.setDefaultType(Foo.class);
Map<String, Class<?>> mappings = new HashMap<>();
mappings.put("foo", Foo.class);
mappings.put("bar", Object.class);
mapper.setIdClassMapping(mappings);
converter.setClassMapper(mapper);
return converter;
}
public static class MyFatalExceptionStrategy extends ConditionalRejectingErrorHandler.DefaultExceptionStrategy {
private final Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
#Override
public boolean isFatal(Throwable t) {
if (t instanceof ListenerExecutionFailedException) {
ListenerExecutionFailedException lefe = (ListenerExecutionFailedException) t;
logger.error("Failed to process inbound message from queue "
+ lefe.getFailedMessage().getMessageProperties().getConsumerQueue()
+ "; failed message: " + lefe.getFailedMessage(), t);
}
return super.isFatal(t);
}
}
public static class Foo {
private String foo;
public Foo() {
super();
}
public Foo(String foo) {
this.foo = foo;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
#Override
public String toString() {
return "Foo [foo=" + this.foo + "]";
}
}
public static class Bar {
private String foo;
public Bar() {
super();
}
public Bar(String foo) {
this.foo = foo;
}
public String getFoo() {
return this.foo;
}
public void setFoo(String foo) {
this.foo = foo;
}
#Override
public String toString() {
return "Bar [foo=" + this.foo + "]";
}
}
}

Resources