| Last Modified On : | October 22, 2008 4:15 PM PDT |
Rate |
|
by Bruce A. Tate;
programming examples by Mike Clark
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*.
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:
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 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:
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.
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?
Realistically, your message is your interface. Design it as closely and carefully as you do your major method interfaces.
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.
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:
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.
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:
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.
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.
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.
