Message Types

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.

Message

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.

TextMessage

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.

ObjectMessage

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.

BytesMessage

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.

StreamMessage

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

write<TYPE>( )

read<TYPE>( )

boolean
boolean, String
byte
byte, short, int, long, String
short
short, int, long, String
char
char, String
Long
long, String
Int
int, long, String
float
float, double, String
double
double, String
String
String, boolean, byte, short, int, long, float, double
byte []
byte []

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(  );

MapMessage

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 Strings. 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 Strings, 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);
   }
}

Read-Only Messages

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.

Client-Acknowledged Messages

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.

Note

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.

Interoperability and Portability of Messages

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset