Skip to content

A Simple Guide to Using IBM MQ with Java Messaging Service

August 3, 2023

Soham Patak

This blog is a guide for developers that are looking to get started with IBM message queues. By the end of this blog, you should be able to:

  1. Have a basic understanding of how message queues work
  2. How to set up your application to use an IBM MQ
  3. Spin up a docker container to host the message queue
  4. How to switch queues your application writes to without re-deploying your application

A basic understanding of Java and Spring is recommended for this tutorial.

Why MQ?

Before discussing the specifics and requirements to set up IBM MQs, let’s discuss what problem message queues are solving and what type of applications can benefit the most from a message queue architecture.

In the ever-growing use of microservice architecture, the ability for multiple services to communicate with each other in a fast and asynchronous way is very useful. Using message queues enables two applications to communicate with each other asynchronously by writing to a queue instead of calling each other. By doing so, message queues additionally make the two applications less coupled.

Alternative methods to using message queues like using REST APIs make the message processing a blocking process which makes asynchronous processing difficult.

The advantage of using queues is that due to the option for asynchronous communication, they’re always available to process the next available “message” in the queue. Unfortunately, alternative methods like REST API and GraphQL are naturally unable to accommodate the message blocking issues, making MQ the better solution. Another advantage of using an MQ between services is that if one of the services goes down, it allows the service to process the requests when it comes back up. In other words, it essentially decouples the services.

What is MQ

A Message Queue (MQ) is a component of messaging middleware that makes asynchronous communication between applications easier. Message queue temporarily stores messages by providing an intermediary platform that allows software components to access the queue. In this tutorial, we will be using IBM MQ specifically.

Some of the key features and benefits of IBM MQ are:

  • High availability
  • Deployment flexibility
  • Compatibility with various languages and frameworks
  • Extensive documentation and support

Implementation

The MQ setup consists of two main parts:

  1. Setting up a docker container for IBM MQ
  2. Java Messaging Service to use the MQ

Part 1: Setting up a docker container for IBM MQ

The first part of setting up comprises of creating a docker compose file and a shell script. The docker compose file will be used by the shell script to spin up the docker container that will allow the applications to use message queues locally. The following code snippet shows a basic docker compose code that is used to define the docker image.

version: '2.1'

services:
  ibmmq:
    image: 'docker.io/ibmcom/mq'
    environment:
      - LICENSE=accept
      - MQ_QMGR_NAME=QM1
    ports:
      - '1414:1414'
      - '9443:9443'
    volumes:
      - ibmmq:/data/ibmmq
    container_name: ibmmq

volumes:
  ibmmq:
    driver: local

The following script spins up the docker container and creates the queues that will be used by our application. This script only creates two queues, but this command can be used multiple times based on how many queues the application needs to read and write from.

#!/bin/bash

echo "Let's clean up the environment by taking the container down..."
docker-compose --log-level WARNING down --remove-orphans
# spins up the docker container for the mq
echo "Let's spin up the container..."
docker-compose -f docker-compose.yml up --detach
# The queues that are created here are also available within application.yaml.
# If you add one here, make sure it aligns with application.yaml
echo "INFO Creating the MQ queues..."
cat << EOF | docker exec --interactive ibmmq sh
runmqsc QM1
define qlocal (MQCLIENT_MQ1.RESPONSE.FROM.MQSERVER_MQ1)
define qlocal (MQCLIENT_MQ1.RESPONSE.FROM.MQSERVER_MQ2)
end
EOF

That concludes the first part of the setup.

Part 2: Java Messaging Service to use the MQ

For the second part, the first step is creating a spring application. For this, we will use a spring initializer. We’ll be using Java Messaging Service (JMS) for this tutorial. JMS lets Java applications use messaging systems – like IBM MQ in our case – to communicate. We’ll also need to add the required dependencies for JMS.

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

		<dependency>
			<groupId>javax.jms</groupId>
			<artifactId>javax.jms-api</artifactId>
			<version>2.0.1</version>
		</dependency>

		<dependency>
			<groupId>commons-beanutils</groupId>
			<artifactId>commons-beanutils</artifactId>
			<version>1.9.4</version>
		</dependency>

Next, we’ll create a class called JmsConfig.java where we’ll inject the necessary properties required to connect the Java application to the message queues using Spring Framework’s @Value annotation. These properties can be added to the application.yaml file under the resources folder of your application.

server:
  url: "http://localhost"
  port: 80

ibm:
  mq:
    queueManager: QM1
    channel: DEV.ADMIN.SVRCONN
    connName: localhost(1414)
    host: localhost
    port: 1414
    user: admin
  queues:
    sampleQueues: MQCLIENT_MQ1.RESPONSE.FROM.MQSERVER_MQ2,MQCLIENT_MQ1.RESPONSE.FROM.MQSERVER_MQ1

demo:
  concurrency:
    size:
      low: 1
      high: 6
@Configuration
public class JmsConfig {
	@Value( "${ibm.mq.host}" )
	private String host;

	@Value( "${ibm.mq.port}" )
	private Integer port;

	@Value( "${ibm.mq.queueManager}" )
	private String queueManager;

	@Value( "${ibm.mq.channel}" )
	private String channel;

	@Value( "${ibm.mq.user}" )
	private String user;

	@Value( "${ibm.mq.password}" )
	private String password;

	@Value( "${ibm.mq.connName}" )
	private String connName;

	@Value( "${ibm.queues.sampleQueues}" )
	private List<String> sampleQueues;

	@Value( "${demo.concurrency.size.low}" )
	private String concurrencyMin;

	@Value( "${demo.concurrency.size.high}" )
	private String concurrencyMax;

	@Bean
	public JmsTemplate jmsTemplate() throws JMSException{
		JmsTemplate jmsTemplate = new JmsTemplate();
		jmsTemplate.setConnectionFactory( cachingConnectionFactory() );
		return jmsTemplate;
	}

	@Bean
	public CachingConnectionFactory cachingConnectionFactory() throws JMSException{
		CachingConnectionFactory factory = new CachingConnectionFactory();
		factory.setSessionCacheSize( 1 );
		factory.setTargetConnectionFactory( createConnectionFactory() );
		factory.setReconnectOnException( true );
		factory.afterPropertiesSet();
		return factory;
	}

	@Bean
	public JmsConnectionFactory createConnectionFactory() throws JMSException{
		JmsFactoryFactory ff = JmsFactoryFactory.getInstance( JmsConstants.WMQ_PROVIDER );
		JmsConnectionFactory factory = ff.createConnectionFactory();
		factory.setObjectProperty( WMQConstants.WMQ_CONNECTION_MODE, Integer.valueOf( WMQConstants.WMQ_CM_CLIENT ) );
		factory.setStringProperty( WMQConstants.WMQ_HOST_NAME, host );
		factory.setObjectProperty( WMQConstants.WMQ_PORT, port );
		factory.setStringProperty( WMQConstants.WMQ_QUEUE_MANAGER, queueManager );
		factory.setStringProperty( WMQConstants.WMQ_CHANNEL, channel );
		factory.setStringProperty( WMQConstants.USERID, user );
		factory.setStringProperty( WMQConstants.PASSWORD, password );
		return factory;
	}

	@Bean
	@Primary
	public JmsListenerEndpointRegistry createRegistry(){
		JmsListenerEndpointRegistry registry = new JmsListenerEndpointRegistry();
		return registry;
	}

	@Bean
	public JmsListenerEndpointRegistrar createRegistrar() throws JMSException{
		JmsListenerEndpointRegistrar registrar = new JmsListenerEndpointRegistrar();
		registrar.setEndpointRegistry( createRegistry() );
		registrar.setContainerFactory( createDefaultJmsListenerContainerFactory() );
		return registrar;
	}

	public DefaultJmsListenerContainerFactory createDefaultJmsListenerContainerFactory() throws JMSException{
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
		factory.setConnectionFactory( createConnectionFactory() );
		return factory;
	}
}

Important things to note in the above snippet:

  • The properties that we inject using here are used by the JmsConnectionFactory Bean to establish a connection to the message queues.

Next, we want to make sure our application is also able to receive messages. To do this, we can add another class called QueueConfig.java that will let our application receive messages. We’ll use SimpleJmsListenerEndpoint to do that. We set each queue we create as a destination to a SimpleJmsListenerEndpoint which lets JMS read messages from that queue.

@Configuration
public class MqConfig{

	@Autowired
	JmsListenerEndpointRegistrar registrar;

	@Autowired
	private MessageHandler queueController;

	@Value( "${ibm.queues.sampleQueues}" )
	String[] sampleQueues;

	@Value( "${demo.concurrency.size.low}" )
	Integer messageConcurrencyLow;

	@Value( "${demo.concurrency.size.high}" )
	Integer messageConcurrencyHigh;

	String jmsMessageConcurrency = "";

	@PostConstruct
	public void init(){
		jmsMessageConcurrency = String.format( "%s-%s", messageConcurrencyLow, messageConcurrencyHigh );
		configureJmsListeners( registrar );
	}

	public void configureJmsListeners( JmsListenerEndpointRegistrar registrar ){
		int i = 0;
		for( final String queueName : sampleQueues ){
			SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
			endpoint.setId( "demo-" + i++ );
			endpoint.setDestination( queueName );
			endpoint.setConcurrency( jmsMessageConcurrency );

			endpoint.setMessageListener( message -> {
				queueController.recv( queueName, message );
			} );
			registrar.registerEndpoint( endpoint );
		}
	}
}

Now, we need to add a class called MessageHandler.java that we’ll use to process the received message from and send messages to the queue.

@Component
public class MessageHandler{

	@Autowired
	private JmsTemplate jmsTemplate;

	protected final static Logger L = LoggerFactory.getLogger( MessageHandler.class );

	public void recv( String destination, Message message ){
		try{
			MQMessage mqMessage = new MQMessage();
			mqMessage.writeString( ( (TextMessage) message ).getText() );
			mqMessage.setStringProperty( "IncomingDestination", destination );
			processMessage( mqMessage );
		}
		catch( Exception e ){
			L.error("Error while reading message", e);
		}
	}

	public void processMessage( MQMessage mqMessage ){
		// TODO add application specific message processing code here
	}

	public void send(String destinationQueue,String messageBody){
		// TODO Create a messageCreator using the  messageBody here and use JmsTemplate to send the message
		// The message body depends on what the application needs
		//jmsTemplate.send(destinationQueue, messageCreator);
	}
}

That concludes the basic setup required for the application to be able to read and write messages from the queue. Note that this is just the basic setup. The next step would be to create a class that will process, and store information passed in the message based on the purposes of your application.

Context Switch

When the application is deployed in different environments, we can run into situations where we want to use different sets of queues for different purposes such as testing where we don’t want to send a test message to a real queue. This next part of the blog will help us do that.

Inject the following JMS-related properties that we defined earlier in the tutorial.

@RestController
@RequestMapping( "/context_switch" )
public class ContextSwitchController{
	protected final static Logger L = LoggerFactory.getLogger( ContextSwitchController.class );
	@Autowired
	private JmsTemplate jmsTemplate;
	@Autowired
	private JmsConnectionFactory factory;
	@Autowired
	private JmsListenerEndpointRegistry registry;
	@Autowired
	private JmsListenerEndpointRegistrar registrar;
	@Autowired
	private MessageHandler messageHandler;
	@Autowired
	private JmsConfig jmsConfig;
  }

Create a REST API controller called ContextController.java with a post endpoint that can be used to switch the queues programmatically.

@PostMapping
	public String switchContext( @RequestBody JmsConfig configRequest ) throws Exception{
		try{
			if( configRequest.getQueueManager() != null ){
				BeanUtils.copyProperties( jmsConfig, configRequest );
				setupConnection( configRequest );
				switchQueues( configRequest );
			}
			L.debug( "Context switched successfully" );
		}
		catch( Exception e ){
			throw new Exception( "Unable to switch context." );
		}
		return "Switched to QM: " + jmsConfig.getQueueManager() + " with host: " + jmsConfig.getHost() + " and port: " + jmsConfig.getPort();
	}

The request body of this endpoint is of the same type as the JmsConfig bean we injected above. We can use BeanUtils.copyProperties method to copy the new JMSproperties to the JmsConfig bean.

Next, do the following to close the existing connections and update the connectionfactory with the new JMSproperties.

	private void setupConnection( JmsConfig configRequest ) throws Exception{
		Set<String> listenerContainerIds = registry.getListenerContainerIds();
		for( String id : listenerContainerIds ){
			registry.getListenerContainer( id ).stop();
		}
		try{
			factory.setStringProperty( WMQConstants.WMQ_HOST_NAME, configRequest.getHost() );
			factory.setObjectProperty( WMQConstants.WMQ_PORT, Integer.valueOf( configRequest.getPort() ) );
			factory.setStringProperty( WMQConstants.WMQ_QUEUE_MANAGER, configRequest.getQueueManager() );
			factory.setStringProperty( WMQConstants.WMQ_CHANNEL, configRequest.getChannel() );
			factory.setStringProperty( WMQConstants.USERID, configRequest.getUser() );
			factory.setStringProperty( WMQConstants.PASSWORD, configRequest.getPassword() );

			factory.createConnection();
			jmsTemplate.setConnectionFactory( factory );
		}
		catch( JMSException e ){
			throw new Exception( "Invalid Connection Factory Parameters" );
		}
	}

Finally, we need to make sure that the new set of queues is accepting messages by using SimpleJmsListenerEndpoint like we did when we first set up the application.

	private void switchQueues( JmsConfig configRequest ){
		for( String queueName : configRequest.getSampleQueues() ){
			SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
			endpoint.setId( "demo-" + UUID.randomUUID() );
			endpoint.setDestination( queueName );
			endpoint.setConcurrency( configRequest.getConcurrencyMin() + "-" + configRequest.getConcurrencyMax() );
			endpoint.setMessageListener( message -> {	
				messageHandler.recv( queueName, message );
			} );
			registrar.registerEndpoint( endpoint );
		}
	}

That concludes the context switch part of the tutorial.

In Conclusion

To summarize, this tutorial went through the following things:

  1. Spinning up a docker container locally to host IBM MQ
  2. Using JMS to connect to, read, and write to the MQ
  3. Add a controller for the ability to programmatically switch queues in a deployed application

This guide is simply a skeleton framework that can be enhanced flexibly according to the needs of your project.

GOT A PROJECT?