












Table of Contents
Introduction
JMS Architecture
JMS Messaging Models
JMS Example
JMS
Provider: JBoss configuration
Core JMS API
Client: JMS
Producer
Client:
JMSConsumer
JMS Unified Client API
Execute the
example
Client:
JMSConsumer revisited
JMS
Topics instead of Queues
Reliability - Durable Subscriptions
Message Driven Beans
Client:
JMS Consumer revisited
Transactions
Distributed
Transactions
Summary
References
Introduction
JMS
or Java Messaging System is
an asynchronous communication API for Java applications. The JMS
Specification is defined in JSR 914 [1].
The Java Message Service was developed by Sun Microsystems to provide a
means for Java programs to access enterprise
messaging systems or message oriented middleware
(MOM). They provide a mechanism for integrating
applications in a loosely
coupled way. They provide asynchronous delivery
of data between applications on a store and forward basis; i.e., the
applications do not communicate directly with each other, but instead
communicate with the MOM, which acts as an intermediary [2], thus handling all network
communication details for you. If e.g. the network connection is not
available, the MOM will store the message until the connection becomes
available, and then forward it to the destination. Thus, the JMS API is
characterised as asynhronous
and reliable (it
can ensure that a message is delivered once and only once).
Even though Messaging follows the same principles as electronic mail
(email), messaging differs from electronic mail, which is a method of
communication between people or between software applications and
people. Messaging is used for communication between software
applications or software components.
In
the following we 'll show a simple Java "Hello JMS" message
exchange application using JMS 1.1.
JMS Architecture
The main players of the JMS architecture are the following [
2, 3]:
- JMS Provider: A messaging
system that implements the JMS specification. A JMS Provider, also
known as a JMS Server, routes messages between clients.
- Clients: Java applications
that send or receive JMS messages. A message sender is called the
Producer, and the recipient is called a Consumer.
- Messages: Messages contain
data or events exchanged between Producers and Consumers.
- Destinations: A Producer sends
a message to a JMS Destination (either a Queue or a Topic), and the
Consumer(s) listening on the JMS Destination receives the message.
JMS
Messaging Models
JMS has two messaging models [3]: Point-to-Point
(P2P) and Publish-Subscribe (Pub-Sub).
P2P is a traditional one-to-one queueing mechanism, i.e. although
multiple Consumers can listen on a queue, only one Consumer receives a
particular message. Producers send messages to a queue, and the JMS
Provider (aka JMS Server) delivers each message sequentially to a
Consumer listening on the queue. The following figure shows the
relationships between Point-to-Point Producers and Consumers.

Figure
1 JMS Queue
Publish-Subscribe is a one-to-many
broadcast model, similar to a newsgroup or a bulletin board or an RSS
newsfeed. Producers publish messages to a topic,
and the JMS Server delivers messages sequentially to those Consumers
subscribed to that topic. The following figure shows the relationships
between Pub-Sub Producers and Consumers.

Figure
2 JMS
Topic
The biggest difference between P2P and
Pub-Sub is that in the Publish-Subscribe Model, all Consumers that
subscribe to a Topic can receive all messages
published to that topic, while with Point-to-Point, only one
Consumer on a queue receives a particular message.
Even though JMS is inherently asynchronous, the JMS
specification allows for messages to be consumed in either of two ways [10]:
-
Synchronously:
A subscriber or a receiver explicitly fetches the message from the
destination by calling the receive method.
The receive method can block until a message arrives or can
time out if a message does not arrive within a specified time limit.
-
Asynchronously:
A client can
register a message listener with a consumer. A message listener is
similar to an event listener. Whenever a message arrives at the
destination, the JMS provider delivers the message by calling the
listener's onMessage
method, which acts on the contents of the message.
A JMS message consists
of a header, properties and a body [
2,10]. The
header is a standard set of
fields that are used by both clients and providers to identify and
route messages; it contains a message id, destination, timestamp,
priority etc. The
properties provide
a facility for adding optional header fields to a message e.g. for
categorisation or classification, to provide compatibility with other
messaging systems, or to use them to create message selectors. JMS
defines a standard set of properties that are optional for providers to
supply. The
body contains
the content to be delivered to a receiving application. The body or
content of the JMS message can be many things, e.g. XML, a JavaBean
(i.e. an object) etc. JMS API provides five message body formats: text,
map, bytes, stream, and object. To send an object as a JMS Message, it
must obey the following rules:
- An object must
implement java.io.Serializable.
- Each data member must be serializable. By default, String, the
Java primitives (int,
float,
and so on) and the Java primitive wrappers (Integer, Float, and so
on) are all serializable.
JMS Example
Now that we know the basics of JMS, we 'll create the players of our
JMS application:
- JMS Provider:
The JMS
Provider will be JBoss, a full J2EE application
server which provides support for JMS 1.1.
- Clients:
Our clients
will be a JMSProducer
and a JMSConsumer
java class.
- Messages: The
JMSProducer
will send a simple "Hello JMS" TextMessage
to be delivered to the JMSConsumer.
- Destinations:
The JMS Destination will be a queue, named MyQueueXA. The JMSProducer
will post to this queue and the JMSConsumer
will listen to this queue to consume the message.
JMS
Provider: JBoss configuration
To
execute and test the program, you need access to a vendor
implementation of JMS. Most Java 2 Enterprise Edition (J2EE)
application servers provide an implementation of JMS. JBoss is such an
application server. JBoss Messaging provided by default with JBoss 4.2
supports the latest JMS 1.1 specification and it offers vastly improved
performance in both single node and clustered environments compared to
the older JBossMQ JBoss JMS provider [9].
However, this is something that doesn't bother us as users of the JBoss
JMS service.
To
setup a JMS message queue or topic in
JBoss is very easy. There are actually 3 ways to do
this [3,7,8,9]:
- Using the JMX console:
Start the JBoss application server (%JBOSS_HOME%/bin/run.bat
or $JBOSS_HOME/bin.run.sh)
and open the JMX console (http://localhost:8080/jmx-console).
Click on the service=DestinationManager
link (under the jboss.mq heading) to see the
JBossMQ DestinationManager MBean page. Look for
the createQueue( ) operation and enter jms/MyQueue
for the first parameter (the J2EE JNDI name would then be java:comp/env/jms/MyQueue),
and queue/MyQueue for the second parameter (the
JBoss-specific JNDI name). The JMX-console has the advantage that it is
easy to use and you can dynamically specify JMS resources; the
disadvantage though is that your changes are lost once you shut down
JBoss
- Add an <mbean>
element for the queue to $JBOSS_HOME/server/default/deploy/jms/jbossmq-destinations-service.xml
file. This file is part of the core JBoss
deployment and contains JBoss-specific default test Queues and Topics.
This has the advantage that the changes survive JBoss
startup/shutdowns; the disandvantage is however that you mix-up your
destinations with JBoss internal destinations.
|
<mbean
code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,name=MyQueue">
<depends optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager</depends>
</mbean>
|
-
The
best solution is to create your own service descriptor
file (post-fixed with
-service.xml)
that resides in $JBOSS_HOME/server/default/deploy
and add an <mbean> element for the queue. This way your
changes survice JBoss startup/shutdowns and you
don't mix-up your destinations with JBoss internal
destinations. Co-mingling destinations is bad because each
time you upgrade to a new version of JBoss, you have to re-add the <mbean>
elements to your service descriptor to make things work again.
<?xml
version="1.0" encoding="UTF-8"?>
<server>
<mbean code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,name=MyQueue">
<depends
optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager</depends>
</mbean>
</server>
That's
it! Please note that destination without a configured SecurityManager
or without a SecurityConf
will default to role guest
with read=true,
write=true, create=false. If you want to setup a SecurityManager,
you should modify your mbean
similar to this:
<?xml
version="1.0" encoding="UTF-8"?>
<server>
<mbean code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,name=MyQueue">
<depends
optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager</depends>
<depends
optional-attribute-name="SecurityManager">jboss.mq:service=SecurityManager</depends>
<attribute name="SecurityConf">
<security>
<role name="guest" create="false" read="true"
write="true"/>
<role name="senders" create="false" read="false"
write="true"/>
<role name="receivers" create="false" read="true"
write="false"/>
</security>
</attribute>
</mbean>
</server>
We shall do one last
optional thing to be able to debug our JMS example application. JBoss
uses an internal DBMS, an instance of HSQLDB, to store the
JMS messages until they are consumed. Of course, it is very easy to
setup another DBMS [7,8]
but we don't bother with the details in this article. What you can do
however, if you have not setup another DBMS, is to enable
the HSQL MBean to accept TCP/IP connections [7,8]. Open the hsqldb-ds.xml file which
you’ll find in the deploy directory and which
sets up the default datasource. Near the top of the file,
you’ll find the connection-url element. Make sure
the value is set to jdbc:hsqldb:hsql://localhost:1701 and that any other connection-url elements are
commented out.
|
<!-- The jndi name of
the DataSource, it is prefixed with java:/ -->
<!--
Datasources are not available outside the virtual machine -->
<jndi-name>DefaultDS</jndi-name>
<!-- for
tcp connection, allowing other processes to use the hsqldb
database. This
requires the org.jboss.jdbc.HypersonicDatabase mbean. -->
<connection-url>jdbc:hsqldb:hsql://localhost:1701</connection-url>
<!-- for
totally in-memory db, not saved when jboss stops.
The
org.jboss.jdbc.HypersonicDatabase mbean is unnecessary
<connection-url>jdbc:hsqldb:.</connection-url>
-->
<!-- for
in-process db with file store, saved when jboss stops. The
org.jboss.jdbc.HypersonicDatabase
is unnecessary
<connection-url>jdbc:hsqldb:${jboss.server.data.dir}/hypersonic/localDB
</connection-url>
-->
|
Then scroll down to the
bottom of the file, and uncomment the MBean declaration for the
Hypersonic service.
|
<mbean
code="org.jboss.jdbc.HypersonicDatabase"
name="jboss:service=Hypersonic">
<attribute name="Port">1701</attribute>
<attribute name="Silent">true</attribute>
<attribute
name="Database">default</attribute>
<attribute name="Trace">false</attribute>
<attribute
name="No_system_exit">true</attribute>
</mbean>
|
Open the
jmx-console
again (you need to restart JBoss), and click on the
service=Hypersonic
link which you’ll find under the section
jboss. Invoke
the
startDatabaseManager()
MBean operation. The HSQLDB Manager is executed.
Figure 3 HSQL DB Manager
As you can see, JBoss uses 5 tables for JMS handling,
which are shown in the following ER diagram.
Figure 4 JMS Entity-Relationship model
used by JBoss
Each time you execute
the JMSProducer
you will see a new row in the JMS_MESSAGES
table like so:
| MESSAGEID |
DESTINATION |
TXID |
TXOP |
MESSAGEBLOB |
| 1 |
QUEUE.MyQueueXA |
|
A |
SpyTextMessage
{Header ...} |
When you run the JMSConsumer,
the message is consumed, i.e. removed from the table.
Core JMS API
The JMS API resides in
the javax.jms
package. These are the most important classes and interfaces for our
purposes:
- Message:
Holds business data and routing information. Although you'll find
several types of Messages, most of the time you'll use either a TextMessage
(that contains textual data in its body) or an ObjectMessage
(that holds serializable objects in its body).
- Destination:
Holds messages sent by the Producer to be received by the Consumer(s).
A Destination is either a Queue or a Topic that the JMS Server manages
on behalf of its clients.
- Connection:
Enables a JMS client to send or receive Messages. Use a Connection to
create one or more Sessions.
- ConnectionFactory:
A ConnectionFactory is either a QueueConnectionFactory
or a TopicConnectionFactory,
depending on the messaging model, and it exists to create Connections.
- Session:
A Session creates Producers, Consumers, and Messages. A
Publish-Subscribe application uses a TopicSession, and a Point-to-Point
application uses a QueueSession. Sessions are single-threaded.
Figure 5 JMS API - class diagram
Figure 6
JMS State Transition
Diagram
Client:
JMSProducer
We'll take the following steps to send a message:
- Look up a ConnectionFactory
using JNDI.
- Get a Connection
from the ConnectionFactory.
- Create a Session
associated with the Connection.
- Look up a Destination
using JNDI.
- Create a Message Producer tied to the Destination.
- Create a Message.
- Send the Message.
- Tear down the Message Producer, Session, and Connection.
JMS requires a lot of low-level tedious API calls to send a message, so
we encapsulate everything into a
JmsProducer
class. To compile this and the following classes you need the
jbossall-client.jar
found inside
$JBOSS/client
directory.
| Listing
1. JmsProducer.java |
package jms.producer;
import javax.jms.*;
import jms.ServiceLocator; import jms.ServiceLocatorException;
/** * <code>JmsProducer</code> encapsulates sending a JMS Message. * */ public class JmsProducer { private static final String CONNECTION_FACTORY ="ConnectionFactory"; private static final String MY_QUEUE = "queue/MyQueue"; private static final String HELLO_MSG = "Hello JMS";
public static void main(String args[]) { JmsProducer.sendMessage(HELLO_MSG, CONNECTION_FACTORY, MY_QUEUE); } /** * Making the default (no arg) constructor private * ensures that this class cannnot be instantiated. */ private JmsProducer() { } /** * * @param txtMessage The message (xml file) to send via JMS * @param connectionFactoryJndiName connection factory JNDI name * @param destinationJndiName destination JNDI name * @throws JmsProducerException */ public static void sendMessage(String txtMessage, String connectionFactoryJndiName, String destinationJndiName) throws JmsProducerException { Connection connection = null; try { ConnectionFactory connectionFactory = null; Session session = null; Destination destination = null; MessageProducer messageProducer = null; TextMessage message = null;
// 1. Look up a ConnectionFactory using JNDI. connectionFactory = ServiceLocator .getJmsConnectionFactory(connectionFactoryJndiName); // 2. Get a Connection from the ConnectionFactory with default user id. connection = connectionFactory.createConnection(); // 3. Create a Session associated with the Connection. // 1st parameter: true if Session is transacted // 2nd parameter: Acknowledgement method session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4. Look up a Destination using JNDI. destination = ServiceLocator.getJmsDestination(destinationJndiName); // 5. Create a Message Producer tied to the Destination. messageProducer = session.createProducer(destination);
// 6. Create a Message. message = session.createTextMessage(txtMessage); // 7. Send a Message. messageProducer.send(message); } catch (JMSException je) { throw new JmsProducerException(je); } catch (ServiceLocatorException sle) { throw new JmsProducerException(sle); } finally { // 8. Tear down the Message Producer, Session, and Connection. if (connection != null) { try { // automatically session and producer are closed connection.close(); } catch (Exception e) { e.printStackTrace(); } } } } }
|
The JMS ConnectionFactory
and Connection
are both JNDI-based resources, so we've encapsulated the JNDI lookups
with the ServiceLocator
class which is presented later. The ServiceLocator.getJmsConnectionFactory(
) call finds and instantiates the ConnectionFactory
to gain access to the JMS provider. The ConnectionFactory
creates a Connection.
Use the following parameters in the Connection.createSession( )
call to create the Session:
- The first parameter is
a boolean
that determines whether the Session
is transacted. We've set this value to false because
we don't want to manage the transaction programmatically. We use
Container-Managed Transactions so that JBoss manages the transaction
for us.
- The second parameter
sets the acknowledgment mode. Using AUTO_ACKNOWLEDGE
means that once the Consumer receives a message, an acknowledgment is
sent to the JMS server automatically. Usually, you should use AUTO_ACKNOWLEDGE
and let the container do all the work for you.
After we've created the Session, the ServiceLocator.getJmsDestination(
) call finds the Destination,
a proxy to the actual destination (a queue, in this case, because of
how it's deployed) on the JMS server. The Session's createProducer( )
method creates the MessageProducer, which sends messages to a
destination asynchronously. The call to the Session's createTextMessage( )
method creates a TextMessage
method that contains the String
passed into the JmsProducer's sendMessage(
) method. The MessageProducer's send( ) method
sends the message to the destination, and we wrap up by closing the MessageProducer,
Session,
and Connection.
The careful reader may
have noticed that we don't define the connection factory "ConnectionFactory"
in any configuration file in JBoss. This is true. "ConnectionFactory" is
the default name for a connection factory provided by JBoss [8].
Client:
JMSConsumer
The code for the
JMSConsumer
is similar.
| Listing
2. JmsConsumer.java |
package jms.consumer;
import javax.jms.*;
import jms.ServiceLocator; import jms.ServiceLocatorException;
/** * <code>JmsConsumer</code> encapsulates receiving a JMS Message. * */ public class JmsConsumer { private static final String QUEUE_CONNECTION_FACTORY ="ConnectionFactory"; private static final String MY_QUEUE = "queue/MyQueue";
public static void main(String args[]) throws Exception { JmsConsumer.receiveMessage(QUEUE_CONNECTION_FACTORY, MY_QUEUE); } /** * Making the default (no arg) constructor private * ensures that this class cannnot be instantiated. */ private JmsConsumer() { } /** * Receives a JMS message. * @param connectionFactoryJndiName connection factory JNDI name * @param destinationJndiName destination JNDI name * @throws JmsProducerConsumerException */ public static void receiveMessage( String connectionFactoryJndiName, String destinationJndiName) throws JmsConsumerException { Connection connection = null; try { ConnectionFactory connectionFactory = null; Session session = null; Destination destination = null; MessageConsumer messageConsumer = null; TextMessage textMessage = null;
// 1. Look up a ConnectionFactory using JNDI. connectionFactory = ServiceLocator .getJmsConnectionFactory(connectionFactoryJndiName); // 2. Get a Connection from the ConnectionFactory with default user id. connection = connectionFactory.createConnection(); // 3. Create a Session associated with the Connection. // 1st parameter: true if Session is transacted // 2nd parameter: Acknowledgement method session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4. Look up a Destination using JNDI. destination = ServiceLocator.getJmsDestination(destinationJndiName); // 5. Create a Message Consumer tied to the Destination. messageConsumer = session.createConsumer(destination); // 6. Receive a Message. connection.start(); Message message = messageConsumer.receive(); if (message instanceof TextMessage) { textMessage = (TextMessage) message; System.out.println("Message received: " + textMessage.getText()); } } catch (JMSException je) { throw new JmsConsumerException(je); } catch (ServiceLocatorException sle) { throw new JmsConsumerException(sle); } finally { // 7. Tear down the Message Consumer, Session, and Connection. if (connection != null) { try { // messageProducer.close(); // session.close(); connection.close(); // automatically session and producer are closed } catch (Exception e) { e.printStackTrace(); } } } } }
|
Notice
the important connection.start(); command in step 6
that causes message delivery to begin. It activates the delivery of
messages from the JMS server [2].Without
this command, your messages won't be delivered to the
consumer. If you want to stop message delivery
temporarily without closing the connection, you call the
stop() method.
The receive()
method can be used in
several ways to perform a synchronous receive. If you specify no
arguments or an argument of 0, the method blocks indefinitely until a
message arrives. For a simple client
program, this may not matter. But if you
do not want your program to consume system resources unnecessarily, use
a timed synchronous receive.
Message message = messageConsumer.receive(1); // wait for 1 ms
Message message = messageConsumer.receiveNoWait(); // receive a message only if one is available
JMS
Unified Client API
As of JMS 1.1, the
Unified API is now available and works with both the Publish-Subscribe
and Point-to-Point models [
2,3,7-9].
The JMS 1.1 specification encourages you to write all your new JMS code
by using the Unified API, since the Queue and Topic-based APIs could
become deprecated in a future version of JMS. However, the
model-specific APIs will last for a while because so much production
code uses them. Don't get the wrong idea, the physical JMS Queues and
Topics are not going away. You'll still use them with the Unified API.
The Unified API resembles the Queue and Topic-based APIs because it
follows the same calling sequence. The big difference is that your code
doesn't really care if you use Queues or Topics. All you need to
provide are the JNDI names for Queue/Topic Connection Factory and
Queue/Topic.
We recommend using the Unified API because it simplifies development;
you don't need the Queue or Topic APIs anymore. You'll still send
messages to a Queue or Topic (depending on the JNDI name you use), but
this is now just a configuration issue. Your code becomes much more
generic because it no longer uses queue or topic-specific API calls.
With the Unified API, you always access JMS-based resources in the same
way, regardless of your JMS messaging model.
We've wrapped the JMS API calls in the
JmsProducer
class to make it easy to send a JMS message. The
JmsProducer
uses the
ServiceLocator
utility class to find the
ConnectionFactory
and the JMS
Destination
using JNDI.
| Listing 3.
ServiceLocator.java |
package jms;
import java.util.Properties;
import javax.jms.ConnectionFactory; import javax.jms.Destination; import javax.naming.*;
/** * <code>ServiceLocator</code> encapsulates JNDI lookups to make it * easier to access JNDI-based resources (EJBs, DataSources, * JMS Destinations, and so on). * */ public class ServiceLocator { private static Properties environment = null; static { // can be found in <JBoss>/server/default/conf/jndi.properties environment = new Properties(); environment.setProperty("java.naming.provider.url", "localhost:1099"); environment.setProperty("java.naming.factory.initial", "org.jboss.naming.NamingContextFactory"); environment.setProperty("java.naming.factory.url.pkgs", "org.jboss.naming:org.jnp.interfaces"); }
/** * Making the default (no arg) constructor private * ensures that this class cannnot be instantiated. */ private ServiceLocator() { }
/** * Gets a JMS <code>ConnectionFactory</code> using the given JNDI name. * * @param jmsConnectionFactoryJndiName The JMS <code>ConnectionFactory</code>'s JNDI name. * @return ConnectionFactory The JMS <code>ConnectionFactory</code>. * @throws ServiceLocatorException If there are JNDI lookup problems. * @see javax.jms.ConnectionFactory */ public static ConnectionFactory getJmsConnectionFactory(String jmsConnectionFactoryJndiName) throws ServiceLocatorException { ConnectionFactory jmsConnectionFactory = null;
try { Context ctx = new InitialContext(environment); jmsConnectionFactory = (ConnectionFactory) ctx.lookup(jmsConnectionFactoryJndiName); } catch (ClassCastException cce) { throw new ServiceLocatorException(cce); } catch (NamingException ne) { throw new ServiceLocatorException(ne); }
return jmsConnectionFactory; }
/** * Gets a JMS <code>Destination</code> using the given JNDI name. * * @param jmsDestinationJndiName The JMS <code>Destination</code>'s JNDI name. * @return Destination The JMS <code>Destination</code>. * @throws ServiceLocatorException If there are JNDI lookup problems. * @see javax.jms.Destination */ public static Destination getJmsDestination(String jmsDestinationJndiName) throws ServiceLocatorException { Destination jmsDestination = null;
try { Context ctx = new InitialContext(environment);
jmsDestination = (Destination) ctx.lookup(jmsDestinationJndiName); } catch (ClassCastException cce) { throw new ServiceLocatorException(cce); } catch (NamingException ne) { throw new ServiceLocatorException(ne); }
return jmsDestination; } }
|
The
getJmsConnectionFactory(
)
and
getJmsDestination(
) methods, respectively, encapsulate a JNDI lookup for a
JMS
ConnectionFactory
or JMS
Destination.
Both methods have the following steps in common:
- Create the InitialContext
to access the JNDI tree.
- Perform a JNDI lookup.
- Cast the object returned from JNDI to the correct JMS
object type (ConnectionFactory
or Destination)
.
- Throw a ServiceLocatorException
that chains a low-level JNDI-related exception and contains a
corresponding error message.
Notice the environment
properties variable that is passed in the InitialContext
in order to be able to access JBoss' JNDI lookup.
Execute
the example
That's it! This is the
simplest JMS application you could write! Compile and run the
JmsProducer
first; open the HSQL DB Manager and make sure that a new message has
been inserted into the
JMS_MESSAGES
table; run the
JmsConsumer to
see the "
Hello JMS"
message to your output; make sure that the new row has disappeared from
the
JMS_MESSAGES
table. Go for a beer!
Client:
JMSConsumer revisited
You might have noticed,
that JmsConsumer
consumes only one message at a time, and that if the JmsProducer
sends more messages, they are stored in the JMS_MESSAGES
table, until the JmsConsumer
runs again and again to consume them. This is synchronous behaviour. We
can easily fix this, by
creating a Listener to the queue (asynchronous behaviour). The code is
shown in Listing 4.
The JmsListener
class implements the MessageListener
interface which provides only one method onMessage(). MessageListener
is an interface with a single method -- onMessage(Message)
-- that provides asynchronous receipt andd processing of messages. Inside
this method you process the received messages.
| Listing
4. JmsListener.java |
package jms.consumer;
import javax.jms.*;
/** * <code>JmsListener</code> is the message listener of JMS Messages. * It implements the specified <code>onMessage</code> method. */ public class JmsListener implements MessageListener { public JmsListener( ) { } /** * Receives a JMS message. * @param message a JMS message */ public void onMessage( Message message ) { // retrieve and process message try { if ( message instanceof TextMessage ) { TextMessage textMessage = (TextMessage) message; System.out.println(textMessage.getText()); } else { System.out.println( "Expecting " + "TextMessage object, received " + message.getClass().getName() ); } } // process JMS exception from message catch ( JMSException jmsException ) { jmsException.printStackTrace(); } } // end method onMessage }
|
You need to modify the JmsConsumer
class a bit to delegate the processing of the JMS messages to the JmsListener.
The changes are shown in Listing 5. You replace step 6:
|
// 6. Receive a Message.
Message message =
messageConsumer.receive();
if (message
instanceof TextMessage) {
textMessage = (TextMessage) message;
System.out.println("Message received: " + textMessage.getText());
}
|
with the steps shown in
Listing 5, i.e. by setting the message listener of the messageConsumer
to be the JmsListener
(step 6). Because it takes some time to consume a JMS message, we
simply wait for 3'' before closing the connection.
| Listing
5. JmsConsumer.java |
// 5. Create a Message Consumer tied to the Destination. messageConsumer = session.createConsumer(destination);
// 6. Initialize and set message listener messageConsumer.setMessageListener(new JmsListener());
// 7. Receive a Message. connection.start();
// Give it some time to consume the messages try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
// 8. Tear down the Message Consumer, Session, and Connection. //messageConsumer.close(); //session.close(); connection.close();
|
That's it. Run the
JmsProducer
again a couple of times, check the
JMS_MESSAGES
table to see that more than one JMS messages have entered the queue,
and then execute the modified
JmsConsumer.
You will notice that this time all JMS messages are consumed, before
the program exits, and that you see the message
"Hello JMS"
more than once in your console.
JMS
Topics instead of Queues
As we have already mentioned, deciding on a topic or a queue is simply
a configuration issue. Here is the service descriptor for the
topic.
<?xml
version="1.0" encoding="UTF-8"?>
<server>
<mbean code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,name=MyQueue">
<depends
optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager</depends>
</mbean>
<mbean code="org.jboss.mq.server.jmx.Topic"
name="jboss.mq.destination:service=Topic,name=MyTopic">
<depends
optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager</depends>
</mbean>
</server>
That's
it! The java code, in both JMSProducer and JMSConsumer, remains the
same, apart, of course, from the reference
which has to change to topic/MyTopic
instead of queue/MyQueue!
This is the power of the unified API.
private static final String MY_TOPIC = "topic/MyTopic";
You 'll see the difference only when you run the programs again. Run
(multiple) instances of JmsConsumer
(first), then as you send messages by executing JmsProducer,
all JmsConsumer
instances receive each message sent by the JmsProducer. If
you start the JmsProducer
first and then the JmsConsumers,
you will notice that the consumers receive no messages. This is another
dfference among a queue and a topic, i.e.
a consumer subscribing to a topic cannot
view past messages. Additionally, notice that you won't see a record in
the JMS_MESSAGES
table. Topic
messages are delivered in the same way a television show is delivered;
unless you have the TV on and are watching the show, you will miss it.
Unacknowledged messages for a nondurable
TopicSubscriber
are dropped when the session is closed.
This
problem is solved by durable
subscriptions,
discussed next. Durable subscriptions are like having a VCR recording a
show you cannot watch at its scheduled time so that you can see what
you missed when you turn your TV back on.
Reliability - Durable
Subscriptions
As we saw in the previous section, a consumer subscribing to a topic
cannot
view past messages. This kind of subscription is called
non-durable
subscription. A subscription can be
durable
or
non-durable.
A
non-durable
subscriber can only receive messages that are published while
it is
active. A non-durable subscription doesn't guarantee the
delivery of the
message or may deliver the same message more than once.
A durable subscription, on the other hand, guarantees that the
consumer receives
the message only once.
The following command, that we already used in
Listing
2, creates a
non-durable subscription.
// 5. Create a Message Consumer tied to the Destination. messageConsumer = session.createConsumer(destination);
|
The most reliable way to produce
a message is to send a PERSISTENT message within
a transaction. JMS messages are PERSISTENT by
default.
The most reliable way to consume
a message is to do so within a transaction, either from a queue or from
a durable subscription to a topic.
Durable subscriptions
allow consumers to receive messages
sent to topics while the consumers are not active. Durable
subscriptions exist
only for Topics; queues are reliable by default. A durable subscriber
registers a durable subscription
by specifying a unique identity [10]
that is
retained by the JMS provider. Subsequent subscriber objects that have
the same identity resume the subscription in the state in which it was
left by the preceding subscriber. If a durable subscription has no
active subscriber, the JMS provider retains the subscription's messages
until they are received by the subscriber or until they expire.
OK, let's start. First, we need to create our users:
JMS_USERS table
| USERID |
PASSWD |
CLIENTID |
| myPublisher |
passwd |
|
| mySubscriber |
passwd |
mySubscriberClientID |
We created two users, one publisher and one subscriber. Check that the ClientID
needs to be setup for the subscriber; this way the publisher has
flexibility to send messages to specific subscribers only. Next, we
setup the roles:
JMS_ROLES table
| ROLEID |
USERID |
| publishers |
myPublisher |
| subscribers |
mySubscriber |
| durablesubscribers |
mySubscriber |
Be careful to declare mySubscriber both in subscribers and in durablesubscribers. Finally, we wire the clientID with the topic.
JMS_SUBSCRIPTIONS table
| CLIENTID |
SUBNAME |
TOPIC |
SELECTOR |
| mySubscriberClientID |
mySubscriber |
MyTopic |
(null) |
Then,
the my-jbossmq-destinations-service.xml
file has to be modified like so:
<?xml
version="1.0" encoding="UTF-8"?>
<server>
<mbean code="org.jboss.mq.server.jmx.Topic"
name="jboss.mq.destination:service=Topic,name=MyTopic">
<depends
optional-attribute-name="DestinationManager">
jboss.mq:service=DestinationManager</depends>
<depends
optional-attribute-name="SecurityManager">jboss.mq:service=SecurityManager</depends>
<attribute name="SecurityConf">
<security>
<role name="guest" create="false"
read="true"
write="true"/>
<role name="publishers" create="false"
read="false"
write="true"/>
<role name="subscribers" create="true"
read="true"
write="false"/>
<role name="durablesubscribers" create="true"
read="true"
write="true"/>
</security>
</attribute>
</mbean>
</server>
Note that the role names must match the ROLEIDs in the JMS_ROLES table.
If you are using another database than the default hsqldb, open $jboss/conf/login-config.xml and locate the following:
<!-- Security domain for JBossMQ -->
<application-policy name = "jbossmq">
<authentication>
<login-module
code = "org.jboss.security.auth.spi.DatabaseServerLoginModule"
flag = "required">
<module-option name =
"unauthenticatedIdentity">guest</module-option>
<module-option name = "dsJndiName">java:/DefaultDS</module-option>
<module-option name = "principalsQuery">SELECT PASSWD FROM
JMS_USERS WHERE USERID=?</module-option>
<module-option name = "rolesQuery">SELECT ROLEID, 'Roles' FROM
JMS_ROLES WHERE USERID=?</module-option>
</login-module>
</authentication>
</application-policy>
Make sure that the dsJndiName points to the correct datasource and not to the DefaultDS, otherwise you will get a security error.
Then the code for the JMSProducer has to be modified like so:
// 2. Get a Connection from the ConnectionFactory. connection = connectionFactory.createConnection("myPublisher","passwd"); // username, password found in JMS_USERS
|
JMSConsumer's
connection
has to be modified accordignly:
// 2. Get a Connection from the ConnectionFactory. connection = connectionFactory.createConnection("mySubscriber","passwd"); // username, password found in JMS_USERS ... // 5. Create a durable subscriber tied to the Destination. messageConsumer = session.createDurableSubscriber((Topic)destination, "mySubscriber"); // the SUBNAME from JMS_SUBSCRIPTIONS table
|
If you run the publisher and then the subscriber, you will see the message 'Hello JMS' in the console output.
Message Driven Beans
As you might have expected, JEE has
full support for JMS API messaging through Message Driven Beans or
MDBs. We saw previously, that we had to create a JMS consumer to
consume a JMS message. This isn't necessary with EJB MDBs because the
EJB container does that on the MDB's behalf.
A simple MDB is shown in
Listing 6. Actually, it is a copy of JmsListener of
Listing 4, annotated with EJB 3 annotations, shown in bold!
Attention! Your container must support EJB 3.0 or later to execute this code. This is true for JBoss 4.2 or later.
| Listing 6. MessageDrivenBean.java |
package jms.consumer;
//Java extension packages import javax.ejb.ActivationConfigProperty; import javax.ejb.MessageDriven; import javax.jms.*;
/** * <code>MessageDrivenBean</code> is the message listener of JMS Messages. * It implements the specified <code>onMessage</code> method. */ @MessageDriven(activationConfig = { @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"), @ActivationConfigProperty(propertyName="destination", propertyValue="queue/MyQueue") }) public class MessageDrivenBean implements MessageListener { /** * Receives a JMS message. * @param message a JMS message */ public void onMessage( Message message ) { // retrieve and process message try { if ( message instanceof TextMessage ) { TextMessage textMessage = (TextMessage) message; System.out.println(textMessage.getText()); } else { System.out.println( "Expecting " + "TextMessage object, received " + message.getClass().getName() ); } } // process JMS exception from message catch ( JMSException jmsException ) { jmsException.printStackTrace(); } } // end method onMessage }
|
MDBs are initially described by an annotation—in this case,
@MessageDriven (javax.ejb.MessageDriven).
This annotation is used by the container to determine certain
behavioral traits about the bean (just as with entity beans and session
beans), most notably the
activation configuration.
The activation configuration for a message driven bean can instruct the
container for the appropriate messaging method to apply to the bean or
what types of messages are allowed to be processed by the bean [
12]. As you saw in
Listing 6, the MDB has its activation configuration specified by an array of
@ActivationConfigProperty (javax.ejb.ActivationConfigProperty) annotations. This alerts the EJB container as to where and how the messages received by this bean should be treated.
This annotation is an example of an interceptor of Aspect Oriented
Programming (AOP) in EJB 3. It is actually executed before the
MessageDriveBean class' method
onMessage() is executed. This is a handy tool for quickly adding value to your application without modifying existing code!
So, this is it! Now you only need to package the above class to an EJB.
Go to the correct output directory and type the following in a command
or shell prompt:
$> jar cf MDB.ejb3 jms/consumer/MessageDrivenBean.class
Copy the MDB.ejb3 to $JBoss/server/default/deploy/ directory. Execute the JMSProducer from Listing 1; notice the message "Hello JMS" that is produced at the JBoss output window (or check $JBoss/server/default/log/server.log)
22:39:28,655 INFO [STDOUT] Hello JMS
Transactions
A JMS transaction groups a set of
produced messages and a set of consumed messages into an atomic unit of
work [2]. If an error
occurs during a transaction, the production and consumption of messages
that occurred before the error can be "undone." Session objects control
transactions. A transacted Session always has a begin();
commit() and
rollback().
As you might have already guessed, you declare a transaction by setting
the first parameter in the createSession()
method to true;
the second indicates that message acknowledgment is not specified for
transacted sessions.
|
// 3. Create a Session
associated with the Connection.
// 1st
parameter: true if Session is transacted
// 2nd
parameter: Acknowledgement method
session =
connection.createSession(true,
0);
|
MDBs in EJB 3.0 are able to use transactions to allow a higher order of
control over message processing [
12]. By using transactions, you may
“roll back” the processing of a particular message if an action fails.
This rollback includes the business method that initially required the
transaction and the methods it invoked that support transaction
processing. There are two types of transactions:
Container-Managed Transactions (CMT) and
User-defined Transactions.
Container-Managed Transactions (CMT)
With Container-Managed Transactions (CMT), transactions
are handled by the container itself. The EJB specification has made
provisions for containers to provide their own transaction
functionality. This functionality can be added to an MDB merely by
annotating the class name with the @TransactionManagement (javax.ejb.TransactionManagement)
descriptor. Once the class is annotated, the business methods of
the class must be annotated to specify how the container’s
transaction functionality will interpret their invocation. This
behavior can be specified by using the @TransactionAttribute (javax.ejb.TransactionAttribute) annotation on business methods with either of the following two attributes: REQUIRED or NOT_SUPPORTED.
NOT_SUPPORTED
as a transaction type merely alerts the container that any previously
existing transactions should be suspended as the MDB executes the
method in question. Once the method has finished executing, the transaction is resumed and business continues as usual.
REQUIRED
transactions, on the other hand, cascade the transaction functionality
to the methods invoked within the original function. Therefore,
if a simple SQL INSERT statement fails, all methods invoked up to that
point that use transactions are alerted to roll back, taking on
their state before the error.
So, to provide CMT functionality to the MDB in Listing 6, you have to add the following annotations:
/**
* <code>MessageDrivenBean</code> is the message listener of JMS Messages.
* It implements the specified <code>onMessage</code> method.
*/
@TransactionManagement
@MessageDriven(activationConfig =
{
@ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
@ActivationConfigProperty(propertyName="destination", propertyValue="queue/MyQueue")
})
public class MessageDrivenBean implements MessageListener {
/**
* Receives a JMS message.
* @param message a JMS message
*/
@TransactionAttribute(REQUIRED)
public void onMessage( Message message ) {
// retrieve and process message
Should a database (or other) operation triggered in the
onMessage()
method cause a rollback in the current transaction, that rollback will
cascade up to the MDB level. This makes sure messages don’t
get processed until all the dependent logic executes successfully.
User-Defined Transactions
If you wish to use user-defined transactions, you need to modify your code like so:
/**
* <code>MessageDrivenBean</code> is the message listener of JMS Messages.
* It implements the specified <code>onMessage</code> method.
*/
@TransactionManagement(BEAN)
@MessageDriven(activationConfig =
{
@ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
@ActivationConfigProperty(propertyName="destination", propertyValue="queue/MyQueue")
})
public class MessageDrivenBean implements MessageListener {
/**
* Receives a JMS message.
* @param message a JMS message
*/
@Resource javax.transaction.UserTransaction transaction;
public void onMessage( Message message ) {
// retrieve and process message
transaction.update(); // Start the transaction
...
transaction.commit(); // Assume transaction went well, commit
Simply add the parameter
BEAN to the
@TransactionManagement
class annotation and the container will assume your MDB is handling all
the finer details of transaction management. The first step in managing
the transactions manually for a particular class is obtaining it
via resource injection. The line
@Resource javax.transaction.UserTransaction transaction accomplishes this.
Distributed
Transactions
Distributed transactions can also be supported by the Java
Transaction API (JTA) XAResource API, though this is optional
for providers. [TBA]
Summary
In
this article we gave a detailed introduction to
Java Messaging Service (JMS). JMS allows for anynchronous communication
between applications in a loosely coupled manner.
We discussed the JMS architecture, the messaging models (queue and
topic), and we provided useful examples to help you up and running. We
also used JBoss as our JMS provider, but you can use any other JMS
Provider you wish. Please notice that we used the latest JMS 1.1
specification which allows for more generic code to be used. With JMS
1.1, the destination (queue or topic) is now simply a configuration
setting, and you saw the impact in your code.
There
are some topics we didn’t cover in detail, like security, message selection, queue browsers etc. For
that reason the interested reader is invited to check
the references for more information.
References
- SUN Microsystems (2002),
JMS Specification JSR 914, http://java.sun.com/products/jms/docs.html.
- Farrell,
W. (2004), Introducing the Java
Messaging Service, IBM.
- Davis,
S. & Marrs, T. (2005),
JBoss at Work: A Practical Guide, O'Reilly.
- Monson-Haefel,
R. & Chappell D. A. (2001),
Java Messaging Service, O'Reilly.
- Haase,
K. (2002), Java Messaging Service API Tutorial,
SUN
Microsystems.
- Giotta,
P. et. al. (2000),
Professional JMS Programming, Apress.
- JBoss
(2006),
Getting Started with JBoss 4.0,
Release 5, JBoss Inc..
- JBoss
(2005),
The JBoss 4 Application Server Guide,
Release 3, JBoss Inc.
- JBoss
(2007),
JBoss
Messaging 1.4 User's Guide, JBoss Inc.
- Jendrock E. et al. (2006),
The Java™ EE 5 Tutorial, Third Edition, Addison
Wesley Professional.
- "JMS Transaction with JTA in JBoss 4", http://www.odi.ch/prog/jms-tx.php.
- Mukhar K. & Zelenak C. (2006), Beginning Java EE 5 from Novice to Professional, Apress.

Creator: John N. Kostaras - email
[email protected]
Last modification: 25 June 2008
URL: http://www.geocities.com/jnkjavaconnection/jmstutorial.html
URL: http://jkost.ergoway.gr/jnkjavaconnection/jmstutorial.html