Spring boot integration test fails because of missing rabbit factory - java

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

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.

configuring multiple Vhosts in AMQP in rabbitmq configuration spring boot

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.

How to use multiple vhosts in a Spring RabbitMQ project?

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

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