Publisher Confirms with Spring Cloud Stream - rabbitmq

I'm interested to use Publisher Confirms in some producers that we have in a project, using Spring Cloud Stream. I have tried doing a small PoC but it is not working. As far as I see in the documentation, this is possible for Asyncrhonous Publisher Confirm, and it should be as easy as do the next changes:
Add in the application.yml the confirmAckChannel and enable the errorChannelEnabled property.
spring.cloud.stream:
binders:
rabbitDefault:
defaultCandidate: false
type: rabbit
environment.spring.rabbitmq.host: ${spring.rabbitmq.addresses}
....
bindings:
testOutput:
destination: test
binder: rabbitDefault
content-type: application/json
rabbit.bindings:
testOutput.producer:
confirmAckChannel: "testAck"
errorChannelEnabled: true
Then a simple service triggered by an endpoint, where I insert the header related with the errorChannel to the event.
#Service
#RequiredArgsConstructor
public class TestService {
private final TestPublisher testPublisher;
public void sendMessage() {
testPublisher.send(addHeaders());
}
private Message<Event<TestEvent>> addHeaders() {
return withPayload(new Event<>(TestEvent.builder().build()))
.setHeader(MessageHeaders.ERROR_CHANNEL, "errorChannelTest")
.build();
}
}
And then the Publisher of RabbitMQ
#Component
#RequiredArgsConstructor
public class TestPublisher {
private final MessagingChannels messagingChannels;
public boolean send(Message<Event<TestEvent>> message) {
return messagingChannels.test().send(message);
}
}
Where MessagingChannels is implemented as
public interface MessagingChannels {
#Input("testAck")
MessageChannel testAck();
#Input("errorChannelTest")
MessageChannel testError();
#Output("testOutput")
MessageChannel test();
}
After that, I have implemented 2 listeners, one for errorChannelTest input and the other one for testAck.
#Slf4j
#Component
#RequiredArgsConstructor
class TestErrorListener {
#StreamListener("errorChannelTest")
void onCommandReceived(Event<Message> message) {
log.info("Message error received: " + message);
}
}
#Slf4j
#Component
#RequiredArgsConstructor
class TestAckListener {
#StreamListener("testAck")
void onCommandReceived(Event<Message> message) {
log.info("Message ACK received: " + message);
}
}
However, I didn't receive any ACK or NACK for RabbitMQ in these 2 listeners, the event was sent properly to RabbitMQ and manage by the exchange, but then I haven't received any response from RabbitMQ.
Am I missing something? I have checked also with these 2 properties, but it doesn't work as well
spring:
rabbitmq:
publisher-confirm-type: CORRELATED
publisher-returns: true
I'm using Spring-Cloud-Stream 3.0.1.RELEASE and spring-cloud-starter-stream-rabbit 3.0.1.RELEASE
----EDITED------
This is the sample working updated with the recommendations of Gary Russell
Application.yml
spring.cloud.stream:
binders:
rabbitDefault:
defaultCandidate: false
type: rabbit
environment.spring.rabbitmq.host: ${spring.rabbitmq.addresses}
bindings:
testOutput:
destination: exchange.output.test
binder: rabbitDefault
content-type: application/json
testOutput.producer:
errorChannelEnabled: true
rabbit.bindings:
testOutput.producer:
confirmAckChannel: "testAck"
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
TestService
#Service
#RequiredArgsConstructor
public class TestService {
private final TestPublisher testPublisher;
public void sendMessage() {
testPublisher.send(addHeaders());
}
private Message<Event<TestEvent>> addHeaders(Test test) {
return withPayload(new Event<>(TestEvent.builder().test(test).build()))
.build();
}
}
TestService is triggered by an endpoint in the next simple controller to check this PoC.
#RestController
#RequiredArgsConstructor
public class TestController {
private final TestService testService;
#PostMapping("/services/v1/test")
public ResponseEntity<Object> test(#RequestBody Test test) {
testService.sendMessage(test);
return ResponseEntity.ok().build();
}
}
And then the Publisher of RabbitMQ with both ServiceActivators
#Component
#RequiredArgsConstructor
public class TestPublisher {
private final MessagingChannels messagingChannels;
public boolean send(Message<Event<TestEvent>> message) {
log.info("Message for Testing Publisher confirms sent: " + message);
return messagingChannels.test().send(message);
}
#ServiceActivator(inputChannel = TEST_ACK)
public void acks(Message<?> ack) {
log.info("Message ACK received for Test: " + ack);
}
#ServiceActivator(inputChannel = TEST_ERROR)
public void errors(Message<?> error) {
log.info("Message error for Test received: " + error);
}
}
Where MessagingChannels is implemented as
public interface MessagingChannels {
#Input("testAck")
MessageChannel testAck();
#Input("testOutput.errors")
MessageChannel testError();
#Output("testOutput")
MessageChannel test();
}
This is the Main of the application (I have checked with #EnableIntegration too).
#EnableBinding(MessagingChannels.class)
#SpringBootApplication
#EnableScheduling
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}

testAck should not be a binding; it should be a #ServiceActivator instead.
.setHeader(MessageHeaders.ERROR_CHANNEL, "errorChannelTest")
That won't work in this context; errors are sent to a channel named testOutput.errors; again; this needs a #ServiceActivator, not a binding.
You have errorChannelEnabled in the wrong place; it's a common producer property, not rabbit-specific.
#SpringBootApplication
#EnableBinding(Source.class)
public class So62219823Application {
public static void main(String[] args) {
SpringApplication.run(So62219823Application.class, args);
}
#InboundChannelAdapter(channel = "output")
public String source() {
return "foo";
}
#ServiceActivator(inputChannel = "acks")
public void acks(Message<?> ack) {
System.out.println("Ack: " + ack);
}
#ServiceActivator(inputChannel = "output.errors")
public void errors(Message<?> error) {
System.out.println("Error: " + error);
}
}
spring:
cloud:
stream:
bindings:
output:
producer:
error-channel-enabled: true
rabbit:
bindings:
output:
producer:
confirm-ack-channel: acks
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true

Related

SessionDestroyedEvent does not receive session expired event for Redis cluster

ExpiredEvent seems to be lost..... if we use redis cluster.
It works perfect if we use a stand-alone redis server.
my redis.config
notify-keyspace-events "Egx"
my project code
implementation 'org.springframework.session:spring-session-data-redis:2.7.0'
implementation 'org.springframework.data:spring-data-redis:2.7.3'
implementation 'io.lettuce:lettuce-core:6.1.8.RELEASE'
#configuration
#EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60 * 1)
#EnableRedisRepositories
public class RedisConfig {
#bean
public LettuceConnectionFactory lettuceConnectionFactory() {
RedisClusterConfiguration redisClusterConfiguration
= new RedisClusterConfiguration(List.of("ip:port", "ip:port", "ip:port"));
redisClusterConfiguration.setPassword("password");
return new LettuceConnectionFactory(redisClusterConfiguration);
}
}
#component
public class RedisSessionListener {
#eventlistener
public void onCreate(org.springframework.session.events.SessionCreatedEvent event) {
System.out.println("create!!");
System.out.println(event);
}
#EventListener
public void onDestroy(org.springframework.session.events.SessionDestroyedEvent event) {
System.out.println("destory!!");
System.out.println(event);
}
#EventListener
public void sessionExired(SessionExpiredEvent event) {
System.out.println("exired!!");
System.out.println(event);
}
}
I repeated log in and log out
test result..
As it's seem like the expired event seems to be lost.
Is there a solution?

RabbitMQ channel lifetime and access in ASP Net Core

I have a ASP NET Core application that will serve as a RabbitMQ producer.I have read the tutorial and guides regarding the RabbitMQ .NET client and i still do not know how to deal with the channel lifetime and concurrent access.
From what i have read i understood the following:
IConnection is threadsafe ,but is costly to create
IModel is not threadsafe but is lightweight
For the IConnection i would initialize it in the Startup and inject it as a singleton (service).
However i I do not know how to deal with IModel management.Lets say i have a couple of services that use it, is it scalable to just :
Solution 1
public void Publish(IConnection connection)
{
using(IModel model=connection.CreateChannel())
{
model.BasicPublish(...);
}
}
Solution 2
From what i have read , i understood that its not really scalable.
So another solution would be to create a separate service which would contain a loop , a ConcurrentQueue, and all services would dispatch messages here.
This service would be the sole publisher to RabbitMQ
Publisher
public class Publisher
{
private CancellationTokenSource tcs=new CancellationTokenSource();
public BlockingCollection<byte[]> messages=new BlockingCollection<byte[]>();
private IModel channel;
private readonly string ExchangeName;
private Task loopTask;
public void Run()
{
this.loopTask=Task.Run(()=>Loop(tcs.Token),tcs.Token);
}
private void Loop(Cancellation token)
{
while(true)
{
token.ThrowIfCancellationRequested();
queue.Take(out byte[]data);
channel.BasicPublish(...,body:data);
}
}
public void Publish(byte[] message)
{
this.queue.Add(message);
}
}
Usage
public class SomeDIService
{
private IConnection connection;
SomeDIService(Publisher publisher)
{
this.publisher=publisher;
}
public void DoSomething(byte[] data)
{
//do something...
this.publisher.Publish(data);
}
}
I would prefer solution 1 but i do not know the performance penalty ,while i do not like solution 2 since i wanted to just publish messages directly to RabbitMQ.Now i have to deal with this long running Task too.
Is there any other solution , am i missing something ? Is there a simpler way?
Update
I mentioned concurrent access.I meant i need a way to publish messages from multiple endpoints (services) to RabbitMQ.
Real scenario
public class Controller1:Controller
{
private SomeDIService service; //uses Publisher
[HttpGet]
public void Endpoint1()
{
this.service.DoSomething();
}
[HttpPost]
public void Endpoint2()
{
this.service.DoSomething();
}
}
public class Controller2:Controller
{
private SomeDIService service;
[HttpGet]
public void Endpoint3()
{
this.service.DoSomething();
}
[HttpPost]
public void Endpoint4()
{
this.service.DoSomething();
}
}
after searching for long time i found this solution and it works very good for me
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace BSG.MessageBroker.RabbitMQ
{
public class Rabbit : IRabbit
{
private readonly EnvConfigModel EnvConfig;
private readonly string _hostname;
private readonly string _password;
private readonly string _exchangeName;
private readonly string _username;
private IConnection _connection;
private IModel _Model;
public Rabbit(IOptions<EnvConfigModel> appSettings)
{
EnvConfig = appSettings.Value;
_Logger = services;
_exchangeName = EnvConfig.Rabbit_ExchangeName;
_hostname = EnvConfig.Rabbit_Host;
_username = EnvConfig.Rabbit_Username;
_password = EnvConfig.Rabbit_Password;
CreateConnection();
_Model = _connection.CreateModel();
}
private void CreateConnection()
{
try
{
var factory = new ConnectionFactory
{
HostName = _hostname,
UserName = _username,
Password = _password,
AutomaticRecoveryEnabled = true,
TopologyRecoveryEnabled = true,
NetworkRecoveryInterval = TimeSpan.FromSeconds(3)
};
_connection = factory.CreateConnection();
}
catch (Exception ex)
{
Console.WriteLine($"Could not create connection: {ex.Message}");
}
}
private bool ConnectionExists()
{
if (_connection != null)
{
return true;
}
CreateShredderConnection();
return _connection != null;
}
public bool PushToQueue(string Message)
{
try
{
if (ConnectionExists())
{
byte[] body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(Message));
_Model.BasicPublish(exchange: _exchangeName,
routingKey: 1001,
basicProperties: null,
body: body);
}
return true;
}
catch (Exception ex)
{
return false;
}
}
}
}

RabbitMQ be sure message reaches a queue

I want to be sure the message is reaching a queue.
Otherwise, I want an exception.
I have tried publisher returns, but it is not what I need, because it is on a different thread and I think it would be tricky to somehow wait for it on the thread sent the message.
Without the transacted channel, the convertAndSend method returned successfully when the exchange did not be there, with the transacted channel now it throws an exception.
What I need is the same when there is no route based on the routing key.
#SpringBootApplication
public class DemoApplication {
private static final Logger log = Logger.getGlobal();
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
#Bean
RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info(replyCode + "," + replyText));
rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
#Bean
CommandLineRunner commandLineRunner(RabbitTemplate rabbitTemplate) {
return args -> {
rabbitTemplate.convertAndSend("exchangeName", "routingKey", "message");
log.info("Send is done.");
};
}
}
only property: spring.rabbitmq.publisher-returns=true
Spring boot version: 2.1.7.RELEASE
Actual:
no exchange -> convertAndSend throws exception
no route at exchange -> method returns
Expected:
no exchange -> convertAndSend throws exception
no route at exchange -> convertAndSend throws exception
You need to use publisher confirms and correlation data:
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.template.mandatory=true
#SpringBootApplication
public class So57464212Application {
public static void main(String[] args) {
SpringApplication.run(So57464212Application.class, args);
}
#Bean
public ApplicationRunner runner(RabbitTemplate template) {
template.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
System.err.println("Returned: " + replyText);
});
template.setConfirmCallback((correlationData, ack, cause) -> {
System.err.println("ack:" + ack);
});
return args -> {
CorrelationData correlationData = new CorrelationData("foo");
template.convertAndSend("", "NOQUEUE", "bar", correlationData);
correlationData.getFuture().get(10, TimeUnit.SECONDS);
if (correlationData.getReturnedMessage() != null) {
throw new RuntimeException("Message was returned");
}
};
}
}

Spring amqp and the correlatinId

I have some doubts about Spring AMQP and the correlationId of an AMQP message.
I have a Project with two queues (“queue.A” and “queue.B”) and one MessageListener on each:
public class ServerHandlerQueueA implements MessageListener {
#Override
public void onMessage(Message msg)
public class ServerHandlerQueueB implements MessageListener {
#Override
public void onMessage(Message msg)
In some cases, when I receive a message in the “queue.A”, I have to redirect it to “queue.B”:
rabbitTemplate.convertAndSend(routingkey, msg, new MessagePostProcessor()
{ …});
In all cases I send the response to the client using the following:
String routingkey = msg.getMessageProperties().getReplyTo();
rabbitTemplate.convertAndSend(routingkey, respuesta, new MessagePostProcessor() {
#Override
public Message postProcessMessage(Message msg) throws AmqpException
{….}
});
This is working correctly if I use Spring AMQP on the client side:
Object _response = getRabbitOperations().convertSendAndReceive(requestExchange, routingKeyManagement, msg,
new MessagePostProcessor()
{
public Message postProcessMessage(Message message) throws AmqpException
{….}
});
But If I use the java client (on then client side):
RpcClient _rpcClient = new RpcClient(channel, exchangeName, routingKey);
Response _response = _rpcClient.doCall(new AMQP.BasicProperties.Builder()
.contentType("application/json")
.deliveryMode(2)
.priority(1)
.userId("myUser")
.appId("MyApp")
.replyTo(replyQueueName)
.correlationId(corrId)
.type("NewOrder")
.build(),
messageBodyBytes);
I always get a NullPointerException in:
com.rabbitmq.client.RpcClient$1.handleDelivery(RpcClient.java:195)
I think it's because of the correlationId treatment. When I send a message with Spring AMQP I can see the “spring_listener_return_correlation” and “spring_request_return_correlation” headers in the consumer, but the “correlationId” property is always null.
How can I make it compatible with the pure java client and with Spring AMQP? I am doing something wrong?
Thanks!
------ EDIT ----------
I’ve upgraded to Spring AMQP 1.7.4 version.
I send a message like this:
Object respuesta = getRabbitOperations().convertSendAndReceive(requestExchange, routingKey, _object,
new MessagePostProcessor()
{
public Message postProcessMessage(Message message) throws AmqpException
{
message.getMessageProperties().setUserId(“myUser”);
message.getMessageProperties().setType(“myType”);
message.getMessageProperties().setAppId("myApp");
message.getMessageProperties().setMessageId(counter.incrementAndGet() + "-myType");
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);
message.getMessageProperties().setRedelivered(false);
return message;
}
});
On the server I have:
#Override
public void onMessage(Message msg)
{
MessageProperties mp = msg.getMessageProperties();
Gson __gson = new Gson();
String _stringMP = __gson.toJson(mp);
System.out.println("MessageProperties:\n" + _stringMP);
}
And I think the problem is that I always get the correlationId null:
{"headers":{"spring_listener_return_correlation":"49bd0a84-9abb-4719-b8a7-8668a4a77f32","spring_request_return_correlation":"32","__TypeId__":"MyType"},"messageId":"32-MyType","appId":"myApp","type":"MyType","replyTo":"amq.rabbitmq.reply-to.g2dkABByYWJiaXRATkRFUy1QQzAyAAAsMwAAAAgD.ia4+GgHgoeBnajbHxOgW+w\u003d\u003d","contentType":"application/json","contentEncoding":"UTF-8","contentLength":0,"contentLengthSet":false,"priority":0,"redelivered":false,"receivedExchange":"requestExchange","receivedRoutingKey":"inquiry","receivedUserId":"myUser",
"deliveryTag":5,"deliveryTagSet":true,"messageCount":0,"consumerTag":"amq.ctag-4H_P9CbWYZMML-QsmyaQYQ","consumerQueue":"inquiryQueue","receivedDeliveryMode":"NON_PERSISTENT"}
If I use the Java Client I can see the correlationId:
{"headers":{},"appId":"XBID","type":"MyOrders","correlationId":[49], ….
------------ EDIT 2 --------------------------------
I have tried with:
getRabbitOperations().convertAndSend(requestExchange, routingKeyInquiry,
_object,
new MessagePostProcessor()
{
public Message postProcessMessage(Message message) throws AmqpException
{
message.getMessageProperties().setUserId(“myUser”);
message.getMessageProperties().setType(“myType”);
message.getMessageProperties().setAppId("myApp");
message.getMessageProperties().setMessageId(counter.incrementAndGet() + "-myType");
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);
message.getMessageProperties().setRedelivered(false);
message.getMessageProperties().setCorrelationIdString(UUID.randomUUID().toString());
return message;
}
});
But the "correlationId" is always null at the server side.
What version are you using?
The return correlation headers have nothing to do with correlationId; they are used to correlate returned (mandatory) requests and replies.
As long as you copy the correlationId and replyTo from the queue.A message to the queue.B message, it should all work ok.
If you can't figure it out, post debug logs from all 3 servers someplace.
EDIT
This works fine for me...
#SpringBootApplication
public class So46316261Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(So46316261Application.class, args).close();
}
#Autowired
private RabbitTemplate template;
#Override
public void run(String... arg0) throws Exception {
Object reply = this.template.convertSendAndReceive("queue.A", "foo");
System.out.println(reply);
Connection conn = this.template.getConnectionFactory().createConnection();
Channel channel = conn.createChannel(false);
RpcClient client = new RpcClient(channel, "", "queue.A");
Response response = client.doCall(new AMQP.BasicProperties.Builder()
.contentType("text/plain")
.deliveryMode(2)
.priority(1)
.userId("guest")
.appId("MyApp")
.replyTo("amq.rabbitmq.reply-to")
.correlationId("bar")
.type("NewOrder")
.build(),
"foo".getBytes());
System.out.println(new String(response.getBody()));
channel.close();
conn.close();
}
#Bean
public Queue queueA() {
return new Queue("queue.A");
}
#Bean
public Queue queueB() {
return new Queue("queue.B");
}
#RabbitListener(queues = "queue.A")
public void listen(Message in) {
System.out.println(in);
this.template.send("queue.B", in);
}
#RabbitListener(queues = "queue.B")
public String listenB(Message in) {
System.out.println(in);
return "FOO";
}
}
(Body:'foo' MessageProperties [headers={}, correlationId=1, replyTo=amq.rabbitmq.reply-to.g2dkABByYWJiaXRAbG9jYWxob3N0AAACyAAAAAAB.hp0xZxgVpXcuj9+5QkcOOw==, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=queue.B, deliveryTag=1, consumerTag=amq.ctag-oanHvT3YyUb_Lajl0gpZSQ, consumerQueue=queue.B])
FOO
(Body:'foo' MessageProperties [headers={}, appId=MyApp, type=NewOrder, correlationId=1, replyTo=amq.rabbitmq.reply-to.g2dkABByYWJiaXRAbG9jYWxob3N0AAACzAAAAAAB.okm02YXf0s0HdqZynVIn2w==, contentType=text/plain, contentLength=0, priority=1, redelivered=false, receivedExchange=, receivedRoutingKey=queue.B, deliveryTag=2, consumerTag=amq.ctag-oanHvT3YyUb_Lajl0gpZSQ, consumerQueue=queue.B])
FOO

Spring Amqp: Mix SimpleRoutingConnectionFactory with #RabbitListener

I have an app that is gonna listen to multiple queues, which are declared on different vhost. I used a SimpleRoutingConnectionFactory to store a connectionFactoryMap, and I hope to set up my listener with #RabbitListener.
According to Spring AMQP doc:
Also starting with version 1.4, you can configure a routing connection
factory in a SimpleMessageListenerContainer. In that case, the list of
queue names is used as the lookup key. For example, if you configure
the container with setQueueNames("foo, bar"), the lookup key will be
"[foo,bar]" (no spaces).
I used #RabbitListener(queues = "some-key"). Unfortunately, spring complained "lookup key [null]". See below.
18:52:44.528 WARN --- [cTaskExecutor-1]
o.s.a.r.l.SimpleMessageListenerContainer : Consumer raised exception,
processing can restart if the connection factory supports it
java.lang.IllegalStateException: Cannot determine target
ConnectionFactory for lookup key [null] at
org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory.determineTargetConnectionFactory(AbstractRoutingConnectionFactory.java:119)
at
org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory.createConnection(AbstractRoutingConnectionFactory.java:97)
at
org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils$1.createConnection(ConnectionFactoryUtils.java:90)
at
org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils.doGetTransactionalResourceHolder(ConnectionFactoryUtils.java:140)
at
org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactoryUtils.java:76)
at
org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.start(BlockingQueueConsumer.java:472)
at
org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1306)
at java.lang.Thread.run(Thread.java:745)
Did I do something wrong? If queues attribute is used as lookup key (for connection factory lookup), what am I supposed to use to specify which queue I'd like to listen to?
Ultimately, I hope to do programmatic/dynamic listener setup. If I use "Programmatic Endpoint Registration", am I supposed to drop "Annotation-driven listener endpoints"? I love "Annotation-driven listener endpoints", because a listener could have multiple message handles with different incoming data type as argument, which is very clean and tidy. If I use Programmatic Endpoint Registration, I would have to parse the Message input variable, and call my a particular custom message handler based on the message type/content.
EDIT:
Hi Gary,
I modified your code #2 a little bit, so that it uses Jackson2JsonMessageConverter to serialize class objects (in RabbitTemplate bean), and use it to un-serialize them back to objects (in inboundAdapter). I also removed #RabbitListener because all listeners would be added at runtime in my case. Now the fooBean can receive integer, string and TestData message without any problem! The only issue left behind is that the program constantly report warning:
"[erContainer#0-1] o.s.a.r.l.SimpleMessageListenerContainer : Consumer raised exception, processing can restart if the connection factory supports it
java.lang.IllegalStateException: Cannot determine target ConnectionFactory for lookup key [null]". For the full stacktrace, please see the bottom.
Did I miss anything?
#SpringBootApplication
public class App2 implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(App2.class, args);
}
#Autowired
private IntegrationFlowContext flowContext;
#Autowired
private ConnectionFactory routingCf;
#Autowired
private RabbitTemplate template;
#Override
public void run(String... args) throws Exception {
// dynamically add a listener for queue qux
IntegrationFlow flow = IntegrationFlows.from(Amqp.inboundAdapter(this.routingCf, "qux").messageConverter(new Jackson2JsonMessageConverter()))
.handle(fooBean())
.get();
this.flowContext.registration(flow).register();
// now test it
SimpleResourceHolder.bind(this.routingCf, "[qux]");
this.template.convertAndSend("qux", 42);
this.template.convertAndSend("qux", "fizbuz");
this.template.convertAndSend("qux", new TestData(1, "test"));
SimpleResourceHolder.unbind(this.routingCf);
}
#Bean
RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(routingCf);
template.setMessageConverter(new Jackson2JsonMessageConverter());
return template;
}
#Bean
#Primary
public ConnectionFactory routingCf() {
SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory();
Map<Object, ConnectionFactory> map = new HashMap<>();
map.put("[foo,bar]", routedCf());
map.put("[baz]", routedCf());
map.put("[qux]", routedCf());
rcf.setTargetConnectionFactories(map);
return rcf;
}
#Bean
public ConnectionFactory routedCf() {
return new CachingConnectionFactory("127.0.0.1");
}
#Bean
public Foo fooBean() {
return new Foo();
}
public static class Foo {
#ServiceActivator
public void handleInteger(Integer in) {
System.out.println("int: " + in);
}
#ServiceActivator
public void handleString(String in) {
System.out.println("str: " + in);
}
#ServiceActivator
public void handleData(TestData data) {
System.out.println("TestData: " + data);
}
}
}
Full stack trace:
2017-03-15 21:43:06.413 INFO 1003 --- [ main] hello.App2 : Started App2 in 3.003 seconds (JVM running for 3.69)
2017-03-15 21:43:11.415 WARN 1003 --- [erContainer#0-1] o.s.a.r.l.SimpleMessageListenerContainer : Consumer raised exception, processing can restart if the connection factory supports it
java.lang.IllegalStateException: Cannot determine target ConnectionFactory for lookup key [null]
at org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory.determineTargetConnectionFactory(AbstractRoutingConnectionFactory.java:119) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory.createConnection(AbstractRoutingConnectionFactory.java:97) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.core.RabbitTemplate.doExecute(RabbitTemplate.java:1430) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.core.RabbitTemplate.execute(RabbitTemplate.java:1411) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.core.RabbitTemplate.execute(RabbitTemplate.java:1387) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.core.RabbitAdmin.initialize(RabbitAdmin.java:500) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.core.RabbitAdmin$11.onCreate(RabbitAdmin.java:419) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.connection.CompositeConnectionListener.onCreate(CompositeConnectionListener.java:33) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.connection.CachingConnectionFactory.createConnection(CachingConnectionFactory.java:571) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils$1.createConnection(ConnectionFactoryUtils.java:90) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils.doGetTransactionalResourceHolder(ConnectionFactoryUtils.java:140) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactoryUtils.java:76) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.listener.BlockingQueueConsumer.start(BlockingQueueConsumer.java:505) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1382) ~[spring-rabbit-1.7.1.RELEASE.jar:na]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_112]
Please show your configuration - it works fine for me...
#SpringBootApplication
public class So42784471Application {
public static void main(String[] args) {
SpringApplication.run(So42784471Application.class, args);
}
#Bean
#Primary
public ConnectionFactory routing() {
SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory();
Map<Object, ConnectionFactory> map = new HashMap<>();
map.put("[foo,bar]", routedCf());
map.put("[baz]", routedCf());
rcf.setTargetConnectionFactories(map);
return rcf;
}
#Bean
public ConnectionFactory routedCf() {
return new CachingConnectionFactory("10.0.0.3");
}
#RabbitListener(queues = { "foo" , "bar" })
public void foobar(String in) {
System.out.println(in);
}
#RabbitListener(queues = "baz")
public void bazzer(String in) {
System.out.println(in);
}
}
Regarding your second question, you could build the endpoint manually but it's quite involved. It's probably easier to use a similar feature in a Spring Integration #ServiceActivator.
I will update this answer with details shortly.
EDIT
And here's the update using Spring Integration techniques to dynamically add a multi-method listener at runtime...
#SpringBootApplication
public class So42784471Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(So42784471Application.class, args);
}
#Autowired
private IntegrationFlowContext flowContext;
#Autowired
private ConnectionFactory routingCf;
#Autowired
private RabbitTemplate template;
#Override
public void run(String... args) throws Exception {
// dynamically add a listener for queue qux
IntegrationFlow flow = IntegrationFlows.from(Amqp.inboundAdapter(this.routingCf, "qux"))
.handle(fooBean())
.get();
this.flowContext.registration(flow).register();
// now test it
SimpleResourceHolder.bind(this.routingCf, "[qux]");
this.template.convertAndSend("qux", 42);
this.template.convertAndSend("qux", "fizbuz");
SimpleResourceHolder.unbind(this.routingCf);
}
#Bean
#Primary
public ConnectionFactory routingCf() {
SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory();
Map<Object, ConnectionFactory> map = new HashMap<>();
map.put("[foo,bar]", routedCf());
map.put("[baz]", routedCf());
map.put("[qux]", routedCf());
rcf.setTargetConnectionFactories(map);
return rcf;
}
#Bean
public ConnectionFactory routedCf() {
return new CachingConnectionFactory("10.0.0.3");
}
#RabbitListener(queues = { "foo" , "bar" })
public void foobar(String in) {
System.out.println(in);
}
#RabbitListener(queues = "baz")
public void bazzer(String in) {
System.out.println(in);
}
#Bean
public Foo fooBean() {
return new Foo();
}
public static class Foo {
#ServiceActivator
public void handleInteger(Integer in) {
System.out.println("int: " + in);
}
#ServiceActivator
public void handleString(String in) {
System.out.println("str: " + in);
}
}
}