The following class in included into several consumer applications:
#Component
#Configuration
public class HealthListener {
public static final String HEALTH_CHECK_QUEUE_NAME = "healthCheckQueue";
public static final String HEALTH_CHECK_FANOUT_EXCHANGE_NAME = "health-check-fanout";
#Bean
public Binding healthListenerBinding(
#Qualifier("healthCheckQueue") Queue queue,
#Qualifier("instanceFanoutExchange") FanoutExchange exchange) {
return BindingBuilder.bind(queue).to(exchange);
}
#Bean
public FanoutExchange instanceFanoutExchange() {
return new FanoutExchange(HEALTH_CHECK_FANOUT_EXCHANGE_NAME, true, false);
}
#Bean
public Queue healthCheckQueue() {
return new Queue(HEALTH_CHECK_QUEUE_NAME);
}
#RabbitListener(queues = HEALTH_CHECK_QUEUE_NAME)
public String healthCheck() {
return "some result";
}
}
I'm trying to send a message to fanout exchange, and receive all replies, to know which consumers are running.
I can send a message and get the first reply like this:
#Autowired
RabbitTemplate template;
// ...
String firstReply = template.convertSendAndReceiveAsType("health-check-fanout", "", "", ParameterizedTypeReference.forType(String.class));
However I need to get all repliest to this message, not just the first one. I need to set up a reply listener, but I'm not sure how.
The (convertS|s)endAndReceive.*() methods are not designed to handle multiple replies; they are strictly one request/one reply methods.
You would need to use a (convertAndS|s)end() method to send the request, and implement your own reply mechanism, perhaps using a listener container for the replies, together with some component to aggregate the replies.
You could use something like a Spring Integration Aggregator for that, but you would need some mechanism (ReleaseStrategy) that would know when all expected replies are received.
Or you can simply receive the discrete replies and handle them individually.
EDIT
#SpringBootApplication
public class So54207780Application {
public static void main(String[] args) {
SpringApplication.run(So54207780Application.class, args);
}
#Bean
public ApplicationRunner runner(RabbitTemplate template) {
return args -> template.convertAndSend("fanout", "", "foo", m -> {
m.getMessageProperties().setReplyTo("replies");
return m;
});
}
#RabbitListener(queues = "queue1")
public String listen1(String in) {
return in.toUpperCase();
}
#RabbitListener(queues = "queue2")
public String listen2(String in) {
return in + in;
}
#RabbitListener(queues = "replies")
public void replyHandler(String reply) {
System.out.println(reply);
}
#Bean
public FanoutExchange fanout() {
return new FanoutExchange("fanout");
}
#Bean
public Queue queue1() {
return new Queue("queue1");
}
#Bean
public Binding binding1() {
return BindingBuilder.bind(queue1()).to(fanout());
}
#Bean
public Queue queue2() {
return new Queue("queue2");
}
#Bean
public Binding binding2() {
return BindingBuilder.bind(queue2()).to(fanout());
}
#Bean
public Queue replies() {
return new Queue("replies");
}
}
and
FOO
foofoo
Related
I'm unable to make works queue listener with Spring Boot and SQS
(the message is sent and appear in SQS ui)
The #MessageMapping or #SqsListener not works
Java: 11
Spring Boot: 2.1.7
Dependencie: spring-cloud-aws-messaging
This is my config
#Configuration
#EnableSqs
public class SqsConfig {
#Value("#{'${env.name:DEV}'}")
private String envName;
#Value("${cloud.aws.region.static}")
private String region;
#Value("${cloud.aws.credentials.access-key}")
private String awsAccessKey;
#Value("${cloud.aws.credentials.secret-key}")
private String awsSecretKey;
#Bean
public Headers headers() {
return new Headers();
}
#Bean
public MessageQueue queueMessagingSqs(Headers headers,
QueueMessagingTemplate queueMessagingTemplate) {
Sqs queue = new Sqs();
queue.setQueueMessagingTemplate(queueMessagingTemplate);
queue.setHeaders(headers);
return queue;
}
private ResourceIdResolver getResourceIdResolver() {
return queueName -> envName + "-" + queueName;
}
#Bean
public DestinationResolver destinationResolver(AmazonSQSAsync amazonSQSAsync) {
DynamicQueueUrlDestinationResolver destinationResolver = new DynamicQueueUrlDestinationResolver(
amazonSQSAsync,
getResourceIdResolver());
destinationResolver.setAutoCreate(true);
return destinationResolver;
}
#Bean
public QueueMessagingTemplate queueMessagingTemplate(AmazonSQSAsync amazonSQSAsync,
DestinationResolver destinationResolver) {
return new QueueMessagingTemplate(amazonSQSAsync, destinationResolver, null);
}
#Bean
public QueueMessageHandlerFactory queueMessageHandlerFactory() {
QueueMessageHandlerFactory factory = new QueueMessageHandlerFactory();
MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();
messageConverter.setStrictContentTypeMatch(false);
factory.setArgumentResolvers(Collections.singletonList(new PayloadArgumentResolver(messageConverter)));
return factory;
}
#Bean
public SimpleMessageListenerContainerFactory simpleMessageListenerContainerFactory(AmazonSQSAsync amazonSqs) {
SimpleMessageListenerContainerFactory factory = new SimpleMessageListenerContainerFactory();
factory.setAmazonSqs(amazonSqs);
factory.setMaxNumberOfMessages(10);
factory.setWaitTimeOut(2);
return factory;
}
}
I notice also that org.springframework.cloud.aws.messaging.config.SimpleMessageListenerContainerFactory and org.springframework.cloud.aws.messaging.config.annotation.SqsConfiguration run on startup
And my test
#RunWith(SpringJUnit4ClassRunner.class)
public class ListenTest {
#Autowired
private MessageQueue queue;
private final String queueName = "test-queue-receive";
private String result = null;
#Test
public void test_listen() {
// given
String data = "abc";
// when
queue.send(queueName, data).join();
// then
Awaitility.await()
.atMost(10, TimeUnit.SECONDS)
.until(() -> Objects.nonNull(result));
Assertions.assertThat(result).equals(data);
}
#MessageMapping(value = queueName)
public void receive(String data) {
this.result = data;
}
}
Do you think something is wrong ?
I create a repo for exemple : (https://github.com/mmaryo/java-sqs-test)
In test folder, change aws credentials in 'application.yml'
Then run tests
I had the same issue when using the spring-cloud-aws-messaging package, but then I used the queue URL in the #SqsListener annotation instead of the queue name and it worked.
#SqsListener(value = { "https://full-queue-URL" }, deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void receive(String message) {
// do something
}
It seems you can use the queue name when using the spring-cloud-starter-aws-messaging package. I believe there is some configuration that allows usage of the queue name instead of URL if you don't want to use the starter package.
EDIT: I noticed the region was being defaulted to us-west-2 despite me listing us-east-1 in my properties file. Then I created a RegionProvider bean and set the region to us-east-1 in there and now when I use the queue name in the #SqsMessaging it is found and correctly resolved to the URL in the framework code.
you'll need to leverage the #Primary annotation, this is what worked for me:
#Autowired(required = false)
private AWSCredentialsProvider awsCredentialsProvider;
#Autowired
private AppConfig appConfig;
#Bean
public QueueMessagingTemplate getQueueMessagingTemplate() {
return new QueueMessagingTemplate(sqsClient());
}
#Primary
#Bean
public AmazonSQSAsync sqsClient() {
AmazonSQSAsyncClientBuilder builder = AmazonSQSAsyncClientBuilder.standard();
if (this.awsCredentialsProvider != null) {
builder.withCredentials(this.awsCredentialsProvider);
}
if (appConfig.getSqsRegion() != null) {
builder.withRegion(appConfig.getSqsRegion());
} else {
builder.withRegion(Regions.DEFAULT_REGION);
}
return builder.build();
}
build.gradle needs these deps:
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.0.RELEASE")
implementation("org.springframework.cloud:spring-cloud-aws-messaging:2.2.0.RELEASE")
I am currently working on Kafka module where I am using spring-kafka abstraction of Kafka communication. I am able to integrate the producer & consumer from real implementation standpoint however, I am not sure how to test (specifically integration test) the business logic surrounds at consumer with #KafkaListener. I tried to follow spring-kafk documentation and various blogs on the topic but none of those answer my intended question.
Spring Boot test class
//imports not mentioned due to brevity
#RunWith(SpringRunner.class)
#SpringBootTest(classes = PaymentAccountUpdaterApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class CardUpdaterMessagingIntegrationTest {
private final static String cardUpdateTopic = "TP.PRF.CARDEVENTS";
#Autowired
private ObjectMapper objectMapper;
#ClassRule
public static KafkaEmbedded kafkaEmbedded =
new KafkaEmbedded(1, false, cardUpdateTopic);
#Test
public void sampleTest() throws Exception {
Map<String, Object> consumerConfig =
KafkaTestUtils.consumerProps("test", "false", kafkaEmbedded);
consumerConfig.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
consumerConfig.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
ConsumerFactory<String, String> cf = new DefaultKafkaConsumerFactory<>(consumerConfig);
ContainerProperties containerProperties = new ContainerProperties(cardUpdateTopic);
containerProperties.setMessageListener(new SafeStringJsonMessageConverter());
KafkaMessageListenerContainer<String, String>
container = new KafkaMessageListenerContainer<>(cf, containerProperties);
BlockingQueue<ConsumerRecord<String, String>> records = new LinkedBlockingQueue<>();
container.setupMessageListener((MessageListener<String, String>) data -> {
System.out.println("Added to Queue: "+ data);
records.add(data);
});
container.setBeanName("templateTests");
container.start();
ContainerTestUtils.waitForAssignment(container, kafkaEmbedded.getPartitionsPerTopic());
Map<String, Object> producerConfig = KafkaTestUtils.senderProps(kafkaEmbedded.getBrokersAsString());
producerConfig.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
producerConfig.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
ProducerFactory<String, Object> pf =
new DefaultKafkaProducerFactory<>(producerConfig);
KafkaTemplate<String, Object> kafkaTemplate = new KafkaTemplate<>(pf);
String payload = objectMapper.writeValueAsString(accountWrapper());
kafkaTemplate.send(cardUpdateTopic, 0, payload);
ConsumerRecord<String, String> received = records.poll(10, TimeUnit.SECONDS);
assertThat(received).has(partition(0));
}
#After
public void after() {
kafkaEmbedded.after();
}
private AccountWrapper accountWrapper() {
return AccountWrapper.builder()
.eventSource("PROFILE")
.eventName("INITIAL_LOAD_CARD")
.eventTime(LocalDateTime.now().toString())
.eventID("8730c547-02bd-45c0-857b-d90f859e886c")
.details(AccountDetail.builder()
.customerId("idArZ_K2IgE86DcPhv-uZw")
.vaultId("912A60928AD04F69F3877D5B422327EE")
.expiryDate("122019")
.build())
.build();
}
}
Listener Class
#Service
public class ConsumerMessageListener {
private static final Logger LOGGER = LoggerFactory.getLogger(ConsumerMessageListener.class);
private ConsumerMessageProcessorService consumerMessageProcessorService;
public ConsumerMessageListener(ConsumerMessageProcessorService consumerMessageProcessorService) {
this.consumerMessageProcessorService = consumerMessageProcessorService;
}
#KafkaListener(id = "cardUpdateEventListener",
topics = "${kafka.consumer.cardupdates.topic}",
containerFactory = "kafkaJsonListenerContainerFactory")
public void processIncomingMessage(Payload<AccountWrapper,Object> payloadContainer,
Acknowledgment acknowledgment,
#Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
#Header(KafkaHeaders.RECEIVED_PARTITION_ID) String partitionId,
#Header(KafkaHeaders.OFFSET) String offset) {
try {
// business logic to process the message
consumerMessageProcessorService.processIncomingMessage(payloadContainer);
} catch (Exception e) {
LOGGER.error("Unhandled exception in card event message consumer. Discarding offset commit." +
"message:: {}, details:: {}", e.getMessage(), messageMetadataInfo);
throw e;
}
acknowledgment.acknowledge();
}
}
My question is: In the test class I am asserting the partition, payload etc which is polling from BlockingQueue, however, my question is how can I verify that my business logic in the class annotated with #KafkaListener is getting executed properly and routing the messages to different topic based on error handling and other business scenarios. In some of the examples, I saw CountDownLatch to assert which I don't want to put in my business logic to assert in a production grade code. Also the message processor is Async so, how to assert the execution, not sure.
Any help, appreciated.
is getting executed properly and routing the messages to different topic based on error handling and other business scenarios.
An integration test can consume from that "different" topic to assert that the listener processed it as expected.
You could also add a BeanPostProcessor to your test case and wrap the ConsumerMessageListener bean in a proxy to verify the input arguments are as expected.
EDIT
Here is an example of wrapping the listener in a proxy...
#SpringBootApplication
public class So53678801Application {
public static void main(String[] args) {
SpringApplication.run(So53678801Application.class, args);
}
#Bean
public MessageConverter converter() {
return new StringJsonMessageConverter();
}
public static class Foo {
private String bar;
public Foo() {
super();
}
public Foo(String bar) {
this.bar = bar;
}
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
#Override
public String toString() {
return "Foo [bar=" + this.bar + "]";
}
}
}
#Component
class Listener {
#KafkaListener(id = "so53678801", topics = "so53678801")
public void processIncomingMessage(Foo payload,
Acknowledgment acknowledgment,
#Header(KafkaHeaders.RECEIVED_TOPIC) String topic,
#Header(KafkaHeaders.RECEIVED_PARTITION_ID) String partitionId,
#Header(KafkaHeaders.OFFSET) String offset) {
System.out.println(payload);
// ...
acknowledgment.acknowledge();
}
}
and
spring.kafka.consumer.enable-auto-commit=false
spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.listener.ack-mode=manual
and
#RunWith(SpringRunner.class)
#SpringBootTest(classes = { So53678801Application.class,
So53678801ApplicationTests.TestConfig.class})
public class So53678801ApplicationTests {
#ClassRule
public static EmbeddedKafkaRule embededKafka = new EmbeddedKafkaRule(1, false, "so53678801");
#BeforeClass
public static void setup() {
System.setProperty("spring.kafka.bootstrap-servers",
embededKafka.getEmbeddedKafka().getBrokersAsString());
}
#Autowired
private KafkaTemplate<String, String> template;
#Autowired
private ListenerWrapper wrapper;
#Test
public void test() throws Exception {
this.template.send("so53678801", "{\"bar\":\"baz\"}");
assertThat(this.wrapper.latch.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(this.wrapper.argsReceived[0]).isInstanceOf(Foo.class);
assertThat(((Foo) this.wrapper.argsReceived[0]).getBar()).isEqualTo("baz");
assertThat(this.wrapper.ackCalled).isTrue();
}
#Configuration
public static class TestConfig {
#Bean
public static ListenerWrapper bpp() { // BPPs have to be static
return new ListenerWrapper();
}
}
public static class ListenerWrapper implements BeanPostProcessor, Ordered {
private final CountDownLatch latch = new CountDownLatch(1);
private Object[] argsReceived;
private boolean ackCalled;
#Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Listener) {
ProxyFactory pf = new ProxyFactory(bean);
pf.setProxyTargetClass(true); // unless the listener is on an interface
pf.addAdvice(interceptor());
return pf.getProxy();
}
return bean;
}
private MethodInterceptor interceptor() {
return invocation -> {
if (invocation.getMethod().getName().equals("processIncomingMessage")) {
Object[] args = invocation.getArguments();
this.argsReceived = Arrays.copyOf(args, args.length);
Acknowledgment ack = (Acknowledgment) args[1];
args[1] = (Acknowledgment) () -> {
this.ackCalled = true;
ack.acknowledge();
};
try {
return invocation.proceed();
}
finally {
this.latch.countDown();
}
}
else {
return invocation.proceed();
}
};
}
}
}
I have classes:
public class MqMessage implements Serializable{
private String event;
private String absolutePath;
private boolean isDirectory;
private Integer hash;
private Node node;
get/set
}
Configuration class:
public class RabbitConfiguration {
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory =
new CachingConnectionFactory("localhost");
return connectionFactory;
}
#Bean
public AmqpAdmin amqpAdmin() {
return new RabbitAdmin(connectionFactory());
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
template.setExchange("exchange-events");
return template;
}
//объявляем очередь
#Bean
public Queue myQueue1() {
return new Queue("queue-events");
}
#Bean
public FanoutExchange fanoutExchangeA() {
return new FanoutExchange("exchange-events");
}
#Bean
public Binding binding1() {
return BindingBuilder.bind(myQueue1()).to(fanoutExchangeA());
}
Send message
public class ServerHandler implements EventHandler {
//сама структура отражающая состояние файлов, содеражащая метоы для работы с ними
#Autowired
Node fileTreeRoot;
SimpleMessageConverter simpleMessageConverter;
#Override
public void setRoot(Node fileTreeRoot) {
this.fileTreeRoot = fileTreeRoot;
}
#Autowired
RabbitTemplate rabbitTemplate;
//логика обработки событий
#Override
public void eventHandle(String event, String path) {
/*bussines-logick
*/
rabbitTemplate.setExchange("exchange-events");
rabbitTemplate.convertAndSend(new MqMessage(event,fileTreeRoot));
return;
}
public ServerHandler() {
}
Listener:
public class Client {
Node rootNodeClient = new Node();
EventHandler handlerClient = new ClientHandler();
#RabbitListener(queues = "queue-events")
public void onMessage(MqMessage message) {
System.out.println(message.getNode().hashCode());
rootNodeClient = message.getNode();
}
a have error only start app
2017-08-08 12:58:02.128 WARN 5024 --- [cTaskExecutor-1]
s.a.r.l.ConditionalRejectingErrorHandler : Execution of Rabbit message
listener failed.
org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException:
Listener method could not be invoked with the incoming message
Caused by:
org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException:
Could not resolve method parameter at index 0 in public void
prcjt.client.Client.onMessage(prcjt.message.MqMessage): 1 error(s):
[Error in object 'message': codes []; arguments []; default message
[Payload value must not be empty]]
Error does not always exist
Help please
Using mappingJackson2MessageConverted did not work for me. After lots of investigation found that I was using wrong converter. Add the below code in Listener service, it worked for me.
#Bean
public Jackson2JsonMessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
Caused by: org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException: Could not resolve method parameter at index 0
From the exception information, it seems that spring could not resolve MqMessage in the listener correctly, you can try to add a mappingJackson2MessageConverter to the client. Refer to this link.
I faced this issue for couple of reasons:
Not using "implements Serializable" (which I see you are already using)
No constructor in model class. (Use empty constructor)
public ClassToGetQData() {
super();
// TODO Auto-generated constructor stub
}
Also, using #JsonIgnoreProperties(ignoreUnknown = true) helped me with missing data elements in incoming data.
How to acknowledge the messages manually without using auto acknowledgement.
Is there a way to use this along with the #RabbitListener and #EnableRabbit style of configuration.
Most of the documentation tells us to use SimpleMessageListenerContainer along with ChannelAwareMessageListener.
However using that we lose the flexibility that is provided with the annotations.
I have configured my service as below :
#Service
public class EventReceiver {
#Autowired
private MessageSender messageSender;
#RabbitListener(queues = "${eventqueue}")
public void receiveMessage(Order order) throws Exception {
// code for processing order
}
My RabbitConfiguration is as below
#EnableRabbit
public class RabbitApplication implements RabbitListenerConfigurer {
public static void main(String[] args) {
SpringApplication.run(RabbitApplication.class, args);
}
#Bean
public MappingJackson2MessageConverter jackson2Converter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
return converter;
#Bean
public SimpleRabbitListenerContainerFactory myRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(rabbitConnectionFactory());
factory.setMaxConcurrentConsumers(5);
factory.setMessageConverter((MessageConverter) jackson2Converter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
#Bean
public ConnectionFactory rabbitConnectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost("localhost");
return connectionFactory;
}
#Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
registrar.setContainerFactory(myRabbitListenerContainerFactory());
}
#Autowired
private EventReceiver receiver;
}
}
Any help will be appreciated on how to adapt manual channel acknowledgement along with the above style of configuration.
If we implement the ChannelAwareMessageListener then the onMessage signature will change.
Can we implement ChannelAwareMessageListener on a service ?
Add the Channel to the #RabbitListener method...
#RabbitListener(queues = "${eventqueue}")
public void receiveMessage(Order order, Channel channel,
#Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
...
}
and use the tag in the basicAck, basicReject.
EDIT
#SpringBootApplication
#EnableRabbit
public class So38728668Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(So38728668Application.class, args);
context.getBean(RabbitTemplate.class).convertAndSend("", "so38728668", "foo");
context.getBean(Listener.class).latch.await(60, TimeUnit.SECONDS);
context.close();
}
#Bean
public Queue so38728668() {
return new Queue("so38728668");
}
#Bean
public Listener listener() {
return new Listener();
}
public static class Listener {
private final CountDownLatch latch = new CountDownLatch(1);
#RabbitListener(queues = "so38728668")
public void receive(String payload, Channel channel, #Header(AmqpHeaders.DELIVERY_TAG) long tag)
throws IOException {
System.out.println(payload);
channel.basicAck(tag, false);
latch.countDown();
}
}
}
application.properties:
spring.rabbitmq.listener.acknowledge-mode=manual
Just in case you need to use #onMessage() from ChannelAwareMessageListener class. Then you can do it this way.
#Component
public class MyMessageListener implements ChannelAwareMessageListener {
#Override
public void onMessage(Message message, Channel channel) {
log.info("Message received.");
// do something with the message
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
And for the rabbitConfiguration
#Configuration
public class RabbitConfig {
public static final String topicExchangeName = "exchange1";
public static final String queueName = "queue1";
public static final String routingKey = "queue1.route.#";
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost");
connectionFactory.setUsername("xxxx");
connectionFactory.setPassword("xxxxxxxxxx");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("vHost1");
return connectionFactory;
}
#Bean
public RabbitTemplate rabbitTemplate() {
return new RabbitTemplate(connectionFactory());
}
#Bean
Queue queue() {
return new Queue(queueName, true);
}
#Bean
TopicExchange exchange() {
return new TopicExchange(topicExchangeName);
}
#Bean
Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(routingKey);
}
#Bean
public SimpleMessageListenerContainer listenerContainer(MyMessageListener myRabbitMessageListener) {
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
listenerContainer.setConnectionFactory(connectionFactory());
listenerContainer.setQueueNames(queueName);
listenerContainer.setMessageListener(myRabbitMessageListener);
listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);
listenerContainer.setConcurrency("4");
listenerContainer.setPrefetchCount(20);
return listenerContainer;
}
}
Thanks for gary's help. I finally solved the issue. I am documenting this for the benefit of others.
This needs to be documented as part of standard documentation in Spring AMQP reference documentation page.
Service class is as below.
#Service
public class Consumer {
#RabbitListener(queues = "${eventqueue}")
public void receiveMessage(Order order, Channel channel) throws Exception {
// the above methodname can be anything but should have channel as second signature
channel.basicConsume(eventQueue, false, channel.getDefaultConsumer());
// Get the delivery tag
long deliveryTag = channel.basicGet(eventQueue, false).getEnvelope().getDeliveryTag();
try {
// code for processing order
catch(Exception) {
// handle exception
channel.basicReject(deliveryTag, true);
}
// If all logic is successful
channel.basicAck(deliveryTag, false);
}
the configuration has also been modified as below
public class RabbitApplication implements RabbitListenerConfigurer {
private static final Logger log = LoggerFactory.getLogger(RabbitApplication .class);
public static void main(String[] args) {
SpringApplication.run(RabbitApplication.class, args);
}
#Bean
public MappingJackson2MessageConverter jackson2Converter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
return converter;
}
#Bean
public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setMessageConverter(jackson2Converter());
return factory;
}
#Autowired
private Consumer consumer;
#Override
public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(myHandlerMethodFactory());
}
...
}
Note: no need to configure Rabbitconnectionfactory or containerfactor etc since the annotation implicity take care of all this.
I have to decode AMQP message using Spring. To handle it I now use:
// Configure queue.
RabbitAdmin admin = new RabbitAdmin(cf);
Queue queue = new Queue(queueName);
admin.declareQueue(queue);
FanoutExchange exchange = new FanoutExchange(exchangeName);
admin.declareExchange(exchange);
admin.declareBinding(BindingBuilder.bind(queue).to(exchange));
// set up the listener and container
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf);
MessageListenerAdapter adapter = new MessageListenerAdapter(listener);
container.setMessageListener(adapter);
container.setQueueNames(queueName);
container.start();
And my Listener is
public class DataListener {
public void handleMessage(Object incomingMessage) {
LOGGER.error("AMQP: got message.{}", incomingMessage);
}
}
The message is sent using convertAndSend method of AmqpTemplate. No configuration was given to AmqpTemplate, everything is by default.
How I can possibly receive my incomingMessage as a HashMap of fields? I do not want to strongly couple it to particular object type.
Assuming you mean your message is a POJO bean...
Use JSON - on the outbound side use a Jackson2JsonMessageConverter instead of the default SimpleMessageConverter, which uses Java serialization.
On the receiving side, the same JSON converter will try to convert the incoming stream to the original POJO.
To avoid that, configure the JSON message converter to map the class name to a HashMap instead of the original POJO.
You can do that by providing the converter with a custom DefaultJackson2JavaTypeMapper that is configured to map the class name from the __TypeId__ header to java.util.HashMap.
EDIT
Or you can simply inject a ClassMapper that always returns HashMap - here's a quick boot App I wrote to illustrate the technique:
#SpringBootApplication
public class So36837736Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(So36837736Application.class, args);
context.getBean(RabbitTemplate.class).convertAndSend(new Foo("bar"));
Thread.sleep(10000);
context.close();
}
#Bean
public RabbitTemplate template(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setRoutingKey(queue().getName());
return rabbitTemplate;
}
#Bean
public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueues(queue());
MessageListenerAdapter adapter = new MessageListenerAdapter(new Object() {
#SuppressWarnings("unused")
public void handleMessage(Map<String, Object> map) {
System.out.println("\n\n\n" + map + "\n\n\n");
}
});
Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter();
ClassMapper classMapper = new ClassMapper() {
#Override
public void fromClass(Class<?> clazz, MessageProperties properties) {
}
#Override
public Class<?> toClass(MessageProperties properties) {
return HashMap.class;
}
};
messageConverter.setClassMapper(classMapper);
adapter.setMessageConverter(messageConverter);
container.setMessageListener(adapter);
return container;
}
#Bean
public Queue queue() {
return new AnonymousQueue();
}
public static class Foo {
private final String bar;
private Foo(String bar) {
this.bar = bar;
}
public String getBar() {
return this.bar;
}
}
}