How to listen to multiple queues simultaneously on same queue manager? - spring-boot

I want to listen to multiple (2) queues on the same queue manager from my spring boot (kotlin) app. I have the following MQ connection factory but not sure how to add the listeners for my 2 queues.
#Configuration
open class MqConfig {
#Value("\${jms.mq.port}")
private var port: Int = 0
#Value("\${jms.mq.channel}")
private var channel: String? = null
#Value("\${jms.mq.host}")
private var host: String? = null
#Value("\${jms.mq.manager}")
private var queueManager: String? = null
#Value("\${jms.mqA.queue}")
private var jmsMqA: String? = null
#Value("\${jms.mqB.queue}")
private var jmsMqB: String? = null
#Bean
open fun jmsMQConnectionFactory(): MQQueueConnectionFactory {
val mqQueueConnectionFactory = MQQueueConnectionFactory()
mqQueueConnectionFactory.port = port
mqQueueConnectionFactory.channel = channel
mqQueueConnectionFactory.hostName = host
mqQueueConnectionFactory.queueManager = queueManager
mqQueueConnectionFactory.transportType = WMQConstants.WMQ_CM_CLIENT;
mqQueueConnectionFactory.sslSocketFactory = sslSocketfactory;
}
As you can see in the code above, the two queues I want to listen to are jmsMqA and jmsMqB but not sure where/how to use them.
I am new to queues and MQ, so if anyone can point me in the right direction on how to implement a listener and (possibly) publisher for those 2 queues, that will be really helpful.

You can use #JmsListener to listen the queues. Here is the link.
For example:
#Component
class QueueAListener {
#JmsListener(
destination = "\${jms.mqA.queue}",
containerFactory = "jmsMQContainerFactory"
)
fun onMessage(message: Message) {
// yours logic
}
}
For queue B you can create new component or add method to the same class(don't forget to change method name).
UPD:
Container factory:
#Bean(name = "jmsMQContainerFactory")
public JmsListenerContainerFactory<?> topicJmsListenerContainerFactory() {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setPubSubDomain(true);
factory.setConnectionFactory(jmsMQConnectionFactory());
return factory;
}
You can use 'JmsTemplate' to send messages to queues. Please, check this manual

Note: This answer is based on the sample in https://github.com/ibm-messaging/mq-dev-patterns/tree/master/Spring-JMS/src/main/java/com/ibm/mq/samples/jms/spring/level114
Your jmsMQConnectionFactory bean should look something like (this is java)
import com.ibm.mq.jms.MQConnectionFactory;
import com.ibm.mq.samples.jms.spring.globals.handlers.OurDestinationResolver;
import com.ibm.mq.samples.jms.spring.globals.handlers.OurMessageConverter;
import com.ibm.mq.spring.boot.MQConfigurationProperties;
import com.ibm.mq.spring.boot.MQConnectionFactoryFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.QosSettings;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
#Configuration
public class MQConfiguration114 {
protected final Log logger = LogFactory.getLog(getClass());
#Bean
public MQConnectionFactory mqConnectionFactory() throws JMSException {
MQConfigurationProperties properties = new MQConfigurationProperties();
// Properties will be a mix of defaults, and those found in application.properties
// under ibm.mq
// Here we can override any of the properties should we need to
MQConnectionFactoryFactory mqcff = new MQConnectionFactoryFactory(properties,null);
MQConnectionFactory mqcf = mqcff.createConnectionFactory(MQConnectionFactory.class);
return mqcf;
}
#Bean
public JmsListenerContainerFactory<?> myContainerFactory114() throws JMSException {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(mqConnectionFactory());
// ... any customisations go here
return factory;
}
// If you are going to be sending messages then
#Bean("myJmsTemplate114")
public JmsTemplate myJmsTemplate114() throws JMSException {
JmsTemplate jmsTemplate = new JmsTemplate(mqConnectionFactory());
// .. Any customisations go here
return jmsTemplate;
}
}
Your listener code would look something like:
package com.ibm.mq.samples.jms.spring.level114;
import com.ibm.mq.samples.jms.spring.globals.data.OurData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
//#Component
public class MessageConsumer114 {
protected final Log logger = LogFactory.getLog(getClass());
#JmsListener(destination = "${queue.name1}", containerFactory = "myContainerFactory114")
public void receiveRequest(OurData message) {
logger.info("");
logger.info( this.getClass().getSimpleName());
logger.info("Received message of type: " + message.getClass().getSimpleName());
logger.info("Received message :" + message);
}
}
#JmsListener(destination = "${queue.name2}", containerFactory = "myContainerFactory114")
public void receiveRequest(OurData message) {
logger.info("");
logger.info( this.getClass().getSimpleName());
logger.info("Received message of type: " + message.getClass().getSimpleName());
logger.info("Received message :" + message);
}
}
Your sender logic would look like:
import com.ibm.mq.samples.jms.spring.globals.data.OurData;
import com.ibm.mq.samples.jms.spring.globals.handlers.OurMessageConverter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
//#Service
public class SendMessageService114 {
protected final Log logger = LogFactory.getLog(getClass());
#Value("${queue.name1}")
public String sendQueue1;
#Value("${queue.name2}")
public String sendQueue2;
final private JmsTemplate myJmsTemplate114;
final private OurMessageConverter ourConverter = new OurMessageConverter();
SendMessageService114(JmsTemplate myJmsTemplate114) {
this.myJmsTemplate114 = myJmsTemplate114;
}
public void send1(OurData msg) {
logger.info("Sending Message");
myJmsTemplate114.convertAndSend(sendQueue1, msg);
}
public void send2(OurData msg) {
logger.info("Sending Message");
myJmsTemplate114.convertAndSend(sendQueue2, msg);
}
}

Related

MQTT and Spring Boot Integration - When Connection lost

I am trying to using Spring MQTT Integration to build a client that is subscribe to MQTT broker. The code works as expected, no issues. I am struggling configuring it so that when the connection is lost, it subscribes automatically. What is happening now, is that when it disconnects, the connection is established but no is not subscribed anymore to my topic.
What should I do to capture the event correctly, and resubscribe again when connection is lost?
Here is my configuration
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.ConsumerStopAction;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
#Configuration
public class MqttBeans {
Logger logger = LoggerFactory.getLogger(MqttBeans.class);
#Bean
public MqttConnectOptions mqttConnectOptions() {
MqttConnectOptions options = new MqttConnectOptions();
options.setServerURIs(new String[] { "ssl://URL:8883" });
options.setUserName("ubidot_bridge");
String pass = "PASS";
options.setPassword(pass.toCharArray());
options.setCleanSession(false);
options.setAutomaticReconnect(true);
options.setConnectionTimeout(30);
options.setKeepAliveInterval(90);
options.setMqttVersion(MqttConnectOptions.MQTT_VERSION_3_1_1);
return options;
}
#Bean
public MqttPahoClientFactory mqttClientFactory(MqttConnectOptions options) {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setConnectionOptions( options );
factory.setConsumerStopAction(ConsumerStopAction.UNSUBSCRIBE_NEVER);
logger.info("Reconnected to the broker");
return factory;
}
#Bean
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
#Bean
public MqttPahoMessageDrivenChannelAdapter mqttPahoMessageDrivenChannelAdapterConfig(MqttConnectOptions options) {
MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("ubidot_bridge_in",
mqttClientFactory(options), "#");
adapter.setCompletionTimeout(5000);
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(2);
adapter.setOutputChannel(mqttInputChannel());
logger.info("Setting up inbound channel");
return adapter;
}
#Bean
public MessageProducer inbound(MqttPahoMessageDrivenChannelAdapter adapter) {
return adapter;
}
#Bean
#ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
logger.info("Setting up msg receiver handler");
return new MessageHandler() {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
logger.info("Msg received .. Topic: " + topic);
logger.info("Payload " + message.getPayload());
System.out.println();
}
};
}
#Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
#Bean
#ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound( MqttConnectOptions options ) {
// clientId is generated using a random number
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler("ubidot_bridge_out", mqttClientFactory(options));
messageHandler.setAsync(true);
messageHandler.setDefaultTopic("#");
messageHandler.setDefaultRetained(false);
return messageHandler;
}
}
Thank you in advance for the help
T.

spring batch integration configuration with azure service bus

I am trying to configure inbound and outbound adaptors as provided in the spring batch remote partitioning samples for Manager and worker beans. Having difficulty since they are configured in context of AMQPConnectionFactory.
However when I follow spring integration samples, there is no class which can provide Connection Factory. Help appreciated.
Below is sample code:-
import com.microsoft.azure.spring.integration.core.DefaultMessageHandler;
import com.microsoft.azure.spring.integration.core.api.CheckpointConfig;
import com.microsoft.azure.spring.integration.core.api.CheckpointMode;
import com.microsoft.azure.spring.integration.servicebus.inbound.ServiceBusQueueInboundChannelAdapter;
import com.microsoft.azure.spring.integration.servicebus.queue.ServiceBusQueueOperation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.integration.partition.RemotePartitioningManagerStepBuilderFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.util.concurrent.ListenableFutureCallback;
#Configuration
#IntegrationComponentScan
public class ManagerConfiguration {
private static final int GRID_SIZE = 3;
private static final String REQUEST_QUEUE_NAME = "digital.intg.batch.cm.request";
private static final String REPLY_QUEUE_NAME = "digital.intg.batch.cm.reply";
private static final String MANAGER_INPUT_CHANNEL = "manager.input";
private static final String MANGER_OUTPUT_CHANNEL = "manager.output";
private static final Log LOGGER = LogFactory.getLog(ManagerConfiguration.class);
private final JobBuilderFactory jobBuilderFactory;
private final RemotePartitioningManagerStepBuilderFactory managerStepBuilderFactory;
public ManagerConfiguration(JobBuilderFactory jobBuilderFactory,
RemotePartitioningManagerStepBuilderFactory managerStepBuilderFactory
) {
this.jobBuilderFactory = jobBuilderFactory;
this.managerStepBuilderFactory = managerStepBuilderFactory;
}
/*
* Configure outbound flow (requests going to workers)
*/
#Bean( name = MANGER_OUTPUT_CHANNEL )
public DirectChannel managerRequests() {
return new DirectChannel();
}
/*
* Configure inbound flow (replies coming from workers)
*/
#Bean( name = MANAGER_INPUT_CHANNEL )
public DirectChannel managerReplies() {
return new DirectChannel();
}
#Bean
public ServiceBusQueueInboundChannelAdapter managerQueueMessageChannelAdapter(
#Qualifier( MANAGER_INPUT_CHANNEL ) MessageChannel inputChannel, ServiceBusQueueOperation queueOperation) {
queueOperation.setCheckpointConfig(CheckpointConfig.builder().checkpointMode(CheckpointMode.MANUAL).build());
ServiceBusQueueInboundChannelAdapter adapter = new ServiceBusQueueInboundChannelAdapter(REPLY_QUEUE_NAME,
queueOperation);
adapter.setOutputChannel(inputChannel);
return adapter;
}
#Bean
#ServiceActivator( inputChannel = MANGER_OUTPUT_CHANNEL )
public MessageHandler managerQueueMessageSender(ServiceBusQueueOperation queueOperation) {
DefaultMessageHandler handler = new DefaultMessageHandler(REQUEST_QUEUE_NAME, queueOperation);
handler.setSendCallback(new ListenableFutureCallback<Void>() {
#Override
public void onSuccess(Void result) {
LOGGER.info("Manager Request Message was sent successfully.");
}
#Override
public void onFailure(Throwable ex) {
LOGGER.info("There was an error sending request message to worker.");
}
});
return handler;
}
#Bean
public IntegrationFlow managerOutboundFlow(MessageHandler managerQueueMessageSender) {
return IntegrationFlows
.from(managerRequests())
.handle(managerQueueMessageSender)
.get();
}
#Bean
public IntegrationFlow managerInboundFlow(ServiceBusQueueInboundChannelAdapter managerQueueMessageChannelAdapter) {
return IntegrationFlows
.from(managerQueueMessageChannelAdapter)
.channel(managerReplies())
.get();
}
/*
* Configure the manager step
*/
#Bean
public Step managerStep() {
return this.managerStepBuilderFactory.get("managerStep")
.partitioner("workerStep", new BasicPartitioner())
.gridSize(GRID_SIZE)
.outputChannel(managerRequests())
.inputChannel(managerReplies())
//.aggregator()
.build();
}
#Bean
public Job remotePartitioningJob() {
return this.jobBuilderFactory.get("remotePartitioningJob")
.start(managerStep())
.build();
}
}
The sample uses ActiveMQ because it is easily embeddable in a JVM for our tests and samples. But you can use any other broker that you want.
?? what should I inject here?
You should inject any dependency required by the queueMessageChannelAdapter handler:
.handle(queueMessageChannelAdapter)

Need an example on Embedded kafka with camel

I am facing issue while testing kafka with camel. I used Embedded kafka with camel and here's what I tried
I have tried this example which tells us about testing kafka using embedded kafka
https://codenotfound.com/spring-kafka-embedded-unit-test-example.html
package com.codenotfound.kafka.producer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThat;
import static org.springframework.kafka.test.assertj.KafkaConditions.key;
import static org.springframework.kafka.test.hamcrest.KafkaMatchers.hasValue;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.main.Main;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.listener.KafkaMessageListenerContainer;
import org.springframework.kafka.listener.MessageListener;
import org.springframework.kafka.test.rule.EmbeddedKafkaRule;
import org.springframework.kafka.test.utils.ContainerTestUtils;
import org.springframework.kafka.test.utils.KafkaTestUtils;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
#RunWith(SpringRunner.class)
#SpringBootTest
#DirtiesContext
public class SpringKafkaSenderTest {
private static final Logger LOGGER = LoggerFactory.getLogger(SpringKafkaSenderTest.class);
private static String SENDER_TOPIC = "sender.t";
#Autowired
private Sender sender;
private KafkaMessageListenerContainer<String, String> container;
private BlockingQueue<ConsumerRecord<String, String>> records;
Object groupId;
Object bootstrapServers;
#ClassRule
public static EmbeddedKafkaRule embeddedKafka = new EmbeddedKafkaRule(1, true, SENDER_TOPIC);
#Before
public void setUp() throws Exception {
// set up the Kafka consumer properties
Map<String, Object> consumerProperties = KafkaTestUtils.consumerProps("sender", "false",
embeddedKafka.getEmbeddedKafka());
for (Entry<String, Object> entry : consumerProperties.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
if (entry.getKey().equals("group.id")) {
groupId = entry.getValue();
} else if (entry.getKey().equals("bootstrap.servers")) {
bootstrapServers = entry.getValue();
}
}
// create a Kafka consumer factory
DefaultKafkaConsumerFactory<String, String> consumerFactory = new DefaultKafkaConsumerFactory<String, String>(
consumerProperties);
// set the topic that needs to be consumed
ContainerProperties containerProperties = new ContainerProperties(SENDER_TOPIC);
// create a Kafka MessageListenerContainer
container = new KafkaMessageListenerContainer<>(consumerFactory, containerProperties);
// create a thread safe queue to store the received message
records = new LinkedBlockingQueue<>();
// setup a Kafka message listener
container.setupMessageListener(new MessageListener<String, String>() {
#Override
public void onMessage(ConsumerRecord<String, String> record) {
LOGGER.debug("test-listener received message='{}'", record.toString());
records.add(record);
}
});
// start the container and underlying message listener
container.start();
// wait until the container has the required number of assigned partitions
ContainerTestUtils.waitForAssignment(container, embeddedKafka.getEmbeddedKafka().getPartitionsPerTopic());
}
#After
public void tearDown() {
// stop the container
container.stop();
}
#Test
public void testCamelWithKafka() throws Exception {
String topicName = "topic=javainuse-topic";
String kafkaServer = "kafka:localhost:9092";
String zooKeeperHost = "zookeeperHost=localhost&zookeeperPort=2181";
String serializerClass = "serializerClass=kafka.serializer.StringEncoder";
String toKafka = new StringBuilder().append(kafkaServer).append("?").append(topicName).append("&")
.append(zooKeeperHost).append("&").append(serializerClass).toString();
String embedded = new StringBuilder().append(bootstrapServers).append("?").append(topicName).append("&")
// .append(embeddedKafka.getEmbeddedKafka().getZookeeperConnectionString())
.append(zooKeeperHost).append("&").append(serializerClass).toString();
Main main = new Main();
main.addRouteBuilder(new RouteBuilder() {
#Override
public void configure() throws Exception {
from("file:D://inbox//?noop=true").split().tokenize("\n").to("direct:embedded");
}
});
main.run();
ConsumerRecord<String, String> received =
records.poll(10, TimeUnit.SECONDS);
// assertThat(received, hasValue(greeting));
// AssertJ Condition to check the key
// assertThat(received).has(key(null));
// System.out.println(received);
}
}
Camel should able to read from a file and move the data to kafka and consumer should able to read it.
You can use the #Runwith(CamelSpringBootRunner.class) to run the test case.

How to wait for a spring jms listener thread to finish executing in Junit test

I have a spring boot application that uses spring-JMS. Is there any way to tell the test method to wait the jms lister util it finishes executing without using latches in the actual code that will be tested?
Here is the JMS listener code:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import javax.jms.Message;
import javax.jms.QueueSession;
#Component
public class MyListener {
#Autowired
MyProcessor myProcessor;
#JmsListener(destination = "myQueue", concurrency = "1-4")
private void onMessage(Message message, QueueSession session) {
myProcessor.processMessage(message, session);
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.jms.Message;
import javax.jms.QueueSession;
#Component
public class MyProcessor {
public void processMessage(Message msg, QueueSession session) {
//Here I have some code.
}
}
import org.apache.activemq.command.ActiveMQTextMessage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.QueueSession;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
#SpringBootTest
#ExtendWith(SpringExtension.class)
#ActiveProfiles("test")
public class IntegrationTest {
#Autowired
private JmsTemplate JmsTemplate;
#Test
public void myTest() throws JMSException {
Message message = new ActiveMQTextMessage();
jmsTemplate.send("myQueue", session -> message);
/*
Here I have some testing code. How can I tell the application
to not execute this testing code until all JMS lister threads
finish executing.
*/
}
}
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.util.SocketUtils;
import javax.jms.ConnectionFactory;
#EnableJms
#Configuration
#Profile("test")
public class JmsTestConfig {
public static final String BROKER_URL =
"tcp://localhost:" + SocketUtils.findAvailableTcpPort();
#Bean
public BrokerService brokerService() throws Exception {
BrokerService brokerService = new BrokerService();
brokerService.setPersistent(false);
brokerService.addConnector(BROKER_URL);
return brokerService;
}
#Bean
public ConnectionFactory connectionFactory() {
return new ActiveMQConnectionFactory(BROKER_URL);
}
#Bean
public JmsTemplate jmsTemplate(ConnectionFactory connectionFactory) {
JmsTemplate jmsTemplate = new JmsTemplate(connectionFactory);
return jmsTemplate;
}
}
Note: Is it applicable to solve this without adding testing purpose code to the implementation code (MyListener and MyProcessor).
Proxy the listener and add an advice to count down a latch; here's one I did for a KafkaListener recently...
#Test
public void test() throws Exception {
this.template.send("so50214261", "foo");
assertThat(TestConfig.latch.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(TestConfig.received.get()).isEqualTo("foo");
}
#Configuration
public static class TestConfig {
private static final AtomicReference<String> received = new AtomicReference<>();
private static final CountDownLatch latch = new CountDownLatch(1);
#Bean
public static MethodInterceptor interceptor() {
return invocation -> {
received.set((String) invocation.getArguments()[0]);
return invocation.proceed();
};
}
#Bean
public static BeanPostProcessor listenerAdvisor() {
return new ListenerWrapper(interceptor());
}
}
public static class ListenerWrapper implements BeanPostProcessor, Ordered {
private final MethodInterceptor interceptor;
#Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
public ListenerWrapper(MethodInterceptor interceptor) {
this.interceptor = interceptor;
}
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Listener) {
ProxyFactory pf = new ProxyFactory(bean);
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(this.interceptor);
advisor.addMethodName("listen");
pf.addAdvisor(advisor);
return pf.getProxy();
}
return bean;
}
}
(but you should move the countDown to after the invocation proceed()).
A method annotated with #JmsListener deletes the message after it finishes, so a good option is to read the queue for existing messages and assume the queue is empty after your method is done. Here is the piece of code for counting the messages from the queue.
private int countMessages() {
return jmsTemplate.browse(queueName, new BrowserCallback<Integer>() {
#Override
public Integer doInJms(Session session, QueueBrowser browser) throws JMSException {
return Collections.list(browser.getEnumeration()).size();
}
});
}
Following is the code for testing the countMessages() method.
jmsTemplate.convertAndSend(queueName, "***MESSAGE CONTENT***");
while (countMessages() > 0) {
log.info("number of pending messages: " + countMessages());
Thread.sleep(1_000l);
}
// continue with your logic here
I've based my solution on the answer given by Gary Russell, but rather put the CountDownLatch in an Aspect, using Spring AOP (or the spring-boot-starter-aop variant).
public class TestJMSConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(TestJMSConfiguration.class);
public static final CountDownLatch countDownLatch = new CountDownLatch(1);
#Component
#Aspect
public static class LatchCounterAspect {
#Pointcut("execution(public void be.infrabel.rocstdm.application.ROCSTDMMessageListener.onMessage(javax.jms.TextMessage))")
public void onMessageMethod() {};
#After(value = "onMessageMethod()")
public void countDownLatch() {
countDownLatch.countDown();
LOGGER.info("CountDownLatch called. Count now at: {}", countDownLatch.getCount());
}
}
A snippet of the test:
JmsTemplate jmsTemplate = new JmsTemplate(this.embeddedBrokerConnectionFactory);
jmsTemplate.convertAndSend("AQ.SOMEQUEUE.R", message);
TestJMSConfiguration.countDownLatch.await();
verify(this.listenerSpy).putResponseOnTargetQueueAlias(messageCaptor.capture());
RouteMessage outputMessage = messageCaptor.getValue();
The listenerSpy is a #SpyBean annotated field of the type of my MessageListener. The messageCaptor is a field of type ArgumentCaptor<MyMessageType> annotated with #Captor. Both of these are coming from mockito so you need to run/extend your test with both MockitoExtension (or -Runner) along with the SpringExtension (or -Runner).
My code puts an object on an outbound queue after processing the incoming message, hence the putResponseOnTargetQueueAlias method. The captor is to intercept that object and do my assertions accordingly. The same strategy could be applied to capture some other object in your logic.

Doesn't work rollback() in SessionAwareMessageListener

Even though the message is received by the MessageListener, I don't want to remove from the Queue, I want to do some processing in onMessage method and based on the result:
I want to commit(); for Success - so the message will be completely removed from the Queue.
For Failures - don't commit - rollback(); so the message will be redelivered (some times by default) and then goes to Dead letter Queue (DLQ). That’s OK for us.
I use: SpringBoot and hornetq (spring-boot-starter-hornetq-1.4.7.RELEASE).
Settings:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.connection.UserCredentialsConnectionFactoryAdapter;
import org.springframework.jndi.JndiObjectFactoryBean;
import org.springframework.jndi.JndiTemplate;
import javax.jms.ConnectionFactory;
import javax.naming.Context;
import javax.naming.NamingException;
import java.util.Properties;
import static com.test.hornetq.Receiver.LOG;
import static javax.jms.Session.SESSION_TRANSACTED;
#Configuration
public class JmsConfig {
private String host;
private String port;
private String connectionFactoryJndiName;
private String jndiInit;
private String user;
private String password;
private String jmsReceiverConcurrency;
public JmsConfig(final Environment env) {
host = env.getProperty("host");
port = env.getProperty("port");
connectionFactoryJndiName = env.getProperty("connectionfactory.jndiname");
jndiInit = env.getProperty("jndiInit");
user = env.getProperty("user");
password = env.getProperty("password");
jmsReceiverConcurrency = env.getProperty("jmsReceiverConcurrency");
}
#Bean
public JndiTemplate jndiTemplate() {
final JndiTemplate jndiTemplate = new JndiTemplate();
jndiTemplate.setEnvironment(getProperties());
return jndiTemplate;
}
#Bean
public JndiObjectFactoryBean jmsConnectionFactory() throws NamingException {
final JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiTemplate(jndiTemplate());
jndiObjectFactoryBean.setJndiName(connectionFactoryJndiName);
jndiObjectFactoryBean.afterPropertiesSet();
return jndiObjectFactoryBean;
}
#Bean
#Primary
public ConnectionFactory connectionFactory() throws NamingException {
final UserCredentialsConnectionFactoryAdapter adapter = new UserCredentialsConnectionFactoryAdapter();
adapter.setTargetConnectionFactory((ConnectionFactory) jmsConnectionFactory().getObject());
adapter.setUsername(user);
adapter.setPassword(password);
return adapter;
}
#Bean
public JmsListenerContainerFactory<?> myJmsContainerFactory() throws NamingException {
final DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setSubscriptionDurable(false);
factory.setConcurrency(jmsReceiverConcurrency);
factory.setMaxMessagesPerTask(1);
factory.setSessionTransacted(true);
factory.setSessionAcknowledgeMode(SESSION_TRANSACTED);
factory.setErrorHandler(t -> {
LOG.error("Error in listener!", t);
});
return factory;
}
private Properties getProperties() {
final Properties jndiProps = new Properties();
jndiProps.setProperty(Context.INITIAL_CONTEXT_FACTORY, jndiInit);
jndiProps.setProperty(Context.PROVIDER_URL, "http-remoting://" + host + ":" + port);
jndiProps.setProperty(Context.SECURITY_PRINCIPAL, user);
jndiProps.setProperty(Context.SECURITY_CREDENTIALS, password);
return jndiProps;
}
}
And receiver:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Session;
#Component
public class Receiver {
#JmsListener(destination = "${destination.name}", containerFactory = "myJmsContainerFactory")
public void onReceive(final MapMessage message, Session session) throws JMSException {
try {
System.out.println(">>>> " + message);
session.rollback();
} catch (Exception ex) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>THROW ");
throw ex;
}
}
}
But when I do rollback(); nothing happen and message doesn't comeback.
The code work. The problem was in hornetq settings in server side.
<pre-acknowledge>true</pre-acknowledge>
Extra Acknowledge Modes
Please note, that if you use pre-acknowledge mode, then you will lose transactional semantics for messages being consumed, since clearly they are being acknowledged first on the server, not when you commit the transaction. This may be stating the obvious but we like to be clear on these things to avoid confusion!

Resources