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