How can i intercept incomig messages before they reach methods annotated with #RabbitListener? - spring-boot

I Started by setting up an interceptor for outgoing messages which is working smoothly, but
when i try to intercept incomming messages in the consumers, the postProcessMessage method
is skipped and the message reaches the method annotated with #RabbitListener, bellow is my code for the whole proccess, i ommited unimportant code.
Producer
RabbitMQProducerInterceptor
#Component
#Slf4j
public class RabbitMQProducerInterceptor implements MessagePostProcessor {
#Override
public Message postProcessMessage(Message message) throws AmqpException {
log.info("Getting the current HttpServletRequest");
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
log.info("Extracting the X-REQUEST-ID from the header of the HttpServletRequest");
String XRequestId = req.getHeader(ShareableConstants.X_REQUEST_ID_HEADER);
log.info("Adding X-REQUEST-ID {} to the RabbitMQ Producer Header", XRequestId);
message.getMessageProperties().getHeaders().put(ShareableConstants.X_REQUEST_ID_HEADER, XRequestId);
return message;
}
}
RabbitMQProducerConfig
#Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setReplyTimeout(60000);
MessagePostProcessor[] processors = {new RabbitMQProducerInterceptor()};
rabbitTemplate.addBeforePublishPostProcessors(processors);
return rabbitTemplate;
}
Sending a message to the consumer
User Producer
public UserRegistrationResponseDTO register(UserRegistrationDTO userRegistrationDTO) {
log.info("Sending user registration request {}", userRegistrationDTO);
UserRegistrationDTO response = (UserRegistrationDTO) rabbitTemplate
.convertSendAndReceive(ShareableConstants.EXCHANGE_NAME,
ShareableConstants.CREATE_USER_ROUTING_KEY,
userRegistrationDTO);
return UserRegistrationResponseDTO.builder()
.username(response.getUsername())
.id(response.getId())
.createdAt(response.getCreatedAt()).build();
}
Consumer
RabbitMQConsumerConfig
#Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
MessagePostProcessor[] processors = {new RabbitMQConsumerInterceptor()};
rabbitTemplate.setAfterReceivePostProcessors(processors);
return rabbitTemplate;
}
RabbitMQConsumerInterceptor
#Component
public class RabbitMQConsumerInterceptor implements MessagePostProcessor {
#Override
public Message postProcessMessage(Message message) throws AmqpException {
String XRequestId = message.getMessageProperties().getHeader(ShareableConstants.X_REQUEST_ID_HEADER);
MDC.put(ShareableConstants.X_REQUEST_ID_HEADER, XRequestId);
return message;
}
}
User Consumer
#RabbitListener(bindings =
#QueueBinding(exchange = #Exchange(ShareableConstants.EXCHANGE_NAME),
key = ShareableConstants.CREATE_USER_ROUTING_KEY,
value = #Queue(ShareableConstants.USER_REGISTRATION_QUEUE_NAME)))
public UserRegistrationDTO receiveUser(UserRegistrationDTO userRegistrationDTO) {
log.info("receiving user {} to register ", userRegistrationDTO);
User user = Optional.of(userRegistrationDTO).map(User::new).get();
User createdUser = userService.register(user);
UserRegistrationDTO registrationDTO = UserRegistrationDTO.builder()
.id(createdUser.getId())
.username(createdUser.getUsername())
.createdAt(createdUser.getCreationDate())
.build();
return registrationDTO;
}
Here's the code, no exception is thrown, the only problem is the Interceptor being skipped

The RabbitTemplate is not used to receive messages for a #RabbitListener; messages are received by a listener container; you have to set the afterReceivePostProcessors on the listener container factory.
If you are using Spring Boot, just add the auto-configured SimpleRabbitListenerContainerFactory as a parameter to one of your other #Beans and set the MPP on it.

Related

RabbitMQ message not being sent to Dead Letter Queue on Exception

I have this RabbitMQ Spring Boot Configuration:
#Configuration
public class RabbitConfiguration {
// Main queue configuration
#Value("${rabbitmq.main.messages.queue}")
private String mainQueueName;
#Value("${rabbitmq.main.exchange.queue}")
private String mainExchangeName;
#Value("${rabbitmq.main.routing.key}")
private String mainRoutingKey;
// DLQ configuration
#Value("${rabbitmq.dlq.messages.queue}")
private String dlqQueueName;
#Value("${rabbitmq.dlq.exchange.queue}")
private String dlqExchangeName;
#Value("${rabbitmq.dlq.routing.key}")
private String dlqRoutingKey;
// Connectivity
#Value("${spring.rabbitmq.host}")
private String rabbitmqHost;
#Value("${spring.rabbitmq.port}")
private int rabbitmqPort;
#Value("${spring.rabbitmq.username}")
private String rabbitmqUsername;
#Value("${spring.rabbitmq.password}")
private String rabbitmqPassword;
// Not delivered messages, will be used eventually
#Value("${rabbitmq.not.delivered.messages.queue}")
private String notDeliveredMessagesQueue;
// status with delivered messages (callback default)
#Value("${rabbitmq.delivered.messages.queue}")
private String deliveredMessagesQueue;
#Bean
DirectExchange deadLetterExchange() {
return new DirectExchange(dlqExchangeName);
}
#Bean
DirectExchange exchange() {
return new DirectExchange(mainExchangeName);
}
#Bean
Queue dlq() {
return QueueBuilder.durable(dlqQueueName).build();
}
#Bean
Queue queue() {
return QueueBuilder
.durable(mainQueueName)
.withArgument("x-dead-letter-exchange", "deadLetterExchange")
.withArgument("x-dead-letter-routing-key", dlqQueueName).build();
}
#Bean
Binding deadLetterBinding() {
return BindingBuilder.bind(dlq()).to(deadLetterExchange()).with(dlqRoutingKey);
}
#Bean
Binding binding() {
return BindingBuilder.bind(queue()).to(exchange()).with(mainQueueName);
}
#Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitmqHost);
connectionFactory.setUsername(rabbitmqUsername);
connectionFactory.setPassword(rabbitmqPassword);
return connectionFactory;
}
#Bean
public RabbitAdmin rabbitAdmin() {
RabbitAdmin admin = new RabbitAdmin(connectionFactory());
admin.declareQueue(queue());
admin.declareExchange(exchange());
admin.declareBinding(binding());
return admin;
}
public AmqpTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jsonMessageConverter());
return rabbitTemplate;
}
}
The problem is that when some exception is launched the message is not sent to DLQ queue.
Consumer:
#RabbitListener(queues = { "${rabbitmq.main.messages.queue}" })
public void recievedMessage(#Payload Mensagem item, Channel channel, #Header(AmqpHeaders.DELIVERY_TAG) long tag) throws InvalidMessageException {
if (item.getIdCliente().equals("69")) {
logger.info("Something went wrong to: " + item);
throw new InvalidMessageException();
} else {
logger.info("==> Message consumed successfully: " + item);
}
}
This is the configuration I have on my application.properties:
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.initial-interval=3s
spring.rabbitmq.listener.simple.retry.max-attempts=2
spring.rabbitmq.listener.simple.retry.multiplier=2
spring.rabbitmq.listener.simple.retry.max-interval=10s
When I throw an exception on purpose just to see the message moving to DLQ nothing happens. What's wrong here? What am I forgetting here?
Try adding
spring.rabbitmq.listener.simple.default-requeue-rejected=false
to your application.properties. I think the problem is that the failed deliveries are being requeued instead of being sent to the DLQ.

_AMQ_GROUP_ID present in message but JMSXGroupID null in #JmsListener

From this documentation:
Messages in a message group share the same group id, i.e. they have same group identifier property (JMSXGroupID for JMS, _AMQ_GROUP_ID for Apache ActiveMQ Artemis Core API).
I can see why the property originally set via JMSXGroupID becomes _AMQ_GROUP_ID when I browse the messages in the broker with a value of product=paper. However, In my #JmsListener annotated method I can see the _AMQ_GROUP_ID property is missing and the JMSXGroupID is coming through as null in the Message's headers hashmap.
#JmsListener(destination = "${artemis.destination}", subscription = "${artemis.subscriptionName}",
containerFactory = "containerFactory", concurrency = "15-15")
public void consumeMessage(Message<StatefulSpineEvent<?>> eventMessage)
So
My Producer application sends the message to the queue after setting the string property JMSXGroupID to 'product=paper'
I can see _AMQ_GROUP_ID has a value of 'product=paper' when I browse that message's headers in the Artemis UI
When I debug my listener application and look at the map of headers, _AMQ_GROUP_ID is absent and JMSXGroupID has a value of null instead of 'product=paper'.
Is the character '=' invalid or is there something else that can cause this? I'm running out of things to try.
Edit, with new code:
HeaderMapper:
#Component
public class GroupIdMessageMapper extends SimpleJmsHeaderMapper {
#Override
public MessageHeaders toHeaders(Message jmsMessage) {
MessageHeaders messageHeaders = super.toHeaders(jmsMessage);
Map<String, Object> messageHeadersMap = new HashMap<>(messageHeaders);
try {
messageHeadersMap.put("JMSXGroupID", jmsMessage.getStringProperty("_AMQ_GROUP_ID"));
} catch (JMSException e) {
e.printStackTrace();
}
// can see while debugging that this returns the correct headers
return new MessageHeaders(messageHeadersMap);
}
}
Listener:
#Component
public class CustomSpringJmsListener {
protected final Logger LOG = LoggerFactory.getLogger(getClass());
#JmsListener(destination = "local-queue", subscription = "groupid-example",
containerFactory = "myContainerFactory", concurrency = "15-15")
public void receive(Message message) throws JMSException {
LOG.info("Received message: " + message);
}
}
Application code:
#SpringBootApplication
#EnableJms
public class GroupidApplication implements CommandLineRunner {
private static Logger LOG = LoggerFactory
.getLogger(GroupidApplication.class);
#Autowired
private JmsTemplate jmsTemplate;
#Autowired MessageConverter messageConverter;
public static void main(String[] args) {
LOG.info("STARTING THE APPLICATION");
SpringApplication.run(GroupidApplication.class, args);
LOG.info("APPLICATION FINISHED");
}
#Override
public void run(String... args) {
LOG.info("EXECUTING : command line runner");
jmsTemplate.setPubSubDomain(true);
createAndSendObjectMessage("Message1");
createAndSendTextMessage("Message2");
createAndSendTextMessage("Message3");
createAndSendTextMessage("Message4");
createAndSendTextMessage("Message5");
createAndSendTextMessage("Message6");
}
private void createAndSendTextMessage(String messageBody) {
jmsTemplate.send("local-queue", session -> {
Message message = session.createTextMessage(messageBody);
message.setStringProperty("JMSXGroupID", "product=paper");
return message;
});
}
// BEANS
#Bean
public JmsListenerContainerFactory<?> myContainerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
// This provides all boot's default to this factory, including the message converter
configurer.configure(factory, connectionFactory);
// You could still override some of Boot's default if necessary.
factory.setSubscriptionDurable(true);
factory.setSubscriptionShared(true);
factory.setMessageConverter(messagingMessageConverter());
return factory;
}
#Bean
public MessagingMessageConverter messagingMessageConverter() {
return new MessagingMessageConverter(messageConverter, new GroupIdMessageMapper());
}
}
Stack trace of where SimpleJmsHeaderMapper is being called:
toHeaders:130, SimpleJmsHeaderMapper (org.springframework.jms.support)
toHeaders:57, SimpleJmsHeaderMapper (org.springframework.jms.support)
extractHeaders:148, MessagingMessageConverter
(org.springframework.jms.support.converter) access$100:466,
AbstractAdaptableMessageListener$MessagingMessageConverterAdapter
(org.springframework.jms.listener.adapter) getHeaders:552,
AbstractAdaptableMessageListener$MessagingMessageConverterAdapter$LazyResolutionMessage
(org.springframework.jms.listener.adapter) resolveArgumentInternal:68,
HeaderMethodArgumentResolver
(org.springframework.messaging.handler.annotation.support)
resolveArgument:100, AbstractNamedValueMethodArgumentResolver
(org.springframework.messaging.handler.annotation.support)
resolveArgument:117, HandlerMethodArgumentResolverComposite
(org.springframework.messaging.handler.invocation)
getMethodArgumentValues:148, InvocableHandlerMethod
(org.springframework.messaging.handler.invocation) invoke:116,
InvocableHandlerMethod
(org.springframework.messaging.handler.invocation) invokeHandler:114,
MessagingMessageListenerAdapter
(org.springframework.jms.listener.adapter) onMessage:77,
MessagingMessageListenerAdapter
(org.springframework.jms.listener.adapter) doInvokeListener:736,
AbstractMessageListenerContainer (org.springframework.jms.listener)
invokeListener:696, AbstractMessageListenerContainer
(org.springframework.jms.listener) doExecuteListener:674,
AbstractMessageListenerContainer (org.springframework.jms.listener)
doReceiveAndExecute:318, AbstractPollingMessageListenerContainer
(org.springframework.jms.listener) receiveAndExecute:257,
AbstractPollingMessageListenerContainer
(org.springframework.jms.listener) invokeListener:1190,
DefaultMessageListenerContainer$AsyncMessageListenerInvoker
(org.springframework.jms.listener) executeOngoingLoop:1180,
DefaultMessageListenerContainer$AsyncMessageListenerInvoker
(org.springframework.jms.listener) run:1077,
DefaultMessageListenerContainer$AsyncMessageListenerInvoker
(org.springframework.jms.listener) run:748, Thread (java.lang)
Try subclassing the SimpleJmsHeaderMapper and override toHeaders(). Call super.toHeaders(), create a new Map<> from the result; put() any additional headers you want into the map and return a new MessageHeaders from the map.
Pass the custom mapper into a new MessagingMessageConverter and pass that into the container factory.
If you are using Spring Boot, simply add the converter as a #Bean and boot will auto wire it into the factory.
EDIT
After all this; I just wrote an app and it works just fine for me without any customization at all...
#SpringBootApplication
public class So58399905Application {
public static void main(String[] args) {
SpringApplication.run(So58399905Application.class, args);
}
#JmsListener(destination = "foo")
public void listen(String in, MessageHeaders headers) {
System.out.println(in + headers);
}
#Bean
public ApplicationRunner runner(JmsTemplate template) {
return args -> template.convertAndSend("foo", "bar", msg -> {
msg.setStringProperty("JMSXGroupID", "product=x");
return msg;
});
}
}
and
bar{jms_redelivered=false, JMSXGroupID=product=x, jms_deliveryMode=2, JMSXDeliveryCount=1, ...
EDIT2
It's a bug in the artemis client - with 2.6.4 (Boot 2.1.9) only getStringProperty() returns the value of the _AMQ_GROUP_ID property when getting JMSXGroupID.
The mapper uses getObjectProperty() which returned null. With the 2.10.1 client; the message properly returns the value of the _AMQ_GROUP_ID property from getObjectProperty().

How to handle JmsException /How do I set the redeliveryPolicy in ActiveMQ?

Let see one scenario is there when jms send message to consumer and we have to save that to db,but when db is down we cant able to save that to db due to db server is down.So how to acknowledge jms to send message again?
This issue raised by many people.I resolved this using below code snippet.
#Configuration
public class AppConfiguration {
#Bean
public JmsListenerContainerFactory<?> jmsContainerFactory(DefaultJmsListenerContainerFactoryConfigurer configurer) {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
connectionFactory.setClientID(clientId);
connectionFactory.setBrokerURL(brokerUrl);
CachingConnectionFactory cf = new CachingConnectionFactory(connectionFactory);
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(cf);
factory.setSubscriptionDurable(true);
configurer.configure(factory, cf);
return factory;
}
}
#Component("ApprovalSubsCriber")
public class ApprovalSubscriber implements SessionAwareMessageListener<TextMessage> {
#Override
#JmsListener(destination = "topic/jmsReplyTest1", containerFactory = "jmsContainerFactory", subscription = "ApprovalSubsCriber")
public void onMessage(TextMessage message, Session session) throws JMSException {
// This is the received message
System.out.println("Receive: " + message.getText());
// Let's prepare a reply message - a "ACK" String
ActiveMQTextMessage textMessage = new ActiveMQTextMessage();
textMessage.setText("ACK");
System.out.println(session.getAcknowledgeMode());
if ("notexception".equals(message.getText())) {
session.commit();
} else {
session.rollback();//If exception comes
}
}
}

Publisher Confirms - Rabbit mq

I want to implement publisher confirms functionality into my code, gone through the http://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/ but was unable to understand
I am using QueueBuilder as follows
#Bean
public Queue salesQueue() {
return QueueBuilder.durable(salesQueue)
.withArgument("x-max-length", 1)
.withArgument("x-overflow", "reject-publish")
.build();
}
RabbitTemplate creation
#Bean
public AmqpTemplate rabbitTemplate(ConnectionFactory connectionFactory)
{
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jsonMessageConverter());
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
System.out.println("Message with correlation ID {} confirmed by
broker."+ correlationData.getId());
} else {
System.out.println("Broker is unable to handle message with
correlation ID {} : {}"+correlationData.getId()+","+
cause);
}
});
return rabbitTemplate;
}
sending message to queue
#Service
public class RabbitMQSender {
private static long sentMessageCount =0L;
#Autowired
private AmqpTemplate rabbitTemplate;
#Value("${rabbitmq.queue}")
String queueName;
public void send() {
long x=++sentMessageCount;
rabbitTemplate.convertAndSend(queueName,"
{'empId':'"+x+"','empName':'YYYYYY'}");
}
Presently I have set the queue length as 1, whenever I send messages greater than 1, I am expecting it to inform publisher about rejection of message via a basic.nack message which is not happening...
I request you to help me with this
Thank you

How to set the replyTo with JmsTemplate?

I have a consumer consumes a message do some transformation and create a new Pojo and pass to a producer.
The producer sends the message in queue using the JmsTemplate.
The producer should set the headers of the original message like (JMSType, JMSCorrelationID, JMSExpiration, JMSDeliveryMode) to the new message to send.
But the producer should to change the replyTo Destination of the original message.
I haven't found a way to create a Destination and set to the JMSReplyTo.
Some have an idea how I can do that?
Maybe the JmsTemplate is not the correct class to do that.
public class Producer {
private final JmsTemplate jmsTemplate;
#Value("${jms-destination}")
private String destination;
public Producer(#Autowired JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void send(final MessageHeaders headers, final Pojo pojo) {
Validate.notNull(order);
jmsTemplate.convertAndSend(destination, pojo, (message) -> {
final Destination replyToDestination = ???;
message.setJMSReplyTo(replyToDestination);
message.setJMSType((String) headers.get(JmsHeaders.TYPE));
message.setJMSCorrelationID((String) headers.get(JmsHeaders.CORRELATION_ID));
message.setJMSExpiration((long) headers.get(JmsHeaders.EXPIRATION));
message.setJMSDeliveryMode((int) headers.get(JmsHeaders.DELIVERY_MODE));
return message;
});
}
}
I have found only this way to do, but I don't like and I don't sure that this introduce side effect:
public class Producer {
private final JmsTemplate jmsTemplate;
#Value("${jms-destination}")
private String destination;
#Value("${jms-replyTo}")
private String replyTo;
public Producer(#Autowired JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void send(final MessageHeaders headers, Pojo pojo) {
Validate.notNull(order);
jmsTemplate.convertAndSend(destination, order, (message) -> {
final Destination replyToDestination = buildReplyTo();
message.setJMSReplyTo(replyToDestination);
message.setJMSType((String) headers.get(JmsHeaders.TYPE));
message.setJMSCorrelationID((String) headers.get(JmsHeaders.CORRELATION_ID));
message.setJMSExpiration((long) headers.get(JmsHeaders.EXPIRATION));
message.setJMSDeliveryMode((int) headers.get(JmsHeaders.DELIVERY_MODE));
return message;
});
}
private Destination buildReplyTo() throws JMSException {
final Session session = jmsTemplate.getConnectionFactory().createConnection()
.createSession(false, Session.AUTO_ACKNOWLEDGE);
final Destination queue =
jmsTemplate.getDestinationResolver().resolveDestinationName(session, replyTo, false);
return queue;
}
}
Your solution creates side-along connections, which are not closed.
You should use existing session object and send your pojo manually through send API. Use getRequiredMessageConverter to convert your pojo.
public void send(final MessageHeaders headers, Pojo pojo) {
Validate.notNull(order);
final String responseQueue = "responseQ";
jmsTemplate.send(destination,
session -> {
Message message = getRequiredMessageConverter().toMessage(message, session);
message.setJMSReplyTo(session.createQueue(responseQueue)); //fill queue
//any other setters
return message;
});
}
// based on Spring JmsTemplate library
private MessageConverter getRequiredMessageConverter() throws IllegalStateException {
MessageConverter converter = jmsTemplate.getMessageConverter();
if (converter == null) {
throw new IllegalStateException("No 'messageConverter' specified. Check configuration of JmsTemplate.");
} else {
return converter;
}
}

Resources