JMS - Publish/Subscribe messaging example using ActiveMQ and Maven

In a publish/subscribe (pub/sub) product or application, clients address messages to a topic, which functions somewhat like a bulletin board. Subscribers can receive information, in the form of messages, from publishers. Topics retain messages only as long as it takes to distribute them to current subscribers.

The following post introduces the basic concepts of JMS point-to-point messaging and illustrates them with a code sample using ActiveMQ and Maven.

Publish/Subscribe Messaging

Pub/sub messaging has the following characteristics:

Each message can have multiple consumers.

Publishers and subscribers have a timing dependency. A client that subscribes to a topic can consume only messages published after the client has created a subscription, and the subscriber must continue to be active in order for it to consume messages.

The JMS API relaxes this timing dependency mentioned in the second bullet to some extent by allowing subscribers to create durable subscriptions, which receive messages sent while the subscribers are not active. Durable subscriptions provide the flexibility and reliability of queues but still allow clients to send messages to many recipients.

ActiveMQ Example

Let’s illustrate the above characteristics by creating a message producer that sends a message containing a first and last name to a topic. In turn, a message consumer will read the message and transform it into a greeting. The code is very similar to the JMS Hello World example but contains a few key differences explained below.

Tools used:

ActiveMQ 5.15

Maven 3.5

The code is built and run using Maven. Specified below is the Maven POM file which contains the needed dependencies for Logback, JUnit and Apache ActiveMQ.

Nondurable Subscription

The Publisher class contains a constructor which creates a message producer and needed connection and session objects. The sendName() operation takes as input a first and last name which are set on a TextMessage which in turn is sent to the topic set on the message producer.

packagecom.codenotfound.jms.pubsub;importjavax.jms.Connection;importjavax.jms.ConnectionFactory;importjavax.jms.JMSException;importjavax.jms.MessageProducer;importjavax.jms.Session;importjavax.jms.TextMessage;importjavax.jms.Topic;importorg.apache.activemq.ActiveMQConnection;importorg.apache.activemq.ActiveMQConnectionFactory;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;publicclassPublisher{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(Publisher.class);privateStringclientId;privateConnectionconnection;privateSessionsession;privateMessageProducermessageProducer;publicvoidcreate(StringclientId,StringtopicName)throwsJMSException{this.clientId=clientId;// create a Connection FactoryConnectionFactoryconnectionFactory=newActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_BROKER_URL);// create a Connectionconnection=connectionFactory.createConnection();connection.setClientID(clientId);// create a Sessionsession=connection.createSession(false,Session.AUTO_ACKNOWLEDGE);// create the Topic to which messages will be sentTopictopic=session.createTopic(topicName);// create a MessageProducer for sending messagesmessageProducer=session.createProducer(topic);}publicvoidcloseConnection()throwsJMSException{connection.close();}publicvoidsendName(StringfirstName,StringlastName)throwsJMSException{Stringtext=firstName+" "+lastName;// create a JMS TextMessageTextMessagetextMessage=session.createTextMessage(text);// send the message to the topic destinationmessageProducer.send(textMessage);LOGGER.debug(clientId+": sent message with text='{}'",text);}}

The Subscriber class contains a constructor which creates a message consumer and needed connection and session objects. The getGreeting() operation reads a message from the topic and creates a greeting which is returned. A timeout parameter is passed to assure that the method does not wait indefinitely for a message to arrive.

packagecom.codenotfound.jms.pubsub;importjavax.jms.Connection;importjavax.jms.ConnectionFactory;importjavax.jms.JMSException;importjavax.jms.Message;importjavax.jms.MessageConsumer;importjavax.jms.Session;importjavax.jms.TextMessage;importjavax.jms.Topic;importorg.apache.activemq.ActiveMQConnection;importorg.apache.activemq.ActiveMQConnectionFactory;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;publicclassSubscriber{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(Subscriber.class);privatestaticfinalStringNO_GREETING="no greeting";privateStringclientId;privateConnectionconnection;privateMessageConsumermessageConsumer;publicvoidcreate(StringclientId,StringtopicName)throwsJMSException{this.clientId=clientId;// create a Connection FactoryConnectionFactoryconnectionFactory=newActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_BROKER_URL);// create a Connectionconnection=connectionFactory.createConnection();connection.setClientID(clientId);// create a SessionSessionsession=connection.createSession(false,Session.AUTO_ACKNOWLEDGE);// create the Topic from which messages will be receivedTopictopic=session.createTopic(topicName);// create a MessageConsumer for receiving messagesmessageConsumer=session.createConsumer(topic);// start the connection in order to receive messagesconnection.start();}publicvoidcloseConnection()throwsJMSException{connection.close();}publicStringgetGreeting(inttimeout)throwsJMSException{Stringgreeting=NO_GREETING;// read a message from the topic destinationMessagemessage=messageConsumer.receive(timeout);// check if a message was receivedif(message!=null){// cast the message to the correct typeTextMessagetextMessage=(TextMessage)message;// retrieve the message contentStringtext=textMessage.getText();LOGGER.debug(clientId+": received message with text='{}'",text);// create greetinggreeting="Hello "+text+"!";}else{LOGGER.debug(clientId+": no message received");}LOGGER.info("greeting={}",greeting);returngreeting;}}

The below JUnit test class will be used to illustrate the Pub/Sub messaging characteristics mentioned at the beginning of this post. The testGreeting() test case verifies the correct working of the getGreeting() method of the Subscriber class.

The testMultipleConsumers() test case will verify that the same message can be read by multiple consumers. In order to test this, two Subscriber instances are created on the same 'multipleconsumers.t' topic.

Finally, the testNonDurableSubscriber() test case will illustrate the timing dependency between publisher and subscriber. First, a message is sent to a topic on which only one subscriber listens. Then a second subscriber is added to the same topic and a second message is sent. The result is that the second subscriber only receives the second message and not the first one whereas the first subscriber has received both messages.

Durable Subscription

As mentioned in the beginning of this post it is also possible to create a durable subscription which allows receiving messages sent while the subscribers are not active.

The JMS specification dictates that the identification of a specific durable subscription is done by a combination of the 'client ID', the 'durable subscription name' and the 'topic name'.

As a result of the below DurableSubscriber has three main differences with the previous Subscriber class:

A clientIdis mandatory on the connection in order to allow a JMS provider to uniquely identify a durable subscriber.

A durable subscriber is created using Session.CreateDurableSubscriber.

A subscriptionName is needed when creating the durable subscriber.

Note that creating a MessageConsumer provides the same features as creating a TopicSubscriber. The TopicSubscriber is provided to support existing code.

packagecom.codenotfound.jms.pubsub;importjavax.jms.Connection;importjavax.jms.ConnectionFactory;importjavax.jms.JMSException;importjavax.jms.Message;importjavax.jms.MessageConsumer;importjavax.jms.Session;importjavax.jms.TextMessage;importjavax.jms.Topic;importorg.apache.activemq.ActiveMQConnection;importorg.apache.activemq.ActiveMQConnectionFactory;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;publicclassDurableSubscriber{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(DurableSubscriber.class);privatestaticfinalStringNO_GREETING="no greeting";privateStringclientId;privateConnectionconnection;privateSessionsession;privateMessageConsumermessageConsumer;privateStringsubscriptionName;publicvoidcreate(StringclientId,StringtopicName,StringsubscriptionName)throwsJMSException{this.clientId=clientId;this.subscriptionName=subscriptionName;// create a Connection FactoryConnectionFactoryconnectionFactory=newActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_BROKER_URL);// create a Connectionconnection=connectionFactory.createConnection();connection.setClientID(clientId);// create a Sessionsession=connection.createSession(false,Session.AUTO_ACKNOWLEDGE);// create the Topic from which messages will be receivedTopictopic=session.createTopic(topicName);// create a MessageConsumer for receiving messagesmessageConsumer=session.createDurableSubscriber(topic,subscriptionName);// start the connection in order to receive messagesconnection.start();}publicvoidremoveDurableSubscriber()throwsJMSException{messageConsumer.close();session.unsubscribe(subscriptionName);}publicvoidcloseConnection()throwsJMSException{connection.close();}publicStringgetGreeting(inttimeout)throwsJMSException{Stringgreeting=NO_GREETING;// read a message from the topic destinationMessagemessage=messageConsumer.receive(timeout);// check if a message was receivedif(message!=null){// cast the message to the correct typeTextMessagetextMessage=(TextMessage)message;// retrieve the message contentStringtext=textMessage.getText();LOGGER.debug(clientId+": received message with text='{}'",text);// create greetinggreeting="Hello "+text+"!";}else{LOGGER.debug(clientId+": no message received");}LOGGER.info("greeting={}",greeting);returngreeting;}}

The below JUnit test class will be used to illustrate the durable subscriber messaging characteristics.

It contains a testDurableSubscriber() test case that will first remove one of the two durable subscribers that are listening on the 'durablesubscriber.t' topic by closing it’s connection to the broker. Then a first message is sent to this topic on which only one subscribers is still actively listening. The second subscriber is recreated using the same client ID and subscription name and a second message is sent. The expected result is that both subscribers receive the two messages.

Note that in the tearDownAfterClass() method the durable subscriptions are removed in order to avoid an error when rerunning the test case.