Bitter Messages: Java* Messaging Anti-Patterns

by Bruce A. Tate;
programming examples by Mike Clark

Introduction

Recently, JMS* has brought the joys of asynchronous messaging to the Java* platform. The primary benefit is clear: you don't have to wait if there's no business requirement to do so. But as you've seen through this article series, pleasure frequently comes with pain. This month, we'll look into some of the anti-patterns that center around the Java messaging architecture. Once again, Mike Clark of Clarkware Consulting* assists with the article, providing the code examples and many of the antipatterns from his chapter in the coming book, Bitter EJB*.


Understanding JMS

JMS is a specification that defines an interface to a Java-compliant messaging service. It's the interface specification, not the implementation, since implementation details are left to providers. JMS provides a way to produce and consume messages, as depicted in Figure 1:



As such, the messaging API is deceptively simple. For the most part, you create, send, and receive messages. The resulting applications are loosely-coupled, flexible, reasonably efficient, and can be extremely reliable. Further, since the JMS specification can share the JTA* transaction context, you can achieve sufficient complexity to handle amazingly robust, transactionally-sound applications.

MDBs*, or message-driven beans, allow you to concurrently consume JMS messages in the context of an EJB* application. MDBs are transactional, stateless, and component-oriented consumers of asynchronous messages. If you'd like to know more about JMS, you can read about it at http://java.sun.com/products/jms/*.

So JMS is a simple messaging API that's standardized around message-oriented middleware, and MDB is a component model that supports it in the context of an EJB* container. The next question is this: what can go wrong as you build JMS applications? We'll discuss anti-patterns in each of the three major areas of JMS applications:

  • What's in your message? The contents of your message will depend on your application, but it's easy to stray from practical and efficient design principles. Your message is your interface. If a message is too bloated or too complex, then your interface will have problems.
  • How do you consume your messages? The consumer must often be the most elegant piece of a messaging solution. Consumers can have broad responsibilities, ranging from filtering and distributing to handling messages. It takes some skill and finesse to design a good consumer. It's all too easy to build one simple consumer that grows into an all-consuming, amorphous blob.
  • How do you create your message? Contrary to intuition, the creation of the message is probably the least important of the three major components of a JMS application. Once you've defined the contents, creating and sending a message is relatively straightforward, but you should still be aware of a few anti-patterns like string management.

 

Let's take a look at each in more detail, casting a critical eye toward what's likely to fail. We're interested in each element: the payload, the message type, and the message itself.


The Right Message

The most fundamental building block for any loosely-coupled system based on messaging is the message content. You can divide the content into payload type and structure. Just as effective Java programming demands good interfaces, messaging applications depend on the content and structure of the message. When you build a message, JMS allows several options for message structure:

  • Message. Often, a message such as an acknowledgement does not need to carry a payload.
  • TextMessage. (Simple). To JMS, text is text, but we'll divide text messages into simple and decorated versions. A simple text message uses plain text, with simple delimiters for structure. Simple text is easy to create, easy to consume, and reasonably efficient for some applications. The disadvantages are that they provide no meta-data (leading to tighter coupling), poor performance for random access, and ugly parsing.
  • TextMessage. (Decorated). You can add tags to your text to give hints about the structure. Of course, no other technology has transformed messaging like XML. With it, you can structure the full semantics of the message. The Simple Object Access Protocol (SOAP) does exactly that: it provides a convenient, generic standard structure for messages. You can also use XML to structure only the message payload. One advantage of this option is that you can provide meta-information with the message. A disadvantage is that you must pay a significant processing penalty to produce and consume your messages. We'll say more about this topic when we discuss the impact of XML on producers and consumers.
  • Objects. If your producer and consumer are both built using the same language, then you could serialize an object for your message payload. The advantage of this option is that it's an efficient way to create and consume the message. The disadvantage is that this method depends somewhat on the complexity of the serialized object, as deep inheritance graphs can mercilessly bog performance. You also limit your consumers to those who can reconstruct the object.
  • MappedMessage. The MappedMessage is perhaps the most versatile message type. The message "map" contains predefined keys that point to locations in the message payload. In this way, you can send a message that's flexible and dynamic in nature, supporting ideas like optional parameters with ease. The advantage of this option is that mapped messages allow loose coupling, very efficient random access, and reasonable flexibility of message types. The disadvantage is that you do have to send the message maps with the message, which can limit performance.
  • BytesMessage and StreamMessage. Using raw bytes, the producers and consumers using these types of messages must understand the full structure of the payload. The StreamMessage retains the order and type of the parameters in the payload, but the BytesMessage doesn't. The advantage of this option is that these messages can be very efficient. The disadvantage is that the producer and consumer need to be tightly coupled. BytesMessages are rarely used, unless you're sending a well-understood, standardized message like a MIME type.

 

Of course, all of the structure in the world won't help you if you don't pay close attention to content. Too often, messages contain extraneous data that doesn't need to be included for the message to perform its function. Let's look at an antipattern related to message content.

Anti-Pattern: Fat Messages

When developers create a message structure, few put much thought into optimizing the message, but you should. You should only transmit the minimum message that allows a consumer to do the required job. How does extra data sneak into our messages?

  • Derived data. Don't include data that can easily be derived by the consumer. For example, you can easily derive an age from a birth date.
  • Data that satisfies future requirements. Don't speculate. If you don't need it, don't include it. A message should satisfy a current set of business requirements - not current and possible future requirements.
  • Duplication. Don't duplicate for convenience. Many XML and object structures can duplicate data. For example, if the billing and shipping addresses are the same, then default the shipping address to the billing address, and process that information at the client.
  • Deep object graphs. When you serialize an object, keep in mind that you're serializing the whole ancestor chain. If it's too deep, and you're not achieving full value for the size, then you should use another type of message payload.

 

Realistically, your message is your interface. Design it as closely and carefully as you do your major method interfaces.


Streamlining Production and Consumption

When you're processing your messages, complexity is your enemy. We'll look at two ways that many developers needlessly complicate producers or consumers. You can battle complexity in two primary ways. As an illustration of the first, we choose the wrong message payload type, which explodes the complexity and overhead for both our producers and our consumers.

Anti-Pattern: Golden Hammers

When you've got a golden hammer, everything looks like a nail. For messaging software, XML is the ultimate golden hammer. Your choice of the type of message payload will have a critical impact on the complexity of your message processing. Keep in mind when you swing that XML hammer that the type of message that you send should depend on your application. Sure, XML has clear advantages that attract messaging aficionados like the buttered side of toast attracts the floor: a good meta-model, compelling structure, richness, and flexibility. The problem is that XML's cost and complexity are often prohibitive for simple problems. For this reason, you should limit your use of XML to a limited set of circumstances:

  • Loose coupling outweighs your performance concerns.
  • Your application needs heavy meta-data.
  • Producers and consumers are disparate applications, possibly with different maintenance or development schedules.

 

Solution: The rule of thumb is that you should use XML "around the edges" of your application, where performance requirements are more lenient and meta-data is more important, such as writing to a file or communicating with an external application. Internally, your application should use a more specialized mechanism like a MappedMessage or an ObjectMessage. Figure 2 shows an example. A point-of-sale application communicates internally with mapped messages, but periodically updates inventory with an external application using XML messages.



Note that our messaging model can change based on the requirements of the application. The cash register and the accounting server are within the same application, in the same language, and on the same maintenance schedule. We can and should trade higher performance and simplicity for looser coupling. The inventory application has different circumstances, however. It's a different application with a different set of requirements, and it is probably under a different maintenance schedule. XML is appropriate in this case. As we'll see, XML is not the only case of processing complexity.

Anti-Pattern: The Monolithic Consumer

Often, developers will divide a messaging application along the clear line between producer and consumer, and believe that they've arrived at a completed design. That's usually not enough. Take a look at the following consumer for an order-entry EJB component. This is the consumer, in the form of a message-driven bean.

public class OrderRequestReceiverMDB

implements javax.ejb.MessageDrivenBean,

javax.jms.MessageListener {

   
private MessageDrivenContext ctx;
   
   
public void setMessageDrivenContext(MessageDrivenContext ctx) {
      
this.ctx = ctx;
   
}

   
public void ejbCreate() {}
   

public void onMessage(Message message) 
{

      
if (message instanceof MapMessage) {
         
         
MapMessage mapMessage = (MapMessage)message;
      
         
try {
   
            
String orderId = mapMessage.getString("Order ID");
            
String productId = mapMessage.getString("Product ID");
            
int quantity = mapMessage.getInt("Quantity");
            
double price = mapMessage.getDouble("Price");
         
            
OrderRequest orderRequest =
               
 new OrderRequest(orderId, productId, quantity, price);
      
            
recordOrder(orderRequest);
               
            
OrderStatus status = fulfillOrder(orderRequest);
               
            
notifyOrderStatusSubscribers(status);
                        
         
} catch (JMSException jmse) {
            
jmse.printStackTrace();
         
}
         
      
} 
else 
{
         
System.err.println("OrderRequest must be a MapMessage type!");
      
}
   
}
   
   
public void ejbRemove() {}

}


 

The snippet is not too long, but it's more complex than it needs to be. It also fails to separate the concerns of the business model and the consumer. Just as we don't want to process business logic in our views, nor do we want business logic to creep into our consumers. Take a look at the bold lines. These three methods clearly belong in the business model. They assume business rules, or the absence of them, based on the sequence and conditions that the MDB calls them.

Solution: Delegate. In our case, we can easily refactor the complex business methods to a message handler. We don't need to build a full EJB. A plain old java object (POJO) suits our purposes fine as a handler. We then use the MDB as a thin shell that simply receives the request, creates an object, and marshals the data directly to our handler. Here's the refactored code for the onMessage method:

public void onMessage(Message message) {

   
if (message instanceof MapMessage) {
         
         
MapMessage mapMessage = (MapMessage)message;
      
         
try {
            
            
String orderId = mapMessage.getString("Order ID");
            
String productId = mapMessage.getString("Product ID");
            
int quantity = mapMessage.getInt("Quantity");
            
double price = mapMessage.getDouble("Price");
         
            
OrderRequest orderRequest =
               
new OrderRequest(orderId, productId, quantity, price);

            
OrderRequestHandler handler = new OrderRequestHandler();
            
handler.handle(orderRequest);
            
         
} catch (JMSException jmse) {
            
jmse.printStackTrace();
         
}
         
   
} 
else {
      
System.err.println("OrderRequest must be a MapMessage type!");
   
}

}


 

In general, you will want to maintain these characteristics in your messaging methods in your producers and consumers:

  • Keep them short. They'll be much tougher to maintain with length.
  • Separate the concerns. Let your message handlers message, and let your business model do its business. This philosophy will make it easier to change your middleware components, to maintain and extend your business model, and to build automated tests.
  • Write your unit tests first. It's much tougher to design a JUnit* test for a poorly designed component.

When you put away that golden hammer and think about the design of producers and consumers, you'll be amazed at the opportunities for a simpler design and better performance. Through effective choices in delegation and your messaging model, you can dramatically simplify your producers and consumers.


Putting It All Together

We've taken a brief survey through messaging antipatterns. In my latest project, Bitter EJB (due first quarter 2003 by Manning Publications*), you'll find more perceptive messaging antipatterns, including the Packrat, the Hot Potato, and the Slow Eater. In other books like Ed Roman's "Mastering EJB," you can also find Poison Messages. All of these antipatterns have the same roots: poor message design or poor strategies for production or consumption.


About the Authors

Bruce Tate is an independent consultant and author. He has had fifteen years of development experience, ranging from development at two startups to systems development and consulting at IBM*. Over his career, he has authored three books and published eight patents. His most recent book is the smash hit, Bitter Java*. He is working with Mike Clark on his next project, Bitter EJB.

Mike Clark is an independent consultant and founder of Clarkware Consulting, Inc.* (www.clarkware.com*). When he's not in the trenches helping teams build better software faster, he's an author and frequently speaks on J2EE*, JMS, and JUnit at a highly respected programming symposium.


Per informazioni più dettagliate sulle ottimizzazioni basate su compilatore, vedere il nostro Avviso sull'ottimizzazione.