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:
A basic understanding of Java and Spring is recommended for this tutorial.
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.
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:
The MQ setup consists of two main parts:
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
That concludes the first part of the setup.
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.
com.ibm.mq mq-jms-spring-boot-starter 2.7.4 javax.jms javax.jms-api 2.0.1 commons-beanutils commons-beanutils 1.9.4
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( "$" ) private String host; @Value( "$" ) private Integer port; @Value( "$" ) private String queueManager; @Value( "$" ) private String channel; @Value( "$" ) private String user; @Value( "$" ) private String password; @Value( "$" ) private String connName; @Value( "$" ) private List sampleQueues; @Value( "$" ) private String concurrencyMin; @Value( "$" ) 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:
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( "$" ) String[] sampleQueues; @Value( "$" ) Integer messageConcurrencyLow; @Value( "$" ) 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.
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
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 < SetlistenerContainerIds = 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.
To summarize, this tutorial went through the following things:
This guide is simply a skeleton framework that can be enhanced flexibly according to the needs of your project.