In SpringBoot how & when does @JmsListener gets called?
Asked Answered
A

1

5

I'm new to SpringBoot. Trying to build a simple non-web process where I listen to a MQ Queue and process the messages received. I tried various ways to acheive this in SB, but unfortunately I cant get the @JmsListener method to get called. No errors either & the process just waits.

All the MQ Queue details are in application.properties

I did verify there are messages in the Queue and I could retrieve them using the old MQ receiver way.

I would like to know how & when does @JmsListener Annotation method gets called? I did try creating a JmsListenerContainerFactory and included it in the annotation params but made no difference.

There are few examples similar to this, it looks simple but I just cant get it to work. Any suggestions are appreciated. Thanks.

Main SpringBoot Class

@SpringBootApplication
@EnableJms
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

MQListener Class

@Component
public class MQListener {
    @JmsListener(destination = "${mq.queueName}")
    public void receiveMessage(final Message message) throws JMSException{
        System.out.println("...Message Received...");
        String messageData = null;
        if(message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage)message;
            messageData = textMessage.getText();
        }
    }
}

MQConfiguration Class

@Configuration
public class MQConfiguration {
    @Value("${mq.host}")
    private String host;
    @Value("${mq.port}")
    private Integer port;
    @Value("${mq.queue-manager}")
    private String queueManager;
    @Value("${mq.channel}")
    private String channel;
    @Value("{mq.queueName}")
    private String queueName;
    @Value("${mq.receive-timeout}")
    private long receiveTimeout;
    
    @Bean
    public MQQueueConnectionFactory mqQueueConnectionFactory() {
        MQQueueConnectionFactory mqQueueConnectionFactory = new MQQueueConnectionFactory();
        mqQueueConnectionFactory.setHostName(host);
        try {
            mqQueueConnectionFactory.setTransportType(WMQConstants.WMQ_CM_CLIENT);
            mqQueueConnectionFactory.setChannel(channel);
            mqQueueConnectionFactory.setPort(port);
            mqQueueConnectionFactory.setQueueManager(queueManager);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return mqQueueConnectionFactory;
    }
}
Ass answered 5/5, 2021 at 4:0 Comment(1)
Often with async listeners, you have to explicitly start them. I don't know how this is done in SB and you haven't shown anything that looks like a start, but I mention it in case any of the examples you are following had something like that and you didn't include it because you didn't know why you needed it.Exergue
R
10

As you are using default settings for the MQ connection factory you don't actually need it. Instead you can use the default one that Spring Boot will create for you. You are also only expecting a text message so you can let Spring do the marshalling. In which case all you need is a message consumer derived from this example - https://github.com/ibm-messaging/mq-dev-patterns/tree/master/Spring-JMS/src/main/java/com/ibm/mq/samples/jms/spring/level101

package ...

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 MessageConsumer101 {
    protected final Log logger = LogFactory.getLog(getClass());

    @JmsListener(destination = "${mq.queueName}")
    public void receive(String message) {
        logger.info("");
        logger.info( this.getClass().getSimpleName());
        logger.info("Received message is: " + message);
    }
}

As you will be letting Spring Boot create the MQ container you need to provide settings in application.properties in the form -


# MQ Connection settings
ibm.mq.queueManager=QM1  
ibm.mq.channel=DEV.APP.SVRCONN
ibm.mq.connName=localhost(1414)


# Change the following lines as necessary. Set the ibm.mq.user
# property to an empty string to send no authentication request.
ibm.mq.user=app
ibm.mq.password=passw0rd

You are more likely to want a custom listener than a custom connection factory, but if you do want to configure a ConnectionFactory different from the default, then use a configuration and message consumer derived from this example - https://github.com/ibm-messaging/mq-dev-patterns/tree/master/Spring-JMS/src/main/java/com/ibm/mq/samples/jms/spring/level114

Configuration, you only need to set the properties that deviate from the default.

package ...

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());
        factory.setPubSubDomain(false);

        factory.setMessageConverter(new OurMessageConverter());
        factory.setDestinationResolver(new OurDestinationResolver());

        // reply Qos
        QosSettings rQos = new QosSettings();
        rQos.setPriority(2);
        rQos.setTimeToLive(10000);
        rQos.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
        factory.setReplyQosSettings(rQos);

        return factory;
    }

    @Bean("myNonJmsTemplate114")
    public JmsTemplate myNonJmsTemplate114() throws JMSException {
        JmsTemplate jmsTemplate = new JmsTemplate(mqConnectionFactory());
        jmsTemplate.setDestinationResolver(new OurDestinationResolver());
        jmsTemplate.setMessageConverter(new OurMessageConverter());

        return jmsTemplate;
    }

Note: The listener container factory based on the customised connection factory. You need this step. Your message consumer then looks something like:

package ...

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 = "${mq.queueName}", 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);
    }
}

if you need to perform your own marshalling from JMSMessage objects then use a message consumer derived from this example (You only need a consumer and nothing else) - https://github.com/ibm-messaging/mq-dev-patterns/tree/master/Spring-JMS/src/main/java/com/ibm/mq/samples/jms/spring/level105

package ...

import javax.jms.*;

import com.ibm.mq.samples.jms.spring.globals.Constants;
import com.ibm.mq.samples.jms.spring.globals.data.OurData;
import com.ibm.mq.samples.jms.spring.globals.data.OurOtherData;
import com.ibm.mq.samples.jms.spring.globals.utils.MessageUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jms.annotation.JmsListener;

import org.springframework.stereotype.Component;

import java.io.Serializable;


@Component
public class MessageConsumer105 {
    protected final Log logger = LogFactory.getLog(getClass());

    @JmsListener(destination = "${app.l105.queue.name2}")
    public void receiveData(Message message) {
        logger.info("");
        logger.info( this.getClass().getSimpleName());
        logger.info("Received message of type: " + message.getClass().getSimpleName());
        if (null != message) {
            MessageUtils.checkMessageType(message);
        }
    }
}

Where

package ...

import com.ibm.mq.samples.jms.spring.globals.Constants;
import com.ibm.mq.samples.jms.spring.globals.data.OurData;
import com.ibm.mq.samples.jms.spring.globals.data.OurOtherData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.jms.*;
import java.io.Serializable;
import java.util.Map;

public class MessageUtils {
    protected static final Log logger = LogFactory.getLog(MessageUtils.class);

    private MessageUtils () {}

    public static void checkMessageType(Message message) {
        try {
            if (message instanceof TextMessage) {
                logger.info("Message matches TextMessage");
                logger.info("message payload is " + ((TextMessage) message).getText());
            } else if (message instanceof BytesMessage) {
                logger.info("Message matches BytesMessage");
            } else if (message instanceof MapMessage) {
                logger.info("Message matches MapMessage");
            } else if (message instanceof StreamMessage) {
                logger.info("Message matches StreamMessage");
            } else if (message instanceof ObjectMessage) {
                checkForObject((ObjectMessage) message);
            }
        } catch (JMSException e) {
            logger.warn("Unable to process JMS message");
        }
    }

    public static void logHeaders(Map<String, Object> msgHeaders) {
        if (! msgHeaders.isEmpty() ) {
            logger.info("");
            logger.info("Headers found");
            msgHeaders.forEach((k, v) -> {
                logger.info(k + ": is of type" + v.getClass());
            });
        }
    }

    private static void checkForObject(ObjectMessage message) {
        try {
            int typeValue = message.getIntProperty(Constants.DATATYPE);
            if (Constants.DataTypes.OURDATATYPE.getValue() == typeValue) {
                logger.info("It is one of our objects");
                Serializable serObj = message.getObject();
                OurData data = (OurData) serObj;
                logger.info(data);
            } else if (Constants.DataTypes.OUROTHERDATATYPE.getValue() == typeValue) {
                logger.info("It is one of our other objects");
                Serializable serObj = message.getObject();
                OurOtherData data = (OurOtherData) serObj;
                logger.info(data);
            } else {
                logger.warn("It is not one of our objects");
            }
        } catch (JMSException e) {
            logger.warn("Unable to retrieve message data");
        } catch (ClassCastException e2) {
            logger.warn("Not the object we were expecting");
        }
    }

}

If your consumer and configuration classes have the @Component and @Configuration annotations and sit within the same package family, then they will be found, If not then you need to add some more annotations to the Application to get Spring to pick them up. eg.

package ...

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.jms.annotation.EnableJms;


@SpringBootApplication
@EnableJms
@ComponentScan
@EnableAutoConfiguration
public class MQApplication {
    public static void main(String[] args) {
        SpringApplication.run(MQApplication.class, args);
    }
}

If you don't have

spring.jms.listener.auto-startup=false 

in your application.properties file then all the listeners will start automatically on application start.

You will need to name mq-jms-spring-boot-starter as a dependency. Eg. if using maven :

        <dependency>
            <groupId>com.ibm.mq</groupId>
            <artifactId>mq-jms-spring-boot-starter</artifactId>
            <version>2.4.1</version>
        </dependency>

If this is the only JMS implementor then Spring Boot will be able to figure out that all your listeners are Using IBM MQ. If you have other JMS providers listed as dependancies, then Spring needs to be explicitly told which connection factories to use. There is a sample pom.xml with only the required dependancies in the sample at https://github.com/ibm-messaging/mq-dev-patterns/blob/master/Spring-JMS/pom.xml

Try the 101 sample at https://github.com/ibm-messaging/mq-dev-patterns/tree/master/Spring-JMS It's maven based, has only the required dependancies listed in its pom.xml and all you will need to do is update the application.properties in https://github.com/ibm-messaging/mq-dev-patterns/tree/master/Spring-JMS/src/main/resources to point at your MQ server.

Revile answered 5/5, 2021 at 9:4 Comment(4)
Thanks for taking time to explain and providing the sample code. My code seems be similar the one you have provided, especially at the MQListener Class. I'm failing to see where I'm making the mistake. This time I tried just one listener class, main SB class and my application.properties @Component public class MQListener { @JmsListener(destination = "${ibm.mq.queue}") public void receive(String message) { System.out.println("Inside receive - "+message); } } still no luck. Like before the app runs but no errors or receiving msgs from Q.Ass
Depending on how you initialised your Spring Boot project, it could be due to conflicting dependancies. See my updated answer.Revile
Thanks for your help again. I was able to connect to MQ & start getting messages. Ur Github projects helped a lot. Looks like I didn't add mq-jms spring boot jar which is why Spring Boot couldn't recognize that its a MQ configuration project.Ass
Cool! Please mark the question as answered.Revile

© 2022 - 2024 — McMap. All rights reserved.