Spring + AMQP serialise object message - spring

I have an application that publishes a message using Spring AMQP’s RabbitTemplate and subscribes to the message on a POJO using MessageListenerAdapter, pretty much as per the Getting Started - Messaging with RabbitMQ guide.
void handleMessage(final String message) {...}
rabbitTemplate.convertAndSend(EXCHANGE_NAME, QUEUE_NAME, "message");
However, this is sending messages as String's; surely there is a way to send and receive Object messages directly?
I have tried registering a JsonMessageConverter but to no avail.
Any help would be greatly appreciated - at present I'm manually de/serialising Strings on either side which seems messy and I'm surprised this isn't a supported feature.

I have tried registering a JsonMessageConverter but to no avail.
It would be better to see your attempt and figure out the issue on our side.
Right now I only can say that you should supply JsonMessageConverter for both sending and receiving parts.
I've just tested with the gs-messaging-rabbitmq:
#Autowired
RabbitTemplate rabbitTemplate;
#Autowired
MessageConverter messageConverter;
.....
#Bean
MessageListenerAdapter listenerAdapter(Receiver receiver, MessageConverter messageConverter) {
MessageListenerAdapter adapter = new MessageListenerAdapter(receiver, "receiveMessage");
adapter.setMessageConverter(messageConverter);
return adapter;
}
#Bean
MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
.....
System.out.println("Sending message...");
rabbitTemplate.setMessageConverter(messageConverter);
rabbitTemplate.convertAndSend(queueName, new Foo("Hello from RabbitMQ!"));
Where Receiver has been changed to this:
public void receiveMessage(Foo message) {
System.out.println("Received <" + message + ">");
latch.countDown();
}
So, the output is:
Waiting five seconds...
Sending message...
Received <Foo{foo='Hello from RabbitMQ!'}>
when we use Foo like this:
#Override
public String toString() {
return "Foo{" +
"foo='" + foo + '\'' +
'}';
}
with appropriate getter and setter.

Thanks #Artem for the hints. My code is pretty much as per the Getting Started Guide and I had already tried adding a Converter.
But as #Artem has pointed out, the trick is to register the converter with both the container, the listener adapter, and the rabbit template (which was auto-configured in the example).
So my #Configuration class now looks like so, in addition to whatever is mentioned in the Getting Started Guide:
#Bean
SimpleMessageListenerContainer container(final ConnectionFactory connectionFactory, final MessageListenerAdapter messageListenerAdapter,
final MessageConverter messageConverter)
{
final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(QUEUE_NAME);
container.setMessageListener(messageListenerAdapter);
container.setMessageConverter(messageConverter);
return container;
}
#Bean
RabbitTemplate rabbitTemplate(final ConnectionFactory connectionFactory, final MessageConverter messageConverter)
{
final RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter);
return rabbitTemplate;
}
#Bean
MessageConverter messageConverter()
{
return new Jackson2JsonMessageConverter();
}
#Bean
Receiver receiver()
{
return new Receiver();
}
#Bean
MessageListenerAdapter listenerAdapter(final Receiver receiver, final MessageConverter messageConverter)
{
return new MessageListenerAdapter(receiver, messageConverter);
}
which means the Receiver can have an Object method signature such as:
void handleMessage(final CbeEvent message)

Related

How can I configure RabbitMQ authentication mechanism in Spring Boot?

#Configuration
#EnableRabbit
public class RabbitConfiguration {
private static final String queueName = "3055";
private static final String topicExchangeName = queueName + "-exchange";
#Bean
Queue queue() {
return new Queue(queueName, false);
}
#Bean
TopicExchange exchange() {
return new TopicExchange(topicExchangeName);
}
#Bean
Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("foo.bar.#");
}
#Bean
RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
MessageConverter messageConverter) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter);
return rabbitTemplate;
}
#Bean
MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
Code above is my Spring Boot Project's RabbitMQ configuration class.
However, I cannot connect the RMQ server since below error pops up every time I try to connect.
Caused by: com.rabbitmq.client.AuthenticationFailureException: ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile.
The Server provider told me that I need to set the authentication mechanism to AMQPLAIN.
My question is that How can I set authentication mechanism to AMQPLAIN?
No matter how much I google, I couldn't figure out how.
I confirm to #Raja Anbazhagan. Check the RabbitMQ logs first. Supposedly your user credentials were guest/guest.
The easiest way to solve your problem could be to add those lines in your application.yml:
spring:
rabbitmq:
username: <user-name>
password: <user-password>

AWS SQS (queue) with Spring Boot - performance issues

I have a service that reads all messages from AWS SQS.
#Slf4j
#Configuration
#EnableJms
public class JmsConfig {
private SQSConnectionFactory connectionFactory;
public JmsConfig(
#Value("${amazon.sqs.accessKey}") String awsAccessKey,
#Value("${amazon.sqs.secretKey}") String awsSecretKey,
#Value("${amazon.sqs.region}") String awsRegion,
#Value("${amazon.sqs.endpoint}") String awsEndpoint) {
connectionFactory = new SQSConnectionFactory(
new ProviderConfiguration(),
AmazonSQSClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(
new BasicAWSCredentials(awsAccessKey, awsSecretKey)))
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(awsEndpoint, awsRegion))
.build());
}
#Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
DefaultJmsListenerContainerFactory factory =
new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(this.connectionFactory);
factory.setDestinationResolver(new DynamicDestinationResolver());
factory.setConcurrency("3-10");
factory.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
factory.setReceiveTimeout(2000L); //??????????
return factory;
}
#Bean
public JmsTemplate defaultJmsTemplate() {
return new JmsTemplate(this.connectionFactory);
}
I've heard about long polling so I wonder how I could use it in my case. I wonder how this listener works - I do not want to create unnecessary calls to the AWS SQS.
My listener that reads messages and converts them to the Object and saves on Redis db:
#JmsListener(destination = "${amazon.sqs.destination}")
public void receive(String requestJSON) throws JMSException {
log.info("Received");
try {
Trace trace = Trace.fromJSON(requestJSON);
traceRepository.save(trace);
(...)
I'd like to know your opinions - what is the best approach to minimalize unnecessary calls to SQS to get messages.
Maybe shoud I use for example
factory.setReceiveTimeout(2000L);
Unfortunately there is too little information in Internet about it
Thanks,
Matthew

Argument type error with Spring AMQP receiver

My Spring AMQP application has been logging the following exception on initiation:
org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Failed to invoke target method 'receiveMessage' with argument type = [class [B], value = [{[B#660cff44}]
From my searching I understand that this is because there is a class incompatibility with the message type? However, I am not able to see where this is.
The following are the relevant code segments:
#Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
#Bean
Queue queue() {
return new Queue(config.getAMQPResultsQueue(), false);
}
#Bean
TopicExchange exchange() {
return new TopicExchange(config.getAMQPResultsExchange());
}
#Bean
Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("#");
}
#Bean
SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(config.getAMQPResultsQueue());
container.setMessageListener(listenerAdapter);
container.setMessageConverter(jsonMessageConverter());
return container;
}
#Bean
MessageListenerAdapter listenerAdapter(Receiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
and
#Component
public class Receiver {
public void receiveMessage(String message) {
System.out.println("Received <" + message + ">");
}
}
I have tried setting the class of message to Byte[] but the result is the same. I am sure I am missing something simple - just not sure what it is!
The Jackson2JsonMessageConverter will only perform conversion if the message has a content_type header that contains json.
Otherwise, it will return byte[].
byte[] will also not be converted to Byte[]. Set the header or use byte[].
I ran into this issue when I set the property in RabbitMQ interface to content-type (understandably, since that's how the http spec spells it). But RabbitMQ has it underscored. content_type is the name of the property you would have to set in RabbitMQ interface to publish a message with HTTP Header Content-Type

RabbitListener annotation queue name by ConfigurationProperties

I have configured my rabbit properties via application.yaml and spring configurationProperties.
Thus, when I configure exchanges, queues and bindings, I can use the getters of my properties
#Bean Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(properties.getQueue());
}
#Bean Queue queue() {
return new Queue(properties.getQueue(), true);
}
#Bean TopicExchange exchange() {
return new TopicExchange(properties.getExchange());
}
However, when I configure a #RabbitListener to log the messages on from the queue, I have to use the full properties name like
#RabbitListener(queues = "${some.long.path.to.the.queue.name}")
public void onMessage(
final Message message, final Channel channel) throws Exception {
log.info("receiving message: {}#{}", message, channel);
}
I want to avoid this error prone hard coded String and refer to the configurationProperties bean like:
#RabbitListener(queues = "${properties.getQueue()}")
I had a similar issue once with #EventListener where using a bean reference "#bean.method()" helped, but it does not work here, the bean expression is just interpreted as queue name, which fails because a queue namde "#bean...." does not exist.
Is it possible to use ConfigurationProperty-Beans for RabbitListener queue configuration?
Something like this worked for me where I just used the Bean and SpEL.
#Autowired
Queue queue;
#RabbitListener(queues = "#{queue.getName()}")
I was finally able to accomplish what we both desired to do by taking what #David Diehl suggested, using the bean and SpEL; however, using MyRabbitProperties itself instead. I removed the #EnableConfigurationProperties(MyRabbitProperties.class) in the config class, and registered the bean the standard way:
#Configuration
//#EnableConfigurationProperties(RabbitProperties.class)
#EnableRabbit
public class RabbitConfig {
//private final MyRabbitProperties myRabbitProperties;
//#Autowired
//public RabbitConfig(MyRabbitProperties myRabbitProperties) {
//this.myRabbitProperties = myRabbitProperties;
//}
#Bean
public TopicExchange myExchange(MyRabbitProperties myRabbitProperties) {
return new TopicExchange(myRabbitProperties.getExchange());
}
#Bean
public Queue myQueueBean(MyRabbitProperties myRabbitProperties) {
return new Queue(myRabbitProperties.getQueue(), true);
}
#Bean
public Binding binding(Queue myQueueBean, TopicExchange myExchange, MyRabbitProperties myRabbitProperties) {
return BindingBuilder.bind(myQueueBean).to(myExchange).with(myRabbitProperties.getRoutingKey());
}
#Bean
public MyRabbitProperties myRabbitProperties() {
return new MyRabbitProperties();
}
}
From there, you can access the get method for that field:
#Component
public class RabbitQueueListenerClass {
#RabbitListener(queues = "#{myRabbitProperties.getQueue()}")
public void processMessage(Message message) {
}
}
#RabbitListener(queues = "#{myQueue.name}")
Listener:
#RabbitListener(queues = "${queueName}")
application.properties:
queueName=myQueue

Spring AMQP Transactional Processing and Retry

I am trying the Srpring AMQP features regarding transactional message processing.
I have the following setup - I have a message consumer that is annotated as #Transactional
#Transactional
public void handleMessage(EventPayload event) {
Shop shop = new Shop();
shop.setName(event.getName());
Shop savedShop = shopService.create(shop);
log.info("Created shop {} from event {}", shop, event);
}
In shopService.create I save the shop and send another message about the creation:
#Transactional(propagation = REQUIRED)
#Component
public class ShopService {
...
public Shop create(Shop shop) {
eventPublisher.publish(new EventPayload(shop.getName()));
return shopRepository.save(shop);
}
}
I want to achieve the following - the message sent in the create method should just go to the broker if the database action succeeded. If it fails the message is not sent and the received message is rolled back.
I also have a Retry configured - so I expect each message to be retried 3 times before it is rejected:
#Bean
public RetryOperationsInterceptor retryOperationsInterceptor() {
return RetryInterceptorBuilder.stateless()
.maxAttempts(3)
.backOffOptions(1000, 2.0, 10000)
.build();
}
I am observing the following behaviour:
When I configure the container as follows the message is retried 3 times but every time the message in shopService.create is sent to the broker:
#Bean
SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(testEventSubscriberQueue().getName());
container.setMessageListener(listenerAdapter);
container.setChannelTransacted(true);
container.setAdviceChain(new Advice[]{retryOperationsInterceptor()});
return container;
}
So I tried passing the PlatformTransactionManager to the container -
#Bean
SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter,
PlatformTransactionManager transactionManager) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(testEventSubscriberQueue().getName());
container.setMessageListener(listenerAdapter);
container.setChannelTransacted(true);
container.setTransactionManager(transactionManager);
container.setAdviceChain(new Advice[]{retryOperationsInterceptor()});
return container;
}
Now the message sent in shopService.create is only send to the broker if the database transaction succeeded - which is what I want - but the message is retried indefinitely now - and not discarded after 3 retires as configured. But it seems that the backOff settings are applied - so there is some time between the retries.
The setup described does not really make sense from a business point of view - I am trying to understand and evaluate the transaction capabilities.
I am use spring-amqp 1.5.1.RELEASE
Thanks for any hints.
I had the same requirements, an #RabbitListener annotated with #Transactional, I wanted retry with backoff. It works even stateless with the following config:
#Bean
public RetryOperationsInterceptor retryOperationsInterceptor( ) {
return RetryInterceptorBuilder.stateless()
.maxAttempts( 3 )
.recoverer( new RejectAndDontRequeueRecoverer() )
.backOffOptions(1000, 2, 10000)
.build();
}
#Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter( ObjectMapper objectMapper ) {
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter( objectMapper );
jackson2JsonMessageConverter.setCreateMessageIds( true );
return jackson2JsonMessageConverter;
}
#Bean
SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( ConnectionFactory connectionFactory,
PlatformTransactionManager transactionManager,
Jackson2JsonMessageConverter converter) {
SimpleRabbitListenerContainerFactory container = new SimpleRabbitListenerContainerFactory ();
container.setConnectionFactory(connectionFactory);
container.setChannelTransacted(true);
container.setTransactionManager(transactionManager);
container.setAdviceChain( retryOperationsInterceptor() );
container.setMessageConverter( converter );
return container;
}
To use stateless(), using RejectAndDontRequeueRecoverer was important because otherwise the retry will work but the consumer will then by default put the message back on the queue. Then the consumer will retrieve it again, applying the retry policy and then putting it back on the queue infinitely.

Resources