configuring multiple Vhosts in AMQP in rabbitmq configuration spring boot - java

i'm implementing a project where i have to send messages across different vhosts in rabbitmq. using SimpleRoutingConnectionFactory but get java.lang.IllegalStateException: Cannot determine target ConnectionFactory for lookup key [null].
Anyone who has an idea how to implement such below is my configuration class code.
#Configuration
#EnableRabbit
public class RabbitMQConfiguration {
#Autowired
ConnectionProperties connect;
// client1 exchanges
#Bean
public TopicExchange client1Exchange() {
TopicExchange ex = new TopicExchange("ex_client1");
ex.setAdminsThatShouldDeclare(client1());
return ex;
}
// client2 exchange
#Bean
public TopicExchange client2Exchange() {
TopicExchange ex = new TopicExchange("ex_client2");
ex.setAdminsThatShouldDeclare(client2Admin());
return ex;
}
#Bean
public Queue client1Queue() {
Queue queue = new Queue("client1_queue");
queue.setAdminsThatShouldDeclare(client1());
return queue;
}
#Bean
public Binding client1Binding() {
Binding binding = BindingBuilder.bind(client1Queue())
.to(client1Exchange())
.with("client1_key");
binding.setAdminsThatShouldDeclare(client1());
return binding;
}
#Bean
public Queue client2Queue() {
Queue queue = new Queue("client2_queue");
queue.setAdminsThatShouldDeclare(client2());
return queue;
}
#Bean
public Binding client2Binding() {
Binding binding = BindingBuilder.bind(client2Queue())
.to(client2Exchange())
.with("client2_key");
binding.setAdminsThatShouldDeclare(client2());
return binding;
}
#Bean
#Primary
public ConnectionFactory connectionFactory() {
SimpleRoutingConnectionFactory connectionFactory = new SimpleRoutingConnectionFactory();
Map<Object, ConnectionFactory> targetConnectionFactories = new HashMap<>();
targetConnectionFactories.put("client1", client1ConnectionFactory());
targetConnectionFactories.put("client2", client2ConnectionFactory());
connectionFactory.setTargetConnectionFactories(targetConnectionFactories);
return connectionFactory;
}
#Bean
public ConnectionFactory client1ConnectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(connect.getRabbitMQHost());
connectionFactory.setVirtualHost(connect.getRabbitMQClient1VHost());
connectionFactory.setUsername(connect.getRabbitMQClient1User());
connectionFactory.setPassword(connect.getRabbitMQClient1Pass());
return connectionFactory;
}
#Bean
public ConnectionFactory client2ConnectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(connect.getRabbitMQHost());
connectionFactory.setVirtualHost(connect.getRabbitMQClient2VHost());
connectionFactory.setUsername(connect.getRabbitClient2User());
connectionFactory.setPassword(connect.getRabbitClient2Pass());
return connectionFactory;
}
// You can comment all methods below and remove interface's implementation to use the default serialization / deserialization
#Bean
public RabbitTemplate rabbitTemplate() {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
return rabbitTemplate;
}
#Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
return new Jackson2JsonMessageConverter();
}
#Bean
public MappingJackson2MessageConverter consumerJackson2MessageConverter() {
return new MappingJackson2MessageConverter();
}
#Bean
public DefaultMessageHandlerMethodFactory messageHandlerMethodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setMessageConverter(consumerJackson2MessageConverter());
return factory;
}
#Bean
public TaskExecutor rabbitListenerExecutor() {
int threads = Integer.valueOf(connect.getMinConsumers()) * 2; // threads = min consumers* no of queues
final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(threads);
executor.setMaxPoolSize(threads);
executor.setThreadNamePrefix("RabbitThreadListener");
executor.afterPropertiesSet();
return executor;
}
#Bean
public SimpleRabbitListenerContainerFactory myRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setConcurrentConsumers(Integer.valueOf(connect.getMinConsumers()));
factory.setPrefetchCount(Integer.valueOf(connect.getPrefetchCount()));
factory.setTaskExecutor(rabbitListenerExecutor());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
#Bean
public RabbitAdmin client1() {
RabbitAdmin rabbitAdmin = new RabbitAdmin(client1ConnectionFactory());
rabbitAdmin.afterPropertiesSet();
return rabbitAdmin;
}
#Bean
public RabbitAdmin client2() {
RabbitAdmin rabbitAdmin = new RabbitAdmin(client2ConnectionFactory());
rabbitAdmin.afterPropertiesSet();
return rabbitAdmin;
}
}
i'm getting this stacktrace
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:505)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1335)
at java.lang.Thread.run(Thread.java:748)

The RoutingConnectionFactory is generally used for publishing messages.
When using a routing factory in a listener container you must configure the lookup key to match the queue name(s) configured in the container.
From the documentation:
Also starting with version 1.4, you can configure a routing connection factory in a listener container. 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).
So; if a RabbitListener listens to queue foo the routing lookup key must be [foo]. (You can add the same CF multiple times with different keys).
Or you can simply create multiple container factories, with each getting a concrete CF instead of the routing CF.
EDIT
Let's say you have
#RabbitListener(queues = "myQueue", connectionFactory = "myRabbitListenerContainerFactory")
public void listen(...) {
...
}
If myQueue is in client1's vhost, then you need an entry in the router CF map thus...
targetConnectionFactories.put("[myQueue]", client1ConnectionFactory());
...because the listener container generated for the listener will use the queue name in its lookup key.
Alternatively, create 2 container factories; each wired directly with client1 and client2 CFs instead of the routing CF...
#Bean
public SimpleRabbitListenerContainerFactory client1ListenerContainerFactory() {
#Bean
public SimpleRabbitListenerContainerFactory client2ListenerContainerFactory() {
and
#RabbitListener(queues = "myQueue", connectionFactory = "client1ListenerContainerFactory")
public void listen(...) {
...
}
i.e. don't use the routing CF at all for listeners - containers only have one connection.

Related

How to make #JmsListener transactional (with Spring Boot and Atomikos)?

I have a Spring Boot application which consumes messages from queue (ActiveMQ) and writes them to the database (DB2) and I need it to be fully transactional. I got to a point where I understood that transaction manager (using spring-boot-starter-jta-atomikos) is a best solution for distributed transactions and I'm trying to correctly implement it.
JMS configuration class:
#EnableJms
#Configuration
public class MQConfig {
#Bean
public ConnectionFactory connectionFactory() {
RedeliveryPolicy rp = new RedeliveryPolicy();
rp.setMaximumRedeliveries(3);
rp.setRedeliveryDelay(1000L);
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory();
cf.setBrokerURL("tcp://localhost:61616");
cf.setRedeliveryPolicy(rp);
return cf;
}
#Bean
public JmsTemplate jmsTemplate() {
JmsTemplate template = new JmsTemplate(connectionFactory());
template.setConnectionFactory(connectionFactory());
return template;
}
#Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setCacheLevelName("CACHE_CONSUMER");
factory.setReceiveTimeout(1000L);
factory.setSessionTransacted(true);
return factory;
}
}
JMS listener class:
#Component
public class MQListener {
#Autowired
private ImportRecordsService importRecordsService;
#JmsListener(
containerFactory = "jmsListenerContainerFactory",
destination = "test.queue"
// concurrency = "4-10"
)
public void receiveMessage(TextMessage message) throws JMSException {
importRecordsService.createRecord();
}
}
Service class that writes to DB:
#Service
public class ImportRecordsService {
#Autowired
private ImportRecordsDAO dao;
#Transactional
public void createRecord() {
ImportRecord record = new ImportRecord();
record.setDateCreated(LocalDateTime.now());
record.setName("test-001");
dao.save(record);
}
}
If exception is thrown inside createRecord() after save, rollback works as it should. When an exception is thrown inside JMS listener in receiveMessage() after save, message is returned to queue but database record stays.
Any help greatly appreciated.
This should be as simple as adding your transactionManager to your DefaultJmsListenerContainerFactory.
In this case add the PlatformTransactionManager (should be an available Spring Bean) to your jmsListenerContainerFactory method signature and then call
factory.setTransactionManager(jtaTransactionManager).
#Bean
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, PlatformTransactionManager jtaTransactionManager) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setCacheLevelName("CACHE_CONSUMER");
factory.setReceiveTimeout(1000L);
factory.setTransactionManager(jtaTransactionManager);
factory.setSessionTransacted(true);
return factory;
}
Although you will need to note that setting the transactionManager will reset your cache level to CACHE_NONE.

Use JMSListener with RabbitMQ

My Application currently uses IBM MQ and has queue config setup and working fine with JMS. e.g.
#EnableJms
#Configuration
public class IBMQueueConfig {
#Bean("defaultContainer")
public JmsListenerContainerFactory containerFactory(final ConnectionFactory connectionFactory,
final ErrorHandler errorHandler) {
final DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setErrorHandler(errorHandler);
return factory;
}
}
I can receive message and process as follows:
#Service
public class ProcessMessageReceive {
#JmsListener(destination = "${queue}", concurrency = "${threads}", containerFactory = "defaultContainer")
public Message processMessage(#Payload final String message) {
//do stuff
}
}
I need to use RabbitMQ for testing and require additional configuration. I have the following the class:
#Configuration
#ConfigurationProperties(prefix = "spring.rabbitmq")
#EnableRabbit
public class RabbitMQConfiguration {
private String host;
private int port;
private String username;
private String password;
private String virtualHost;
#Bean
public DirectExchange exchange() {
return new DirectExchange(exchange);
}
#Bean("defaultContainer")
public JmsListenerContainerFactory containerFactory(#Qualifier("rabbit-connection-factory") final ConnectionFactory connectionFactory) {
final DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(); //ERROR
return factory;
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(#Qualifier("rabbit-connection-factory") final ConnectionFactory connectionFactory,
#Value("spring.rabbitmq.listener.simple.concurrency") final int concurrency,
#Value("spring.rabbitmq.listener.simple.max-concurrency") final int maxConcurrency) {
final SimpleRabbitListenerContainerFactory containerFactory = new SimpleRabbitListenerContainerFactory();
containerFactory.setConnectionFactory(connectionFactory);
containerFactory.setConcurrentConsumers(concurrency);
containerFactory.setMaxConcurrentConsumers(maxConcurrency);
containerFactory.setDefaultRequeueRejected(false);
return containerFactory;
}
#Bean(name = "rabbit-connection-factory")
public ConnectionFactory connectionFactory() {
final CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
return connectionFactory;
}
#Bean
public Queue inboundQueue() {
return new Queue(fixInboundQueue, true);
}
#Bean
public Binding inboundQueueBinding() {
return bind(inboundQueue())
.to(exchange())
.with(routingKey);
}
}
I get an error on line factory.setConnectionFactory(connectionFactory); as it expects a javax.jms.ConnectionFactory but provided is Rabbit MQ One.
Is there a way I can wire in the Rabbit MQ ConnectionFactory ? I know it is possible if I use RMQConnectionFactory, but I am looking to see If I can achieve it with Spring Rabbit dependency.
The objective is to avoid writing another processMessage() specifically for the Rabbit MQ and re-use what I already have.
Alternatively, can I use both annotations? In which case I would use spring profile to enable the one I need depending on prod or test?
#RabbitListener(queues = "${app.rabbitmq.queue}")
#JmsListener(destination = "${queue}", concurrency = "${threads}", containerFactory = "defaultContainer")
public Message processMessage(#Payload final String message) {
//do stuff
}
You have to use #RabbitListener instead of #JmsListener if you want to talk to RabbitMQ over AMQP.
You can add both annotations if you want to use JMS in production and RabbitMQ in tests.

How to create dynamic queues in rabbit mq using spring boot?

I need some help.
I'm developing a spring boot application, and I want wo publish messages to a rabbitMQ. I want to send it to a queue, that is named in the message itself. This way i want to create queues dynamicly.
I only found examples that use a "static" queue.
I have reserched some things but didn't find anything.
I'm new to RabbitMQ and learned the basic concepts.
I'm also fairly new to spring.
RabbotMQ Config
#Configuration
public class RabbitMQConfig {
#Value("amq.direct")
String exchange;
#Value("queue-name") // Don't want to do this
String queueName;
#Value("routing-key") // Or this
String routingkey;
#Bean
Queue queue() {
return new Queue(queueName, true);
}
#Bean
DirectExchange exchange() {
return new DirectExchange(exchange);
}
#Bean
Binding binding(Queue queue, DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(routingkey);
}
#Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
#Bean
public AmqpTemplate template(ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jsonMessageConverter());
return rabbitTemplate;
}
}
MessageSender
#Service
public class RabbitMQSender {
#Autowired
private AmqpTemplate template;
#Value("amq.direct")
private String exchange;
public void send(MessageDTO message) {
template.convertAndSend(exchange, message);
}
}
I came to a solution:
You need to create a AmqpAdmin in your config:
#Bean
public AmqpAdmin amqpAdmin() {
return new RabbitAdmin(connectionFactory);
}
Then you add it to your service:
#Autowired
private AmqpAdmin admin;
Finally you can use it to create queues and bindings.
Queue queue = new Queue(queueName, durable, false, false);
Binding binding = new Binding(queueName, Binding.DestinationType.QUEUE, EXCHANGE, routingKey, null);
admin.declareQueue(queue);
admin.declareBinding(binding);
I found the solution here
Not sure which version of RabbitMQ you were on but, your original code was close. This works, too.
#Bean
Queue fanoutQueue() {
// empty name, durable false, exclusive false, autoDelete false
return new Queue("", false, false, true);
}
#Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange("fanout-exchange", true, false);
}
#Bean
Binding fanoutBinding(Queue fanoutQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
}

Spring boot integration test fails because of missing rabbit factory

I have an application that uses rabbitmq, and consumes message. I want to write an integration test to check all the features.My config is below:
#SpringBootApplication(scanBasePackages = {"com.mysite.domaintools", "com.mysite.core",
"com.mysite.database.repository"})
#EntityScan("com.mysite.database.domain")
#EnableMongoRepositories(basePackages = {"com.mysite.database.repository.mongo"})
#EnableJpaRepositories("com.mysite.database.repository") #EnableRabbit
public class DomaintoolsApplication {
private static final String topicExchangeName = "mysite";
private static final String queueName = Queues.DOMAINTOOLS.getName();
#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("domaintools.key.#");
}
#Bean SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(queueName);
container.setMessageListener(listenerAdapter);
return container;
}
#Bean MessageListenerAdapter listenerAdapter(DomainToolsRabbitReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
public static void main(String[] args) {
SpringApplication.run(DomaintoolsApplication.class, args);
}
}
Everything is fine when I run my application, but when I try to run the folowing test:
#RunWith(SpringRunner.class)
#DataJpaTest
//#SpringBootTest
public class DomainToolsWorkerIT {
#Autowired
private DomainRepository domainRepository;
#Test
public void test(){
System.out.println("");
}
}
I get exception that rabbit connection factory was not found! But I am not supposed to init it because spring boot should do it. It says that no candidates found for ConnectionFactory bean, expected at least one. How may I write test in app that uses rabbitmq?
You need to annotate your test class with EnableRabbit :
and add RabbitTemplate with it's ConnectionFactory using different mock object:
mock factory, connection and channel.
#RunWith(SpringRunner.class)
#DataJpaTest
#SpringBootTest(classes = DomaintoolsApplication.class)
#EnableRabbit
public class DomainToolsWorkerIT {
#Autowired
private DomainRepository domainRepository;
/**
* Create test rabbit template to not load a real rabbitMQ instance.
*
* #return rabbit template.
*/
#Bean
public RabbitTemplate template() {
return new RabbitTemplate(connectionFactory());
}
/**
* Connection factory mock to create rabbit template.
*
* #return connection factory mock.
*/
#Bean
public ConnectionFactory connectionFactory() {
ConnectionFactory factory = mock(ConnectionFactory.class);
Connection connection = mock(Connection.class);
Channel channel = mock(Channel.class);
doReturn(connection).when(factory).createConnection();
doReturn(channel).when(connection).createChannel(anyBoolean());
doReturn(true).when(channel).isOpen();
return factory;
}
#Test
public void test(){
System.out.println("");
}
}

How to setup multiple topics in a RabbitMQ Java config class using Spring Framework?

I'm trying to create a RabbitMQ configuration class using Spring Framework. The documentation does not say anything on how to setup multiple topics in a TopicExchange. How do I do that? So far, I have this Java code but I'm not clear on how to setup multiple topics in the binding method below since it only returns one binding. Would I not need multiple bindings if I need multiple topics?
#Configuration
#EnableRabbit
public class MessageReceiverConfiguration {
final static String queueName = "identity";
final static String topic1 = "NewUserSignedUp";
final static String topic2 = "AccountCreated";
#Autowired
RabbitTemplate rabbitTemplate;
#Bean
Queue queue() {
return new Queue(queueName, false);
}
#Bean
TopicExchange exchange() {
return new TopicExchange("DomainEvents");
}
#Bean
Binding binding(Queue queue, TopicExchange exchange) {
// How to setup multiple topics?
return BindingBuilder.bind(queue).to(exchange).with(topic1);
}
#Bean
SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(queueName);
container.setMessageListener(listenerAdapter);
container.setAcknowledgeMode(AcknowledgeMode.AUTO);
return container;
}
#Bean
MessageReceiver receiver() {
return new MessageReceiver();
}
#Bean
MessageListenerAdapter listenerAdapter(MessageReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
}
You can define multiple binding by changing Binding function to return a list instead of a single Binding object.
#Bean
List<Binding> bindings() {
return Arrays.AsList(BindingBuilder.bind(queue()).to(exchange()).with(topic1),
BindingBuilder.bind(queue()).to(exchange()).with(topic2));
}
Tip: You do not need to pass queue and exchange as method params. You can directly refer the bean methods to pass the information of exchange and queue.
Refer documentation for more details.
List<Binding> bindings() not supported by spring boot 2.+ versions.
This one works;
#Bean
public Declarables bindings() {
return new Declarables(
BindingBuilder
.bind(bookingAddQueue())
.to(bookingExchange())
.with("add")
.noargs(),
BindingBuilder
.bind(bookingEditQueue())
.to(bookingExchange())
.with("edit")
.noargs());
}

Categories

Resources