The Java Message Service defines six
Message
interface types that must be supported by
JMS providers. Although JMS defines the Message
interfaces, it doesn't define their implementation. This allows
vendors to implement and transport messages in their own way, while
maintaining a consistent and standard interface for the JMS
application developer. The six message interfaces are
Message
and its five sub-interfaces:
TextMessage
, StreamMessage
,
MapMessage
, ObjectMessage
, and
BytesMessage
.
The Message
interfaces are defined according to
the kind of payload they are designed to carry. In some cases,
Message
types were included in JMS to support
legacy payloads that are common and useful, which is the case with
the text
, bytes
, and
stream
message types. In other cases, the
Message
types were defined to facilitate emerging
needs; for example, ObjectMessage
can transport
serializable Java objects. Some vendors may provide other proprietary
message types. Progress' SonicMQ and SoftWired's iBus,
for example, provide an XMLMessage
type that
extends the TextMessage
, allowing developers to
deal with the message directly through DOM or SAX interfaces. The
XMLMessage
type may become a standard message type
in a future version of the specification. At the time of this
writing, Sun Microsystems was starting discussions about adding an
XMLMessage
type.
The simplest type of message is the
javax.jms.Message
, which serves as the base interface
to the other message types. As shown below, the
Message
type can be
created and used as a JMS message with no payload:
// Create and deliver a Message Message message = session.createMessage( ); publisher.publish(message); ... // Receive a message on the consumer public void onMessage(Message message){ // No payload, process event notification }
This type of message contains only JMS headers and properties, and is
used in event notification. An event notification is a broadcast,
warning, or notice of some occurrence. If the business scenario
requires a simple notification without a payload, then the
lightweight Message
type is the most efficient way
to implement it.
This
type carries a java.lang.String
as its payload.
It's useful for exchanging simple text messages and more
complex character data like XML documents:
package javax.jms;
public interface TextMessage
extends Message {
public String getText( )
throws JMSException;
public void setText(String payload)
throws JMSException, MessageNotWriteableException;
}
Text messages can be created with one of two factory methods defined
in the Session
interface. One factory method takes
no arguments, resulting in a TextMessage
object
with an empty payload—the payload is added using the
setText( )
method defined in the
TextMessage
interface. The other factory method
takes a String
type payload as an argument,
producing a ready-to-deliver TextMessage
object:
TextMessage textMessage = session.createTextMessage( ); textMessage.setText("Hello!"); topicPublisher.publish(textMessage); ... TextMessage textMessage = session.createTextMessage("Hello!"); queueSender.send(textMessage);
When a consumer receives a TextMessage
object it
can extract the String
payload using the
getText( )
method. If the TextMessage
was delivered without a payload, the getText( )
method returns a null
value or an empty
String
(""
) depending on the
JMS provider.
This type carries a serializable Java object as its payload. It's useful for exchanging Java objects:
package javax.jms;
public interface ObjectMessage
extends Message {
public java.io.Serializable getObject( )
throws JMSException;
public void setObject(java.io.Serializable payload)
throws JMSException, MessageNotWriteableException;
}
Object messages can be created with one of two factory methods
defined in the Session
interface. One factory
method takes no arguments, so the serializable object must be added
using the setObject(
)
.
The other factory method takes the Serializable
payload as an argument, producing a ready-to-deliver
ObjectMessage
:
// Order is a serializable object Order order = new Order( ); ... ObjectMessage objectMessage = session.createObjectMessage( ); objectMessage.setObject(order); queueSender.send(objectMessage); ... ObjectMessage objectMessage = session.createObjectMessage(order); topicPublisher.publish(objectMessage);
When a consumer receives an ObjectMessage
it can
extract the payload using the getObject(
)
method. If the ObjectMessage
was delivered without
a payload, the getObject( )
method returns a
null
value:
public void onMessage(Message message) { try { ObjectMessage objectMessage = (ObjectMessage)message; Order order = (Order)objectMessage.getObject( ); ... catch (JMSException jmse){ ... }
The ObjectMessage
is the most modern of message
types. In order for this message type to be useful, however, the
consumers and producers of the message must be Java programs. In
other words, ObjectMessage
is only useful between
Java clients and probably will not work with non-JMS
clients.[2]
The class definition of the object payload has to be available to
both the JMS producer and JMS consumer. If the
Order
class used in the previous example is not
available to the JMS consumer's JVM, an attempt to access the
Order
object from the message's payload
would result in a
java.lang.ClassNotFoundException
. Some JMS
providers may provide dynamic class loading capabilities, but that
would be a vendor-specific quality of service. Most of the time the
class must be placed on the JMS consumer's class path manually
by the developer.
This
type carries an array of primitive bytes as its payload. It's
useful for exchanging data in an application's
native format, which may not be
compatible with other existing Message
types. It
is also useful where JMS is used purely as a transport between two
systems, and the message payload is opaque to the JMS client:
package javax.jms;
public interface BytesMessage
extends Message {
public byte readByte( ) throws JMSException;
public void writeByte(byte value) throws JMSException;
public int readUnsignedByte( ) throws JMSException;
public int readBytes(byte[] value) throws JMSException;
public void writeBytes(byte[] value) throws JMSException;
public int readBytes(byte[] value, int length)
throws JMSException;
public void writeBytes(byte[] value, int offset, int length)
throws JMSException;
public boolean readBoolean( ) throws JMSException;
public void writeBoolean(boolean value) throws JMSException;
public char readChar( ) throws JMSException;
public void writeChar(char value) throws JMSException;
public short readShort( ) throws JMSException;
public void writeShort(short value) throws JMSException;
public int readUnsignedShort( ) throws JMSException;
public void writeInt(int value) throws JMSException;
public int readInt( ) throws JMSException;
public void writeLong(long value) throws JMSException;
public long readLong( ) throws JMSException;
public float readFloat( ) throws JMSException;
public void writeFloat(float value) throws JMSException;
public double readDouble( ) throws JMSException;
public void writeDouble(double value) throws JMSException;
public String readUTF( ) throws JMSException;
public void writeUTF(String value) throws JMSException;
public void writeObject(Object value) throws JMSException;
public void reset( ) throws JMSException;
}
If you've worked with the
java.io.DataInputStream
and
java.io.DataOutputStream
classes, then the methods
of the BytesMessage
interface, which are loosely
based on these I/O classes, will look familiar to you. Most of the
methods defined in BytesMessage
interface allow
the application developer to read and write data to a byte stream
using Java's primitive data types. When a Java
primitive is written to the BytesMessage
, using
one of the set<TYPE>(
)
methods, the primitive value is
converted to its byte representation and appended to the stream.
Here's how a BytesMessage
is created and how
values are written to its byte stream:
BytesMessage bytesMessage = session.createBytesMessage( ); bytesMessage.writeChar('R'), bytesMessage.writeInt(10); bytesMessage.writeUTF("OReilly"); queueSender.send(bytesMessage);
When a BytesMessage
is received by a JMS consumer,
the payload is a raw byte stream, so it is possible to read the
stream using arbitrary types, but this will probably result in
erroneous data. It's best to read the
BytesMessage
's payload in the same order,
and with the same types, with which it was written:
public void onMessage(Message message) { try { BytesMessage bytesMessage = (BytesMessage)message; char c = bytesMessage.readChar( ); int i = bytesMessage.readInt( ); String s = bytesMessage.readUTF( ); } catch (JMSException jmse){ ... }
In order to read and write String
values, the
BytesMessage
uses methods based on the UTF-8
format, which is a standard format for transferring and storing
Unicode text data efficiently.
The methods for accessing the
short
and byte
primitives
include unsigned methods
(readUnsignedShort(
)
, readUnsignedByte(
)
). These methods are something of a surprise,
since the short
and byte
data
types in Java are almost always signed. The values that can be taken
by unsigned byte
and short
data
are what you'd expect:
to 255 for a byte
, and
to 65535 for a short
. Because these values
can't be represented by the (signed) byte
and short
data types, readUnsignedByte(
)
and readUnsignedShort( )
both return
an int
.
In addition to the methods for accessing primitive data types, the
BytesMessage
includes a single
writeObject( )
method. This is used for
String
objects and the primitive wrappers:
Byte
, Boolean
,
Character
, Short
,
Integer
, Long
,
Float
, Double
. When written to
the BytesMessage
, these values are converted to
the byte form of their primitive counterparts. The
writeObject( )
method is provided as a convenience
when the types to be written aren't known until runtime.
If an exception is thrown
while reading the BytesMessage
, the pointer in the
stream must be reset to the position it had just prior to the read
operation that caused the exception. This allows the JMS client to
recover from read errors without losing its place in the stream.
The reset( )
method returns the stream pointer to the
beginning of the stream and puts the BytesMessage
in read-only mode so that the contents of its byte stream cannot be
further modified. This method can be called explicitly by the JMS
client if needed, but it's always called implicitly when the
BytesMessage
is delivered.
In most cases, one of the other message types is a better option then
the BytesMessage
. BytesMessage
should only be used if the data needs to be delivered in the
consumer's native format. In some cases, a JMS client may be a
kind of router, consuming messages from one source and delivering
them to a destination. Routing applications may not need to know the
contents of the data they transport and so may choose to transfer
payloads as binary data, using a BytesMessage
,
from one location to another.
The StreamMessage
carries a stream of primitive Java types (int
,
double
, char
, etc.) as its
payload. It provides a set of convenience methods for mapping a
formatted stream of bytes to Java primitives. Primitive types are
read from the Message
in the same order they were
written. Here's the definition of the
StreamMessage
interface:
public interface StreamMessage
extends Message {
public boolean readBoolean( ) throws JMSException;
public void writeBoolean(boolean value) throws JMSException;
public byte readByte( ) throws JMSException;
public int readBytes(byte[] value) throws JMSException;
public void writeByte(byte value) throws JMSException;
public void writeBytes(byte[] value) throws JMSException;
public void writeBytes(byte[] value, int offset, int length)
throws JMSException;
public short readShort( ) throws JMSException;
public void writeShort(short value) throws JMSException;
public char readChar( ) throws JMSException;
public void writeChar(char value) throws JMSException;
public int readInt( ) throws JMSException;
public void writeInt(int value) throws JMSException;
public long readLong( ) throws JMSException;
public void writeLong(long value) throws JMSException;
public float readFloat( ) throws JMSException;
public void writeFloat(float value) throws JMSException;
public double readDouble( ) throws JMSException;
public void writeDouble(double value) throws JMSException;
public String readString( ) throws JMSException;
public void writeString(String value) throws JMSException;
public Object readObject( ) throws JMSException;
public void writeObject(Object value) throws JMSException;
public void reset( ) throws JMSException;
}
On the surface, the
StreamMessage
strongly resembles the
BytesMessage
, but they are not the same. The
StreamMessage
keeps track of the order and types
of primitives written to the stream, so formal conversion rules
apply. For example, an exception would be thrown if you tried to read
a long
value as a short
:
StreamMessage streamMessage = session.createStreamMessage( ); streamMessage.writeLong(2938302); // The next line throws a JMSException short value = streamMessage.readShort( );
While this would work fine with a BytesMessage
, it
won't work with a StreamMessage
. A
BytesMessage
would write the
long
as 64 bits (8 bytes) of raw data, so that you
could later read some of the data as a short
,
which is only 16 bits (the first 2 bytes of the long). The
StreamMessage
, on the other hand, writes the type
information as well as the value of the long
primitive, and enforces a strict set of conversion rules that prevent
reading the long
as a short
.
Table 3.1 shows the
conversion rules
for each type. The left column shows the type written, and the right
column shows how that type may be read. A
JMSException
is thrown by the accessor methods to
indicate that the original type could not be converted to the type
requested. This is the exception that would be thrown if you
attempted to read long
as a
short
.
Table 3.1. Type Conversion Rules
String
values can be
converted to any
primitive data type if they are formatted
correctly. If the String
value cannot be converted
to the primitive type requested, a
java.lang.NumberFormatException
is thrown.
However, most primitive values can be accessed as a
String
using the readString( )
method. The only exceptions to this rule are char
values and byte
arrays, which cannot be read as
String
values.
The writeObject( )
method follows the rules
outlined for the similar method in the
BytesMessage
class. Primitive wrappers are
converted to their primitive counterparts. The readObject(
)
method returns the appropriate object wrapper for
primitive values, or a String
or a
byte
array, depending on the type that was written
to the stream. For example, if a value was written as a primitive
int
, it can be read as a
java.lang.Integer
object.
The StreamMessage
also allows null
values to be written to the stream. If a JMS client attempts to read
a null
value using the readObject(
)
method, null
is returned. The rest of
the primitive accessor methods attempt to convert the
null
value to the requested type using the
valueOf( )
operations. The readBoolean(
)
method returns false
for
null
values, while the other primitive property
methods throw the java.lang.NumberFormatException
.
The readString( )
method returns
null
or possibly an empty String
("")
depending on the implementation. The
readChar( )
method throws a
NullPointerException
.
If an exception is thrown
while reading the StreamMessage
, the pointer in
the stream is reset to the position it had just prior to the read
operation that caused the exception. This allows the JMS client to
recover gracefully from exceptions without losing the pointer's
position in the stream.
The reset( )
method returns the stream pointer to
the beginning of the stream and puts the message in a read-only mode.
It is called automatically when the message is delivered to the
client. However, it may need to be called directly by the consuming
client when a message is redelivered:
if ( strmMsg.getJMSRedelivered( ) ) strmMsg.reset( );
This type
carries a set of name-value pairs as its
payload. The payload is similar to a
java.util.Properties
object, except the values can
be
Java primitives (or their wrappers) in addition
to String
s. The MapMessage
class is useful for delivering keyed data that may change from one
message to the next:
public interface MapMessage extends Message { public boolean getBoolean(String name) throws JMSException; public void setBoolean(String name, boolean value) throws JMSException; public byte getByte(String name) throws JMSException; public void setByte(String name, byte value) throws JMSException; public byte[] getBytes(String name) throws JMSException; public void setBytes(String name, byte[] value) throws JMSException; public void setBytes(String name, byte[] value, int offset, int length) throws JMSException; public short getShort(String name) throws JMSException; public void setShort(String name, short value) throws JMSException; public char getChar(String name) throws JMSException; public void setChar(String name, char value) throws JMSException; public int getInt(String name) throws JMSException; public void setInt(String name, int value) throws JMSException; public long getLong(String name) throws JMSException; public void setLong(String name, long value) throws JMSException; public float getFloat(String name) throws JMSException; public void setFloat(String name, float value) throws JMSException; public double getDouble(String name) throws JMSException; public void setDouble(String name, double value) throws JMSException; public String getString(String name) throws JMSException; public void setString(String name, String value) throws JMSException; public Object getObject(String name) throws JMSException; public void setObject(String name, Object value) throws JMSException; public Enumeration getMapNames( ) throws JMSException; public boolean itemExists(String name) throws JMSException; }
Essentially, MapMessage
works similarly to JMS
properties: any name-value pair can be written to the payload. The
name must be a String
object, and the value may be
a String
or a primitive type. The values written
to the MapMessage
can then be read by a JMS
consumer using the name as a key:
MapMessage mapMessage = session.createMapMessage( ); mapMessage.setInt("Age", 88); mapMessage.setFloat("Weight", 234); mapMessage.setString("Name", "Smith"); mapMessage.setObject("Height", new Double(150.32)); .... int age = mapMessage.getInt("Age"); float weight = mapMessage.getFloat("Weight"); String name = mapMessage.getString("Name"); Double height = (Double)mapMessage.getObject("Height");
The setObject( )
method writes a Java primitive wrapper type,
String
object, or byte array. The primitive
wrappers are converted to their corresponding primitive types when
set. The getObject(
)
method reads String
s, byte arrays, or any
primitive type as its corresponding primitive wrapper.
The conversion rules defined for the
StreamMessage
apply to the
MapMessage
. See Table 3.1 in
the StreamMessage
section.
A JMSException
is thrown by the accessor methods
to indicate that the original type could not be converted to the type
requested. In addition, String
values can be
converted to any primitive value type if they are formatted
correctly; the accessor will throw a
java.lang.NumberFormatException
if they
aren't.
If a JMS client attempts to read a
name-value pair that doesn't exist, the value is treated as if
it was null
. Although the getObject(
)
method returns null
for nonexistent
mappings, the other types behave differently. While most primitive
accessors throw the
java.lang.NumberFormatException
if a
null
value or nonexistent mapping is read, other
accessors behave as follows: the getBoolean( )
method returns false
for null
values; the getString( )
returns a
null
value or possibly an empty String
(""
), depending on the implementation;
and the getChar( )
method throws a
NullPointerException
.
To avoid reading nonexistent name-value pairs, the
MapMessage
provides an itemExists(
)
test method. In addition, the getMapNames(
)
method lets a JMS client enumerate the names and use them
to obtain all the values in the message. For example:
public void onMessage(Message message) { MapMessage mapMessage = (MapMessage)message; Enumeration names = mapMessage.getMapNames( ); while(names.hasMoreElements( )){ String name = (String)names.nextElement( ); Object value = mapMessage.getObject(name); System.out.println("Name = "+name+", Value = "+value); } }
When
messages
are delivered, the body of the message is made read-only. Any attempt
to alter a message body after it has been delivered results in a
javax.jms.MessageNotWriteableException
. The only
way to change the body of a message after it has been delivered is to
invoke the clearBody(
)
method, which is defined in the Message
interface.
The clearBody( )
method empties the
message's payload so that a new payload can be added.
Properties are also read-only after a message is delivered. Why are
both the body and properties made read-only after delivery? It allows
the JMS provider more flexibility in implementing the
Message
object. For example, a JMS provider may
choose to stream a BytesMessage
or
StreamMessage
as it is read, rather than all at
once. Another vendor may choose to keep properties or body data in an
internal buffer so that it can be read directly without the need to
make a copy, which is especially useful with multiple consumers on
the same client.
The
acknowledge( )
method, defined in the
Message
interface, is used when the consumer has
chosen CLIENT_ACKNOWLEDGE
as its acknowledgment
mode. There are three
acknowledgment modes that
may be set by the JMS consumer when its session is created:
AUTO_ACKNOWLEDGE
,
DUPS_OK_ACKNOWLEDGE
, and
CLIENT_ACKNOWLEDGE
. Here is how a pub/sub consumer
sets one of the three acknowledgment modes:
TopicSession topic = topicConnection.createTopicSession(false, Session.CLIENT_ACKNOWLEDGE);
In
CLIENT_ACKNOWLEDGE
mode, the JMS client explicitly
acknowledges each message as it is received. The
acknowledge( )
method on the
Message
interface is used for this purpose. For
example:
public void onMessage(Message message){ message.acknowledge( ); ... }
The other acknowledgment modes do not require the use of this method and are covered in more detail in Chapter 6 and Appendix B.
Any acknowledgment mode specified for a transacted session is ignored. When a session is transacted, the acknowledgment is part of the transaction and is executed automatically prior to the commit of the transaction. If the transaction is rolled back, no acknowledgment is given. Transactions are covered in more detail in Chapter 6.
A
message
delivered by a JMS client may be converted to a JMS provider's
native format and delivered to non-JMS clients, but it must still be
consumable as its original Message
type by JMS
clients. Messages delivered from non-JMS clients to a JMS provider
may be consumable by JMS clients—the JMS provider should
attempt to map the message to its closest JMS type, or if
that's not possible, to the BytesMessage
.
JMS providers are not required to be interoperable. A message published to one JMS provider's server is not consumable by another JMS provider's consumer. In addition, a JMS provider usually can't publish or read messages from destinations (topic and queues) implemented by another JMS provider. Most JMS providers have, or will have in the future, bridges or connectors to address this issue.
Although interoperability is not required, limited message portability is required. A message consumed or created using JMS provider A can be delivered using JMS provider B. JMS provider B will simply use the accessor methods of the message to read its headers, properties, and payload and convert them to its own native format: not a fast process, but portable. This portability is limited to interactions of the JMS client, which takes a message from one provider and passes it to another.
[2] It's possible that a JMS provider
could use CORBA 2.3 IIOP protocol, which can handle
ObjectMessage
types consumed by non-Java, non-JMS
clients.