How to use multiple vhosts in a Spring RabbitMQ project? - java

I've the following two Configuration classes:
#Configuration
#EnableRabbit
#Import({ LocalRabbitConfigA.class, CloudRabbitConfigA.class })
public class RabbitConfigA {
#Autowired
#Qualifier("rabbitConnectionFactory_A")
private ConnectionFactory rabbitConnectionFactory;
#Bean(name = "admin_A")
AmqpAdmin amqpAdmin() {
return new RabbitAdmin(rabbitConnectionFactory);
}
#Bean(name = "Exchange_A")
DirectExchange receiverExchange() {
return new DirectExchange("Exchange_A", true, false);
}
}
And
#Configuration
#EnableRabbit
#Import({ LocalRabbitConfigB.class, CloudRabbitConfigB.class })
public class RabbitConfigB {
#Autowired
#Qualifier("rabbitConnectionFactory_B")
private ConnectionFactory rabbitConnectionFactory;
#Bean(name = "admin_B")
AmqpAdmin amqpAdmin() {
return new RabbitAdmin(rabbitConnectionFactory);
}
#Bean(name = "Exchange_B")
DirectExchange receiverExchange() {
return new DirectExchange("Exchange_B", true, false);
}
}
Note that the LocalRabbitConfigA and LocalRabbitConfigB classes define the connectionFactory which connects to a different VHost.
When starting the application (within Tomcat), all the Exchanges are created in both VHosts.
The question is how to define that a certain Exchange/Queue is created by a specific ConnectionFactiory ?
So that VHost A contains only the Exchange_A, and VHost B only Exchange_B ?

See conditional declaration.
Specifically:
#Bean(name = "Exchange_B")
DirectExchange receiverExchange() {
DirectExchange exchange = new DirectExchange("Exchange_B", true, false);
exchange.setAdminsThatShouldDeclare(amqpAdmin());
return exchange;
}

We can achieve this using SimpleRoutingConnectionFactory, where we create multiple connection factories each for a vhost and configure it to SimpleRoutingConnectionFactory.
From the spring documentation: spring doc
public class MyService {
#Autowired
private RabbitTemplate rabbitTemplate;
public void service(String vHost, String payload) {
SimpleResourceHolder.bind(rabbitTemplate.getConnectionFactory(), vHost);
rabbitTemplate.convertAndSend(payload);
SimpleResourceHolder.unbind(rabbitTemplate.getConnectionFactory());
}
}
I have created a git repo showing how to do this: spring-boot-amqp-multiple-vhosts

Related

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);
}

Listen message queue SQS with Spring Boot not works with standard config

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")

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("");
}
}

Cannot autowire bean in MessageListener Class AMQP

Ok. So i have a rabbit configuration class with some constants and I try to add a service to my listenercontainer listener.
#Configuration
public class RabbitConfig {
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(HOST);
connectionFactory.setPort(CONN_PORT);
connectionFactory.setUsername(USERNAME);
connectionFactory.setPassword(PASSWORD);
return connectionFactory;
}
#Bean
public AmqpAdmin amqpAdmin() {
return new RabbitAdmin(connectionFactory());
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setReplyQueue(replyQueue());
rabbitTemplate.setCorrelationKey(UUID.randomUUID().toString());
return rabbitTemplate;
}
#Bean
public Queue replyQueue() {
return new Queue(REPLY_QUEUE_NAME);
}
#Bean
public SimpleMessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
container.setQueueNames(QUEUE_NAME);
container.setMessageListener(messageListener());
return container;
}
#Bean
public MessageListener messageListener(){
return new RabbitListener();
}
}
I am trying to inject into the messagelistener which is created in the last lines a service from my project. This triggers an error of cannot autowire field as if the field is not managed by Spring. I did some research and I verified my component scan package and it's set to all the project, I have annotated the rabbitlistener with #Component so I can't really find the mistake or why Spring cannot autowire the field in my listener class. Here is the code.
#Component
public class RabbitListener implements MessageListener {
Logger logger = LoggerFactory.getLogger(this.getClass());
#Autowired
ImagesService imagesService;
#Override
public void onMessage(Message message) {
//processing message
}
Any ideas please?
Would be better if you'd share the full StackTrace, but I suggest you do something like this:
Add #ComponentScan for your #Configuration class and specify there those packages where are your RabbitListener and ImagesService classes
Mark the last two with #Component (Yes, I see that on your RabbitListener, but it isn't clear where your ImagesService and how it is going around it)
Than #Autowire RabbitListener to that RabbitConfig instead of #Bean for it.
And be careful with the #Component and #Bean mix: you end up with two bean, if you have #ComponentScan for that package, of course.
Okay, u need to #Autowire the RabbitListener bean. Since the RabbitListener is a bean that need to be managed by the IOC, since is declared #Component, hence #runtime the RabbitListener is not in the context, hence autowire it in the config class like so
#Configuration
public class RabbitConfig {
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(HOST);
connectionFactory.setPort(CONN_PORT);
connectionFactory.setUsername(USERNAME);
connectionFactory.setPassword(PASSWORD);
return connectionFactory;
}
#Bean
public AmqpAdmin amqpAdmin() {
return new RabbitAdmin(connectionFactory());
}
#Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setReplyQueue(replyQueue());
rabbitTemplate.setCorrelationKey(UUID.randomUUID().toString());
return rabbitTemplate;
}
#Bean
public Queue replyQueue() {
return new Queue(REPLY_QUEUE_NAME);
}
#Bean
public SimpleMessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
container.setQueueNames(QUEUE_NAME);
container.setMessageListener(rabbitListener); // reference the autowired RabbitListener on this line
return container;
}
#Autowire
private RabbitListener rabbitListener;
}
That should resolve this error.

Categories

Resources