|
Related Reading
|
This excerpt is Chapter 13 from Enterprise JavaBeans, 3rd Edition, published in September 2001 by O'Reilly.
This chapter is divided into two subsections: "JMS as a Resource" and "Message-Driven Beans." The first section describes the Java Message Service (JMS) and its role as a resource that is available to any enterprise bean (session, entity, or message-driven). Readers unfamiliar with JMS should read the first section before proceeding to the second section.
If you're already familiar with JMS, you can skip directly to the second section, which provides an overview of the new enterprise bean type -- the message-driven bean. A message-driven bean is an asynchronous bean activated by message delivery. In EJB 2.0, vendors are required to support JMS-based message-driven bean, that receive JMS messages from specific topics or queues and process those messages as they are delivered.
All EJB 2.0 vendors must, by default, support a JMS provider. Most EJB 2.0 vendors have a JMS provider built in, but some may also support other JMS providers. Regardless of how the EJB 2.0 vendor provides the JMS service, having one is a requirement if the vendor expects to support message-driven beans. The advantage of this forced adoption of JMS is that EJB developers can expect to have a working JMS provider on which messages can be both sent and received.
Reimplementing the TravelAgent EJB with JMS
TopicConnectionFactory and Topic
TopicConnection and TopicSession
TopicPublisher
Message types
JMS application client
JMS Messaging Models: Publish-and-Subscribe and Point-to-Point
Publish-and-subscribe
Point-to-point
Which messaging model should you use?
Entity and Session Beans Should Not Receive Messages
The ReservationProcessorBean Class
MessageDrivenBean interface
MessageDrivenContext
MessageListener interface
The onMessage( ) method: Workflow and integration for B2B
Sending messages from a message-driven bean
XML deployment descriptor
<message-selector>
Message selector examples
<acknowledge-mode>
<message-driven-destination>
The ReservationProcessor clients
The Life Cycle of a Message-Driven Bean
Does Not Exist
The Method-Ready Pool
Transitioning to the Method-Ready Pool
Life in the Method-Ready Pool
Transitioning out of the Method-Ready Pool: The death of an MDB instance
|
JMS is a standard vendor-neutral API that is part of the J2EE platform and can be used to access enterprise messaging systems. Enterprise messaging systems (a.k.a. message-oriented middleware) facilitate the exchange of messages among software applications over a network. JMS is analogous to JDBC: whereas JDBC is an API that can be used to access many different relational databases, JMS provides the same vendor-independent access to enterprise messaging systems. Many enterprise messaging products currently support JMS, including IBM's MQSeries, BEA's WebLogic JMS service, Sun Microsystems' iPlanet Message Queue, and Progress' SonicMQ, to name a few. Software applications that use the JMS API for sending or receiving messages are portable across brands of JMS vendors.
Java applications that use JMS are called JMS clients, and the messaging system that handles routing and delivery of messages is called the JMS provider. A JMS application is a business system composed of many JMS clients and, generally, one JMS provider.
A JMS client that sends a message is called a producer, while a JMS client that receives a message is called a consumer. A single JMS client can be both a producer and a consumer. When we use the terms consumer and producer, we mean a JMS client that receives messages or sends messages, respectively.
In EJB, enterprise beans of all types can use JMS to send messages to various destinations. Those messages are consumed by other Java applications or message-driven beans. JMS facilitates sending messages from enterprise beans by using a messaging service, sometimes called a message broker or router. Message brokers have been around for a couple of decades--the oldest and most established being IBM's MQSeries--but JMS is fairly new and is specifically designed to deliver a variety of message types from one Java application to another.
We can modify the TravelAgent EJB developed in Chapter 12 so
that it uses JMS to alert some other Java application that a reservation has
been made. The following code shows how to modify the bookPassage() method so that the TravelAgent EJB will
send a simple text message based on the description information from the TicketDO object:
public TicketDO bookPassage(CreditCardDO card, double price)
throws IncompleteConversationalState {
if (customer == null || cruise == null || cabin == null) {
throw new IncompleteConversationalState();
}
try {
ReservationHomeLocal resHome = (ReservationHomeLocal)
jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal");
ReservationLocal reservation =
resHome.create(customer, cruise, cabin, price, new Date());
Object ref = jndiContext.lookup
("java:comp/env/ejb/ProcessPaymentHomeRemote");
ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote)
PortableRemoteObject.narrow(ref, ProcessPaymentHomeRemote.class);
ProcessPaymentRemote process = ppHome.create();
process.byCredit(customer, card, price);
TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
String ticketDescription = ticket.toString();
TopicConnectionFactory factory = (TopicConnectionFactory)
jndiContext.lookup("java:comp/env/jms/TopicFactory");
Topic topic = (Topic)
jndiContext.lookup("java:comp/env/jms/TicketTopic");
TopicConnection connect = factory.createTopicConnection();
TopicSession session = connect.createTopicSession(true,0);
TopicPublisher publisher = session.createPublisher(topic);
TextMessage textMsg = session.createTextMessage();
textMsg.setText(ticketDescription);
publisher.publish(textMsg);
connect.close();
return ticket;
} catch(Exception e) {
throw new EJBException(e);
}
}
We needed to add a lot of new code to send a message. However, while it may look a little overwhelming at first, the basics of JMS are not all that complicated.
In order to send a JMS message we need a connection to the JMS
provider and a destination address for the message. The connection to the JMS
provider is made possible by a JMS connection factory; the destination address
of the message is identified by a Topic object.
Both the connection factory and the Topic object
are obtained from the TravelAgent EJB's JNDI ENC:
TopicConnectionFactory factory = (TopicConnectionFactory)
jndiContext.lookup("java:comp/env/jms/TopicFactory");
Topic topic = (Topic)
jndiContext.lookup("java:comp/env/jms/TicketTopic");
The TopicConnectionFactory in JMS is similar in function to
the DataSource in JDBC. Just as the DataSource provides a JDBC connection to a database, the
TopicConnectionFactory provides a JMS connection to
a message router. (This analogy is not perfect. One
might also say that the TopicSession is analogous
to the DataSource, since both represent
transaction-resources connections.)
The Topic object itself represents a
network-independent destination to which the message will be addressed. In
JMS, messages are sent to destinations--either topics or queues--instead of
directly to other applications. A topic is analogous to an email list or
newsgroup; any application with the proper credentials can receive messages
from and send messages to a topic. When a JMS client receives messages from a
topic, the client is said to subscribe to that topic.
JMS decouples applications by allowing them to send messages to each other
through a destination, which serves as virtual channel.
JMS also supports another destination type, called a Queue. The difference between topics and queues is
explained in more detail later.
The TopicConnectionFactory is used to
create a TopicConnection, which is an actual
connection to the JMS provider:
TopicConnection connect = factory.createTopicConnection();
TopicSession session = connect.createTopicSession(true,0);
Once a TopicConnection is obtained, it can be used to
create a TopicSession. A TopicSession allows the Java developer to group the
actions of sending and receiving messages. In this case you will need only a
single TopicSession. However, having more than one
TopicSession object is frequently helpful: if you
wish to produce and consume messages using multithreading, a different Session needs to be created by each thread accessing that
thread. This is because JMS Session objects use a
single-threaded model, which prohibits concurrent accessing of a single Session from multiple threads. The thread that creates a
TopicSession is usually the thread that uses that
Session's producers and consumers (i.e., TopicPublisher and TopicSubscriber objects). If you wish to produce and
consume messages using multithreading, a different Session should be created and used by each thread.
The createTopicSession() method has
two parameters:
createTopicSession(boolean transacted, int acknowledgeMode)
According to the EJB 2.0 specification, these arguments are
ignored at runtime because the EJB container manages the transaction and
acknowledgment mode of any JMS resource obtained from the JNDI ENC. The
specification recommends that developers use the arguments true for transacted and 0 for acknowlegeMode, but
since they are supposed to be ignored, it should not matter what you use.
Unfortunately, not all vendors adhere to this part of the specification. Some
vendors ignore these parameters while some do not. Consult your vendor's
documentation to determine the proper values for these parameters in both
container-managed and bean-managed transactions.
It's good programming practice to close a TopicConnection after its been used, in order to conserve
resources:
TopicConnection connect = factory.createTopicConnection();
...
connect.close();
|
The TopicSession is used to create a
TopicPublisher, which sends messages from the
TravelAgent EJB to the destination specified by the Topic object. Any JMS clients that subscribe to that
topic will receive a copy of the message:
TopicPublisher publisher = session.createPublisher(topic);
TextMessage textMsg = session.createTextMessage();
textMsg.setText(ticketDescription);
publisher.publish(textMsg);
In JMS, a message is a Java object with two parts: a header and
a message body. The header is composed of delivery information and metadata,
while the message body carries the application data, which can take several
forms: text, serializable objects, byte streams, etc. The JMS API defines
several message types (TextMessage, MapMessage, ObjectMessage, and
others) and provides methods for delivering messages to and receiving messages
from other applications.
For example, we can change the TravelAgent EJB so that it sends
a MapMessage instead of a TextMessage:
TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
...
TopicPublisher publisher = session.createPublisher(topic);
MapMessage mapMsg = session.createTextMessage();
mapMsg.setInt("CustomerID", ticket.customerID.intValue());
mapMsg.setInt("CruiseID", ticket.cruiseID.intValue());
mapMsg.setInt("CabinID", ticket.cabinID.intValue());
mapMsg.setDouble("Price", ticket.price);
publisher.publish(mapMsg);
The attributes of the MapMessage
(CustomerID, CruiseID,
CabinID, and Price) can
be accessed by name from those JMS clients that receive it.
As an alternative, the TravelAgent EJB could be modified to use
the ObjectMessage type, which would allow us to
send the entire TicketDO object as the message
using Java serialization:
TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
...
TopicPublisher publisher = session.createPublisher(topic);
ObjectMessage objectMsg = session.createObjectMessage();
ObjectMsg.setObject(ticket);
publisher.publish(objectMsg);
In addition to the TextMessage, MapMessage, and ObjectMessage,
JMS provides two other message types: StreamMessage
and BytesMessage. StreamMessage can take the contents of an I/O stream as
its payload. BytesMessage can take any array of
bytes, which it treats as opaque data.
When a JMS resource is used, it must be declared in the bean's XML deployment descriptor, in a manner similar to the JDBC resource used by the Ship EJB in Chapter 10:
<enterprise-beans>
<session>
<ejb-name>TravelAgentBean</ejb-name>
...
<resource-ref>
<res-ref-name>jms/TopicFactory</res-ref-name>
<res-type>javax.jms.TopicConnectionFactory</res-type>
<res-auth>Container</res-auth>
</resource-ref>
<resource-ref>
<res-ref-name>jdbc/titanDB</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>
<resource-env-ref>
<resource-env-ref-name>jms/TicketTopic</resource-env-ref-name>
<resource-env-ref-type>javax.jms.Topic</resource-env-ref-type>
</resource-env-ref>
...
</session>
The <resource-ref> for the JMS
TopicConnectionFactory is similar to the <resource-ref> declaration for the JDBC DataSource: it declares the JNDI ENC name, interface
type, and authorization protocol. In addition to the <resource-ref>, the TravelAgent EJB must also
declare the <resource-env-ref>, which lists
any "administered objects" associated with a <resource-ref> entry. In this case, we declare the
Topic used for sending a ticket message. At
deployment time the deployer will map the JMS TopicConnectionFactory and Topic declared by the <resource-ref> and <resource-env-ref> elements to a JMS factory and
topic.
To get a better idea of how JMS is used, we can create a Java
application whose sole purpose is receiving and processing reservation
messages. We will develop a very simple JMS client that simply prints a
description of each ticket as it receives the messages. We'll assume that the
TravelAgent EJB is using the TextMessage to send a
description of the ticket to the JMS clients. The following code shows how the
JMS application client might look:
import javax.jms.Message;
import javax.jms.TextMessage;
import javax.jms.TopicConnectionFactory;
import javax.jms.TopicConnection;
import javax.jms.TopicSession;
import javax.jms.Topic;
import javax.jms.Session;
import javax.jms.TopicSubscriber;
import javax.jms.JMSException;
import javax.naming.InitialContext;
public class JmsClient_1 implements javax.jms.MessageListener {
public static void main(String [] args) throws Exception {
if(args.length != 2)
throw new Exception("Wrong number of arguments");
new JmsClient_1(args[0], args[1]);
while(true){Thread.sleep(10000);}
}
public JmsClient_1(String factoryName, String topicName) throws Exception {
InitialContext jndiContext = getInitialContext();
TopicConnectionFactory factory = (TopicConnectionFactory)
jndiContext.lookup("TopicFactoryNameGoesHere");
Topic topic = (Topic)jndiContext.lookup("TopicNameGoesHere");
TopicConnection connect = factory.createTopicConnection();
TopicSession session =
connect.createTopicSession(false,Session.AUTO_ACKNOWLEDGE);
TopicSubscriber subscriber = session.createSubscriber(topic);
subscriber.setMessageListener(this);
connect.start();
}
public void onMessage(Message message) {
try {
TextMessage textMsg = (TextMessage)message;
String text = textMsg.getText();
System.out.println("\n RESERVATION RECIEVED:\n"+text);
} catch(JMSException jmsE) {
jmsE.printStackTrace();
}
}
public static InitialContext getInitialContext() {
// create vendor-specific JNDI context here
}
}
|
JNDI also allows the properties to
be set in a |
The constructor of JmsClient_1
obtains the TopicConnectionFactory and Topic from the JNDI InitialContext. This context is created with
vendor-specific properties so that the client can connect to the same JMS
provider as the one used by the TravelAgent EJB. For example, the getInitialContext() method for the WebLogic application
server would be coded as follows:
public static InitialContext getInitialContext() {
Properties env = new Properties();
env.put(Context.SECURITY_PRINCIPAL, "guest");
env.put(Context.SECURITY_CREDENTIALS, "guest");
env.put(Context.INITIAL_CONTEXT_FACTORY,
"weblogic.jndi.WLInitialContextFactory");
env.put(Context.PROVIDER_URL, "t3://localhost:7001");
return new InitialContext(env);
}
Once the client has the TopicConnectionFactory and Topic, it creates a TopicConnection and a TopicSession in the same way as the TravelAgent EJB. The
main difference is that the TopicSession object is
used to create a TopicSubscriber instead of a TopicPublisher. The TopicSubscriber is designed specifically to process
incoming messages that are published to its specified Topic:
TopicSession session =
connect.createTopicSession(false,Session.AUTO_ACKNOWLEDGE);
TopicSubscriber subscriber = session.createSubscriber(topic);
subscriber.setMessageListener(this);
connect.start();
The TopicSubscriber can receive
messages directly, or it can delegate the processing of the messages to a
javax.jms.MessageListener. We chose to have JmsClient_1 implement the MessageListener interface so that it can process the
messages itself. MessageListener objects implement
a single method, onMessage(), which is invoked
every time a new message is sent to the subscriber's topic. In this case,
every time the TravelAgent EJB sends a reservation message to the topic, the
JMS client will have its onMessage() method invoked
so that it can receive a copy of the message and process it:
public void onMessage(Message message) {
try {
TextMessage textMsg = (TextMessage)message;
String text = textMsg.getText();
System.out.println("\n RESERVATION RECIEVED:\n"+text);
} catch(JMSException jmsE) {
jmsE.printStackTrace();
}
}
|
One of the principal advantages of JMS messaging is that it's asynchronous. In other words, a JMS client can send a message without having to wait for a reply. Contrast this flexibility with the synchronous messaging of Java RMI. RMI is an excellent choice for assembling transactional components, but is too restrictive for some uses. Each time a client invokes a bean's method it blocks the current thread until the method completes execution. This lock-step processing makes the client dependent on the availability of the EJB server, resulting in a tight coupling between the client and enterprise bean.
In JMS, a client sends messages asynchronously to a destination (topic or queue), from which other JMS clients can also receive messages. When a JMS client sends a message, it doesn't wait for a reply; it sends the message to a router, which is responsible for forwarding it to other clients. Clients sending messages are decoupled from the clients receiving them; senders are not dependent on the availability of receivers.
The limitations of RMI make JMS an attractive alternative for communicating with other applications. Using the standard JNDI environment naming context, an enterprise bean can obtain a JMS connection to a JMS provider and use it to deliver asynchronous messages to other Java applications. For example, a TravelAgent session bean can use JMS to notify other applications that a reservation has been processed, as shown in Figure 13-1.
|
In this case, the applications receiving JMS messages from the TravelAgent EJB may be message-driven beans, other Java applications in the enterprise, or applications in other organizations that benefit from being notified that a reservation has been processed. Examples might include business partners who share customer information or an internal marketing application that adds customers to a catalog mailing list.
JMS enables the enterprise bean to send messages without blocking. The enterprise bean doesn't know who will receive the message, because it delivers the message to a virtual channel (destination) and not directly to another application. Applications can choose to receive messages from that virtual channel and receive notification of new reservations.
One interesting aspect of enterprise messaging is that the decoupled asynchronous nature of the technology means that the transactions and security contexts of the sender are not propagated to the receiver of the message. For example, when the TravelAgent EJB sends the ticket message, it may be authenticated by the JMS provider but its security context won't be propagated to the JMS client that received the message. When a JMS client receives the message from the TravelAgent EJB, it will have no idea about the security context under which the message was sent. This is how it should be, because the sender and receiver often operate in different environments with different security domains.
Similarly, transactions are never propagated from the sender to the receiver. For one thing, the sender has no idea who the receivers of the message will be. If the message is sent to a topic there could be one receiver or thousands; managing a distributed transaction under such ambiguous circumstances is not tenable. In addition, the clients receiving the message may not get it for a long time after it is sent because they are temporarily down or otherwise unable to receive messages. One key strength of JMS is that it allows senders and receivers to be temporally decoupled. Transactions are designed to be executed quickly because they lock up resources; the possibility of a long transaction with an unpredictable end is also not tenable.
A JMS client can, however, have a distributed transaction with the JMS provider so that it manages the send or receive operation in the context of a transaction. For example, if the TravelAgent EJB's transaction fails for any reason, the JMS provider will discard the ticket message sent by the TravelAgent EJB. Transactions and JMS are covered in more detail in Chapter 14.
JMS provides two types of messaging models: publish-and-subscribe and point-to-point. The JMS specification refers to these as messaging domains. In JMS terminology, publish-and-subscribe and point-to-point are frequently shortened to pub/sub and p2p (or PTP), respectively. This chapter uses both the long and short forms throughout.
In the simplest sense, publish-and-subscribe is intended for a one-to-many broadcast of messages, while point-to-point is intended for one-to-one delivery of messages (see Figure 13-2).
|
In publish-and-subscribe messaging, one producer can send a message to many consumers through a virtual channel called a topic. Consumers can choose to subscribe to a topic. Any messages addressed to a topic are delivered to all the topic's consumers. Every consumer receives a copy of each message. The pub/sub messaging model is by and large a push-based model, where messages are automatically broadcast to consumers without them having to request or poll the topic for new messages.
In the pub/sub messaging model, the producer sending the message is not dependent on the consumers receiving the message. Optionally, JMS clients that use pub/sub can establish durable subscriptions that allow consumers to disconnect and later reconnect and collect messages that were published while they were disconnected.
The TravelAgent EJB in this chapter uses the pub/sub programming
model with a Topic object as a destination.
The point-to-point messaging model allows JMS clients to send and receive messages both synchronously and asynchronously via virtual channels known as queues. The p2p messaging model has traditionally been a pull- or polling-based model, where messages are requested from the queue instead of being pushed to the client automatically. (The JMS specification does not specifically state how the p2p and pub/sub models must be implemented. Either one may use push or pull, but at least conceptually pub/sub is push and p2p is pull.)
A queue may have multiple receivers, but only one receiver may receive each message. As shown earlier in Figure 13-2, the JMS provider will take care of doling out the messages among JMS clients, ensuring that each message is consumed by only one JMS client. The JMS specification does not dictate the rules for distributing messages among multiple receivers.
The messaging API for p2p is similar to that used for pub/sub. The following code listing shows how the TravelAgent EJB could be modified to use the queue-based p2p API instead of the topic-based pub/sub model used in the earlier example:
public TicketDO bookPassage(CreditCardDO card, double price)
throws IncompleteConversationalState {
...
TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
String ticketDescription = ticket.toString();
QueueConnectionFactory factory = (QueueConnectionFactory)
jndiContext.lookup("java:comp/env/jms/QueueFactory");
Queue queue = (Queue)
jndiContext.lookup("java:comp/env/jms/TicketQueue");
QueueConnection connect = factory.createQueueConneciton();
QueueSession session = connect.createQueueSession(true,0);
QueueSender sender = session.createSender(queue);
TextMessage textMsg = session.createTextMessage();
textMsg.setText(ticketDescription);
sender.send(textMsg);
connect.close();
return ticket;
} catch(Exception e) {
throw new EJBException(e);
}
}
The rationale behind the two models lies in the origin of the JMS specification. JMS started out as a way of providing a common API for accessing existing messaging systems. At the time of its conception, some messaging vendors had a p2p model and some had a pub/sub model. Hence JMS needed to provide an API for both models to gain wide industry support. The JMS 1.0.2 specification does not require a JMS provider to support both models. EJB 2.0 vendors, however, are required to support both messaging models.
Almost anything that can be done with the pub/sub model can be done with point-to-point, and vice versa. An analogy can be drawn to developers' programming language preferences. In theory, any application that can be written with Pascal can also be written with C. Anything that can be written in C++ can also be written in Java. In some cases it comes down to a matter of preference, or which model you are already familiar with.
In most cases, the decision about which model to use depends on the distinct merits of each model. With pub/sub, any number of subscribers can be listening on a topic, and they will all receive copies of the same message. The publisher may not care if everybody is listening, or even if nobody is listening. For example, consider a publisher that broadcasts stock quotes. If any particular subscriber is not currently connected and misses out on a great quote, the publisher is not concerned. In contrast, a point-to-point session is likely to be intended for a one-on-one conversation with a specific application at the other end. In this scenario, every message really matters.
The range and variety of the data the messages represent can be
a factor as well. Using pub/sub, messages are dispatched to the consumers
based on filtering that is provided through the use of specific topics. Even
when messaging is being used to establish a one-on-one conversation with
another known application, it can be advantageous to use pub/sub with multiple
topics to segregate different kinds of messages. Each kind of message can be
dealt with separately through its own unique consumer and onMessage() listener.
Point-to-point is more convenient when you want only a particular receiver to process a given message once. This is perhaps the most critical difference between the two models: p2p guarantees that only one consumer processes each message. This is extremely important when messages need to be processed separately but in tandem.
JmsClient_1 was designed to consume
messages produced by the TravelAgent EJB. Can another entity or session bean
receive those messages also? The answer is yes, but it's a really bad
idea.
Entity and session beans respond to Java RMI calls from EJB clients and cannot be programmed to respond to JMS messages as do message-driven beans. That means it's impossible to write a session or entity bean that will be driven by incoming messages. The inability to make EJBs respond to JMS messages was why message-driven beans were introduced in EJB 2.0. Message-driven beans are designed to consume messages from topics and queues. They fill an important niche; we'll learn more about how to program them in the next section.
It is possible to develop an entity or session bean that can consume a JMS message from a business method, but the method must be called by an EJB client first. For example, when the business method on the Hypothetical EJB is called, it sets up a JMS session and then attempts to read a message from a queue:
public class HypotheticalBean implements javax.ejb.SessionBean {
InitialContext jndiContext;
public String businessMethod() {
try{
QueueConnectionFactory factory = (QueueConnectionFactory)
jndiContext.lookup("java:comp/env/jms/QueueFactory");
Queue topic = (Queue)
jndiContext.lookup("java:comp/env/jms/Queue");
QueueConnection connect = factory.createQueueConneciton();
QueueSession session = connect.createQueueSession(true,0);
QueueReceiver receiver = session.createReceiver(queue);
TextMessage textMsg = (TextMessage)receiver.receive();
connect.close();
return textMsg.getText();
} catch(Exception e) {
throws new EJBException(e);
}
}
...
}
The QueueReceiver, which is a message
consumer, is used to proactively fetch a message from the queue. While this
has been programmed correctly, it is a dangerous operation because a call to
the QueueReceiver.receive() method blocks the
thread until a message becomes available. If a message is never delivered to
the receiver's queue, the thread will be blocked indefinitely! In other words,
if no one ever sends a message to the queue, the QueueReceiver will just sit there waiting forever.
To be fair, there are other receive()
methods that are less dangerous. For example, receive(long timeout) allows
you to specify a time after which the QueueReceiver
should stop blocking the thread and give up waiting for a message. There is
also receiveNoWait(), which checks for a message
and returns null if there are none waiting, thus
avoiding a prolonged thread block.
While the alternative receive()
methods are much safer, this is still a dangerous operation to perform. There
is no guarantee that the less risky receive()
methods will perform as expected, and the risk of programmer error (e.g.,
using the wrong receive() method) is too great.
Besides, the message-driven bean provides you with a powerful and simple
enterprise bean that is specially designed to consume JMS messages. This book
recommends that you do not attempt to consume messages from entity or session
beans.
JMS (and enterprise messaging in general) represents a powerful paradigm in distributed computing. In my opinion, the Java Message Service is as important as Enterprise JavaBeans itself and should be well understood before it is used in development.
While this chapter has provided a brief overview of JMS, it has presented you only with enough material to prepare you for the discussion of message-driven beans in the next section. JMS has a cornucopia of features and details that are simply too extensive to cover in this book. To understand JMS and how it is used, you will need to study it independently. (For a detailed treatment of JMS, see Java Message Service by Richard Monson-Haefel and David Chappell (O'Reilly).) Taking the time to learn JMS is well worth the effort.
|
Message-driven beans (MDBs) are stateless, server-side, transaction-aware components for processing asynchronous JMS messages. Newly introduced in EJB 2.0, message-driven beans process messages delivered via the Java Message Service.
Message-driven beans can receive JMS messages and process them. While a message-driven bean is responsible for processing messages, its container takes care of automatically managing the component's entire environment, including transactions, security, resources, concurrency, and message acknowledgment.
One of the most important aspects of message-driven beans is that they can consume and process messages concurrently. This capability provides a significant advantage over traditional JMS clients, which must be custom-built to manage resources, transactions, and security in a multithreaded environment. The message-driven bean containers provided by EJB manage concurrency automatically, so the bean developer can focus on the business logic of processing the messages. The MDB can receive hundreds of JMS messages from various applications and process them all at the same time, because numerous instances of the MDB can execute concurrently in the container.
A message-driven bean is a complete enterprise bean, just like a session or entity bean, but there are some important differences. While a message-driven bean has a bean class and XML deployment descriptor, it does not have component interfaces. The component interfaces are absent because the message-driven bean is not accessible via the Java RMI API; it responds only to asynchronous messages.
The ReservationProcessor EJB is a message-driven bean that receives JMS messages notifying it of new reservations. The ReservationProcessor EJB is an automated version of the TravelAgent EJB that processes reservations sent via JMS by other travel organizations. It requires no human intervention; it is completely automated.
The JMS messages that notify the ReservationProcessor EJB of new reservations might come from another application in the enterprise or an application in some other organization. When the ReservationProcessor EJB receives a message, it creates a new Reservation EJB (adding it to the database), processes the payment using the ProcessPayment EJB, and sends out a ticket. This process is illustrated in Figure 13-3.
|
Here is a partial definition of the ReservationProcessorBean class. Some methods are left
empty; they will be filled in later. Notice that the onMessage() method contains the business logic of the
bean class; it is similar to the business logic developed in the bookPassage() method of the TravelAgent EJB in Chapter
12. Here's the code:
package com.titan.reservationprocessor;
import javax.jms.Message;
import javax.jms.MapMessage;
import com.titan.customer.*;
import com.titan.cruise.*;
import com.titan.cabin.*;
import com.titan.reservation.*;
import com.titan.processpayment.*;
import com.titan.travelagent.*;
import java.util.Date;
public class ReservationProcessorBean implements javax.ejb.MessageDrivenBean,
javax.jms.MessageListener {
MessageDrivenContext ejbContext;
Context jndiContext;
public void setMessageDrivenContext(MessageDrivenContext mdc) {
ejbContext = mdc;
try {
jndiContext = new InitialContext();
} catch(NamingException ne) {
throw new EJBException(ne);
}
}
public void ejbCreate() {}
public void onMessage(Message message) {
try {
MapMessage reservationMsg = (MapMessage)message;
Integer customerPk = (Integer)reservationMsg.getObject("CustomerID");
Integer cruisePk = (Integer)reservationMsg.getObject("CruiseID");
Integer cabinPk = (Integer)reservationMsg.getObject("CabinID");
double price = reservationMsg.getDouble("Price");
// get the credit card
Date expirationDate =
new Date(reservationMsg.getLong("CreditCardExpDate"));
String cardNumber = reservationMsg.getString("CreditCardNum");
String cardType = reservationMsg.getString("CreditCardType");
CreditCardDO card = new CreditCardDO(cardNumber,
expirationDate, cardType);
CustomerRemote customer = getCustomer(customerPk);
CruiseLocal cruise = getCruise(cruisePk);
CabinLocal cabin = getCabin(cabinPk);
ReservationHomeLocal resHome = (ReservationHomeLocal)
jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal");
ReservationLocal reservation =
resHome.create(customer, cruise, cabin, price, new Date());
Object ref = jndiContext.lookup
("java:comp/env/ejb/ProcessPaymentHomeRemote");
ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote)
PortableRemoteObject.narrow(ref, ProcessPaymentHomeRemote.class);
ProcessPaymentRemote process = ppHome.create();
process.byCredit(customer, card, price);
TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
deliverTicket(reservationMsg, ticket);
} catch(Exception e) {
throw new EJBException(e);
}
}
public void deliverTicket(MapMessage reservationMsg, TicketDO ticket) {
// send it to the proper destination
}
public CustomerRemote getCustomer(Integer key)
throws NamingException, RemoteException, FinderException {
// get a remote reference to the Customer EJB
}
public CruiseLocal getCruise(Integer key)
throws NamingException, FinderException {
// get a local reference to the Cruise EJB
}
public CabinLocal getCabin(Integer key)
throws NamingException, FinderException {
// get a local reference to the Cabin EJB
}
public void ejbRemove() {
try {
jndiContext.close();
ejbContext = null;
} catch(NamingException ne) { /* do nothing */ }
}
}
The message-driven bean class is required to implement the javax.ejb.MessageDrivenBean interface, which defines
callback methods similar to those in entity and session beans. Here is the
definition of the MessageDrivenBean interface:
package javax.ejb;
public interface MessageDrivenBean extends javax.ejb.EnterpriseBean {
public void setMessageDrivenContext(MessageDrivenContext context)
throws EJBException;
public void ejbRemove() throws EJBException;
}
The setMessageDrivenContext() method is called at the
beginning of the MDB's life cycle and provides the MDB instance with a
reference to its MessageDrivenContext:
MessageDrivenContext ejbContext;
Context jndiContext;
public void setMessageDrivenContext(MessageDrivenContext mdc) {
ejbContext = mdc;
try {
jndiContext = new InitialContext();
} catch(NamingException ne) {
throw new EJBException(ne);
}
}
The setMessageDrivenContext() method
in the ReservationProcessorBean class sets the
ejbContext instance field to the MessageDrivenContext, which was passed into the method.
It also obtains a reference to the JNDI ENC, which it stores in the jndiContext. MDBs may have instance fields that are
similar to a stateless session bean's instance fields. These instance fields
are carried with the MDB instance for its lifetime and may be reused every
time it processes a new message. Unlike stateful session beans, MDBs do not
have conversational state and are not specific to a single JMS client. MDB
instances are used to processes messages from many different JMS clients and
are tied to a specific topic or queue from which they receive messages, not to
a specific JMS client. They are stateless in the same way that stateless
session beans are stateless.
ejbRemove() provides the MDB instance
with an opportunity to clean up any resources it stores in its instance
fields. In this case, we use it to close the JNDI context and set the ejbContext field to null.
These operations are not absolutely necessary, but they illustrate the kind of
operation that an ejbRemove() method might do. Note
that ejbRemove() is called at the end of the MDB's
life cycle, before it is garbage collected. It may not be called if the EJB
server hosting the MDB fails or if an EJBException
is thrown by the MDB instance in one of its other methods. When an EJBException (or any RuntimeException type) is thrown by any method in the MDB
instance, the instance is immediately removed from memory and the transaction
is rolled back.
The MessageDrivenContext simply
extends the EJBContext; it does not add any new
methods. The EJBContext is defined as:
package javax.ejb;
public interface EJBContext {
// transaction methods
public javax.transaction.UserTransaction getUserTransaction()
throws java.lang.IllegalStateException;
public boolean getRollbackOnly() throws java.lang.IllegalStateException;
public void setRollbackOnly() throws java.lang.IllegalStateException;
// EJB home methods
public EJBHome getEJBHome();
public EJBLocalHome getEJBLocalHome();
// security methods
public java.security.Principal getCallerPrincipal();
public boolean isCallerInRole(java.lang.String roleName);
// deprecated methods
public java.security.Identity getCallerIdentity();
public boolean isCallerInRole(java.security.Identity role);
public java.util.Properties getEnvironment();
}
Only the transactional methods the MessageDrivenContext inherits from EJBContext are available to message-driven beans. The
home methods--getEJBHome() and getEJBLocalHome()--throw a RuntimeException if invoked, because MDBs do not have
home interfaces or EJB home objects. The security methods--getCallerPrincipal() and isCallerInRole()--also throw a RuntimeException if invoked on a MessageDrivenContext. When an MDB services a JMS message
there is no "caller," so there is no security context to be obtained from the
caller. Remember that JMS is asynchronous and doesn't propagate the sender's
security context to the receiver--that wouldn't make sense, since senders and
receivers tend to operate in different environments.
MDBs usually execute in a container-initiated or bean-initiated
transaction, so the transaction methods allow the MDB to manage its context.
The transaction context is not propagated from the JMS sender, but rather is
either initiated by the container or by the bean explicitly using javax.jta.UserTransaction. The transaction methods in the
EJBContext are explained in more detail in Chapter
14.
Message-driven beans also have access to their own JNDI
environment naming contexts (ENCs), which provide the MDB instances access to
environment entries, other enterprise beans, and resources. For example, the
ReservationProcessor EJB takes advantage of the JNDI ENC to obtain references
to the Customer, Cruise, Cabin, Reservation, and ProcessPayment EJBs as well
as a JMS QueueConnectionFactory and Queue for sending out tickets.
In addition to the MessageDrivenBean
interface, MDBs implement the javax.jms.MessageListener interface, which defines the
onMessage() method; bean developers implement this
method to process JMS messages received by a bean. It's in the onMessage() method that the bean processes the JMS
message:
package javax.jms;
public interface MessageListener {
public void onMessage(Message message);
}
It's interesting to consider why the MDB implements the MessageListener interface separately from the MessageDrivenBean interface. Why not just put the onMessage() method, MessageListener's only method, in the MessageDrivenBean interface so that there is only one
interface for the MDB class to implement? This was the solution taken by an
early proposed version of EJB 2.0. However, it was quickly realized that
message-driven beans could, in the future, process messages from other types
of systems, not just JMS. To make the MDB open to other messaging systems, it
was decided that it should implement the javax.jms.MessageListener interface separately, thus
separating the concept of the message-driven bean from the types of messages
it can process. In a future version of the specification, other types of MDB
might be available for technologies such as SMTP (email) or JAXM ( Java API
for XML Messaging) for ebXML. These technologies will use methods other than
onMessage(), which is specific to JMS.
The onMessage() method is where all the business logic goes.
As messages arrive, they are passed to the MDB by the container via the onMessage() method. When the method returns, the MDB is
ready to process a new message.
In the ReservationProcessor EJB, the onMessage() method extracts information about a
reservation from a MapMessage and uses that
information to create a reservation in the system:
public void onMessage(Message message) {
try {
MapMessage reservationMsg = (MapMessage)message;
Integer customerPk = (Integer)reservationMsg.getObject("CustomerID");
Integer cruisePk = (Integer)reservationMsg.getObject("CruiseID");
Integer cabinPk = (Integer)reservationMsg.getObject("CabinID");
double price = reservationMsg.getDouble("Price");
// get the credit card
Date expirationDate =
new Date(reservationMsg.getLong("CreditCardExpDate"));
String cardNumber = reservationMsg.getString("CreditCardNum");
String cardType = reservationMsg.setString("CreditCardType");
CreditCardDO card = new CreditCardDO(cardNumber,
expirationDate, cardType);
JMS is frequently used as an integration point for business-to-business applications, so it's easy to imagine the reservation message coming from one of Titan's business partners (perhaps a third-party processor or branch travel agency).
The ReservationProcessor EJB needs to access the Customer,
Cruise, and Cabin EJBs in order to process the reservation. The MapMessage contains the primary keys for these entities;
the ReservationProcessor EJB uses helper methods (getCustomer(), getCruise(),
and getCabin()) to look up the entity beans and
obtain EJB object references to them:
public void onMessage(Message message) {
...
CustomerRemote customer = getCustomer(customerPk);
CruiseLocal cruise = getCruise(cruisePk);
CabinLocal cabin = getCabin(cabinPk);
...
}
public CustomerRemote getCustomer(Integer key)
throws NamingException, RemoteException, FinderException {
Object ref = jndiContext.lookup("java:comp/env/ejb/CustomerHomeRemote");
CustomerHomeRemote home = (CustomerHomeRemote)
PortableRemoteObject.narrow(ref, CustomerHomeRemote.class);
CustomerRemote customer = home.findByPrimaryKey(key);
return customer;
}
public CruiseLocal getCruise(Integer key)
throws NamingException, FinderException {
CruiseHomeLocal home = (CruiseHomeLocal)
jndiContext.lookup("java:comp/env/ejb/CruiseHomeLocal");
CruiseLocal cruise = home.findByPrimaryKey(key);
return cruise;
}
public CabinLocal getCabin(Integer key)
throws NamingException, FinderException{
CabinHomeLocal home = (CabinHomeLocal)
jndiContext.lookup("java:comp/env/ejb/CabinHomeLocal");
CabinLocal cabin = home.findByPrimaryKey(key);
return cabin;
}
Once the information is extracted from the MapMessage, it is used to create a reservation and process the payment. This is basically the same workflow that was used by the TravelAgent EJB in Chapter 12. A Reservation EJB is created that represents the reservation itself, and a ProcessPayment EJB is created to process the credit card payment:
ReservationHomeLocal resHome = (ReservationHomeLocal)
jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal");
ReservationLocal reservation =
resHome.create(customer, cruise, cabin, price, new Date());
Object ref = jndiContext.lookup("java:comp/env/ejb/ProcessPaymentHomeRemote");
ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote)
PortableRemoteObject.narrow (ref, ProcessPaymentHomeRemote.class);
ProcessPaymentRemote process = ppHome.create();
process.byCredit(customer, card, price);
TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
deliverTicket(reservationMsg, ticket);
This illustrates that, like a session bean, the MDB can access any other entity or session bean and use that bean to complete a task. In this way, the MDB fulfills its role as an integration point in B2B application scenarios. MDB can manage a process and interact with other beans as well as resources. For example, it is commonplace for an MDB to use JDBC to access a database based on the contents of the message it is processing.
|
MDB can also send messages using JMS. The deliverTicket() method sends the ticket information to a
destination defined by the sending JMS client:
public void deliverTicket(MapMessage reservationMsg, TicketDO ticket)
throws NamingException, JMSException{
Queue queue = (Queue)reservationMsg.getJMSReplyTo();
QueueConnectionFactory factory = (QueueConnectionFactory)
jndiContext.lookup("java:comp/env/jms/QueueFactory");
QueueConnection connect = factory.createQueueConneciton();
QueueSession session = connect.createQueueSession(true,0);
QueueSender sender = session.createSender(queue);
ObjectMessage message = session.createObjectMessage();
message.setObject(ticket);
sender.send(message);
connect.close();
}
Obviously, if the destination
identified by the JMSReplyTo attribute is of type
Queue, the point-to-point (queue-based) messaging
model must be used. If the destination type identified by the JMSReplyTo attribute is Topic,
the publish-and-subscribe (topic-based) messaging model must be used. |
As stated earlier, every message type has two parts: a message
header and a message body (a.k.a. payload). The message header contains
routing information and may also have properties for message filtering and
other attributes, including a JMSReplyTo attribute.
When a JMS client sends a message, it may set the JMSReplyTo attribute to be any destination accessible to
its JMS provider.
In the case of the reservation message, the sender set the JMSReplyTo attribute to the queue to which the resulting
ticket should be sent. Another application can access this queue to read
tickets and distribute them to customers or store the information in the
sender's database.
You can also use the JMSReplyTo
address to report business errors that occur while processing the message. For
example, if the Cabin is already reserved, the ReservationProcessor EJB might
send an error message to the JMSReplyTo queue
explaining that the reservation could not be processed. Including this type of
error handling is left as an exercise for the reader.
MDBs have XML deployment descriptors, just like entity and session beans. They can be deployed alone or, more often than not, together with other enterprise beans. For example, the ReservationProcessor EJB would have to be deployed in the same JAR using the same XML deployment descriptor as the Customer, Cruise, and Cabin beans if it's going to use their local interfaces.
Here's the XML deployment descriptor that defines the ReservationProcessor EJB. This deployment descriptor also defines the Customer, Cruise, Cabin, and other beans, but these are left out here for brevity:
<enterprise-beans>
...
<message-driven>
<ejb-name>ReservationProcessorEJB</ejb-name>
<ejb-class>
com.titan.reservationprocessor.ReservationProcessorBean
</ejb-class>
<transaction-type>Container</transaction-type>
<message-selector>MessageFormat = 'Version 3.4'</message-selector>
<acknowledge-mode>Auto-acknowledge</acknowledge-mode>
<message-driven-destination>
<destination-type>javax.jms.Queue</destination-type>
</message-driven-destination>
<ejb-ref>
<ejb-ref-name>ejb/ProcessPaymentHomeRemote</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<home>com.titan.processpayment.ProcessPaymentHomeRemote</home>
<remote>com.titan.processpayment.ProcessPaymentRemote</remote>
</ejb-ref>
<ejb-ref>
<ejb-ref-name>ejb/CustomerHomeRemote</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.titan.customer.CustomerHomeRemote</home>
<remote>com.titan.customer.CustomerRemote</remote>
</ejb-ref>
<ejb-local-ref>
<ejb-ref-name>ejb/CruiseHomeLocal</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>com.titan.cruise.CruiseHomeLocal</local-home>
<local>com.titan.cruise.CruiseLocal</local>
</ejb-local-ref>
<ejb-local-ref>
<ejb-ref-name>ejb/CabinHomeLocal</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>com.titan.cabin.CabinHomeLocal</local-home>
<local>com.titan.cabin.CabinLocal</local>
</ejb-local-ref>
<ejb-local-ref>
<ejb-ref-name>ejb/ReservationHomeLocal</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<local-home>com.titan.reservation.ReservationHomeLocal</local-home>
<local>com.titan.reservation.ReservationLocal</local>
</ejb-local-ref>
<security-identity>
<run-as>
<role-name>everyone</role-name>
</run-as>
</security-identity>
<resource-ref>
<res-ref-name>jms/QueueFactory</res-ref-name>
<res-type>javax.jms.QueueConnectionFactory</res-type>
<res-auth>Container</res-auth>
</resource-ref>
</message-driven>
...
</enterprise-beans>
An MDB is declared in a <message-driven> element within the <enterprise-beans> element, alongside <session> and <entity> beans. Similar to <session> bean types, it defines an <ejb-name>, <ejb-class>, and <transaction-type>, but it does not define
component interfaces (local or remote). MDBs do not have component interfaces,
so these definitions aren't needed.
An MDB can also declare a <message-selector> element, which is unique to
message-driven beans:
<message-selector>MessageFormat = 'Version 3.4'</message-selector>
Message selectors allow an MDB to be more selective about the
messages it receives from a particular topic or queue. Message selectors use
Message properties as criteria in conditional
expressions. (Message selectors are also based
on message headers, which are outside the scope of this chapter.)
These conditional expressions use Boolean logic to declare which messages
should be delivered to a client.
Message properties, upon which message selectors are based, are
additional headers that can be assigned to a message. They give the
application developer or JMS vendor the ability to attach more information to
a message. The Message interface provides several
accessor and mutator methods for reading and writing properties. Properties
can have a String value or one of several primitive
values (boolean, byte,
short, int, long, float, double). The naming of properties, together with their
values and conversion rules, is strictly defined by JMS.
The ReservationProcessor EJB uses a message selector filter to
select messages of a specific format. In this case the format is "Version
3.4"; this is a string Titan uses to identify messages of type MapMessage that contain the name values CustomerID, CruiseID, CabinID, CreditCard, and Price. In other words, by specifying a MessageFormat on every reservation message, we can write
MDBs that are designed to process different kinds of reservation messages. If
a new business partner needs to use a different type of Message object, Titan would use a new message version and
an MDB to process it.
This is how a JMS producer would go about setting a MessageFormat property on a Message:
Message message = session.createMapMessage();
message.setStringPropery("MessageFormat","Version 3.4");
// set the reservation named values
sender.send(message);
The message selectors are based on a subset of the SQL-92 conditional expression syntax that is used in the WHERE clauses of SQL statements. They can become fairly complex, including the use of literal values, Boolean expressions, unary operators, and so on.
Here are three complex selectors used in hypothetical
environments. Although you will have to use your imagination a little, the
purpose of these examples is to convey the power of the message selectors.
When a selector is declared, the identifier always refers to a property name
or JMS header name. For example, the selector UserName !='William' assumes
that there is a property in the message named UserName, which can be compared to the value 'William'.
Managing claims in an HMO
Due to some fraudulent claims, an automatic process is implemented using MDBs that will audit all claims submitted by patients who are employees of the ACME manufacturing company for visits to chiropractors, psychologists, and dermatologists:
<message-selector>
<![CDATA[
PhysicianType IN ('Chiropractic','Psychologists','Dermatologist')
AND PatientGroupID LIKE 'ACME%'
]]>
</message-selector>
TIP: MDB
<message-selector>statements are declared in XML deployment descriptors. XML assigns special meaning to a variety of characters, such as the greater than (>) and less than (<) symbols, so using these symbols in the<message-selector>statements will cause parsing errors unless CDATA sections are used. This is the same reason CDATA sections were needed in EJB QL<ejb-ql>statements, as explained in Chapter 8.
Notification of certain bids on inventory
A supplier wants notification of requests for bids on specific inventory items at specific quantities:
<message-selector>
<![CDATA[
InventoryID ='S93740283-02' AND Quantity BETWEEN 1000 AND 13000
]]>
</message-selector>
Selecting recipients for a catalog mailing
An online retailer wants to deliver a special catalog to any customer that orders more than $500.00 worth of merchandise where the average price per item ordered is greater than $75.00 and the customer resides in one several states. The retailer creates an MBD that subscribes to the order-processing topic and processes catalog deliveries for only those customers that meet the defined criteria:
<message-selector>
<![CDATA[
TotalCharge >500.00 AND ((TotalCharge /ItemCount)>=75.00)
AND State IN ('MN','WI','MI','OH')
]]>
</message-selector>
JMS has the concept of acknowledgment, which means that the JMS client notifies the JMS provider (message router) when a message is received. In EJB, it's the MDB container's responsibility to send an acknowledgment to the JMS provider when it receives a message. Acknowledging a message tells the JMS provider that MDB container has received the message and processed it using an MDB instance. Without an acknowledgment, the JMS provider will not know whether the MDB container has received the message, so it will try to redeliver it. This can cause problems. For example, once we have processed a reservation message using the ReservationProcessor EJB, we don't want to receive the same message again.
When transactions are involved, the acknowledgment mode set by
the bean provider is ignored. In this case, the acknowledgment is performed
within the context of the transaction. If the transaction succeeds, the
message is acknowledged. If the transaction fails, the message is not
acknowledged. If the MDB is using container-managed transactions, as it will
in most cases, the acknowledgment mode is ignored by the MDB container. When
using container-managed transactions with a Required transaction attribute, the <acknowledge-mode> is usually not specified;
however, we included it in the deployment descriptor for the sake of
discussion:
<acknowledge-mode>Auto-acknowledge</acknowledge-mode>
When the MDB executes with bean-managed transactions, or with
the container-managed transaction attribute NotSupported (see Chapter 14), the value of <acknowledge-mode> becomes important.
Two values can be specified for <acknowledge-mode>: Auto-acknowledge and Dups-ok-acknowledge. Auto-acknowledge tells the container that it should send
an acknowledgment to the JMS provider soon after the message is given to an
MDB instance to process. Dups-ok-acknowledge tells
the container that it doesn't have to send the acknowledgment immediately; any
time after the message is given to the MDB instance will be fine. With Dups-ok-acknowledge, it's possible for the MDB container
to delay acknowledgment so long that the JMS provider assumes that the message
was not received and sends a "duplicate" message. Obviously, with Dups-ok-acknowledge, your MDBs must be able to handle
duplicate messages correctly.
Auto-acknowledge avoids duplicate
messages because the acknowledgment is sent immediately. Therefore, the JMS
provider won't send a duplicate. Most MDBs use Auto-acknowledge, to avoid processing the same message
twice. Dups-ok-acknowledge exists because it may
allow a JMS provider to optimize its use of the network. In practice, though,
the overhead of an acknowledgment is so small, and the frequency of
communication between the MDB container and JMS provider is so high, that Dups-ok-acknowledge doesn't have a big impact on
performance.
The <message-driven-destination> element designates the
type of destination from which the MDB receives messages. The allowed values
for this element are javax.jms.Queue and javax.jms.Topic. In the ReservationProcessor EJB this
value is set to javax.jms.Queue, indicating that
the MDB is getting its messages via the p2p messaging model from a queue:
<message-driven-destination>
<destination-type>javax.jms.Queue</destination-type>
</message-driven-destination>
When the MDB is deployed, the deployer will map the MDB so that it listens to a real queue on the network.
When the <destination-type> is
a javax.jms.Topic, the <subscription-durability> element must be declared
with either Durable or NonDurable as its value:
<message-driven-destination>
<destination-type>javax.jms.Topic</destination-type>
<subscription-durability>Durable</subscription-durability>
</message-driven-destination>
The <subscription-durability>
element determines whether or not the MDB's subscription to the topic is Durable. A Durable
subscription outlasts an MDB container's connection to the JMS provider, so if
the EJB server suffers a partial failure, is shut down, or is otherwise
disconnected from the JMS provider, the messages that it would have received
will not be lost. While a Durable MDB container is
disconnected from the JMS provider, it is the responsibility of the JMS
provider to store any messages the subscriber misses. When the Durable MDB container reconnects to the JMS provider, the
JMS provider sends it all the unexpired messages that accumulated while it was
down. This behavior is commonly referred to as store-and-forward messaging. Durable MDBs are tolerant of disconnections, whether they
are intentional or the result of a partial failure.
If <subscription-durability> is
NonDurable, any messages the bean would have
received while it was disconnected will be lost. Developers use NonDurable subscriptions when it is not critical that all
messages be processed. Using a NonDurable
subscription improves the performance of the JMS provider but significantly
reduces the reliability of the MDBs.
When <destination-type> is
javax.jms.Queue, as is the case in the
ReservationProcessor EJB, durability is not a factor because of the nature of
p2p or queue-based messaging systems. With a queue, messages may be consumed
only once and remain in the queue until they are distributed to one of the
queue's listeners.
The rest of the elements in the deployment descriptor should
already be familiar. The <ejb-ref> element
provides JNDI ENC bindings for a remote EJB home object while the <ejb-local-ref> elements provide JNDI ENC bindings
for local EJB home objects. Note that the <resource-ref> element that defined the JMS QueueConnectionFactory used by the ReservationProcessor
EJB to send ticket messages is not accompanied by a <resource-env-ref> element. The queue to which the
tickets are sent is obtained from the JMSReplyTo
header of the MapMessage itself, and not from the
JNDI ENC.
|
In order to test the ReservationProcessor EJB, we need to develop two new client applications: one to send reservation messages and the other to consume ticket messages produced by the ReservationProcessor EJB.
The reservation message producer
The JmsClient_ReservationProducer is
designed to send 100 reservation requests very quickly. The speed with which
it sends these messages will force many MDB containers to use multiple
instances to process the reservation messages. The code for JmsClient_ReservationProducer looks like this:
import javax.jms.Message;
import javax.jms.MapMessage;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueConnection;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.Queue;
import javax.jms.QueueSender;
import javax.jms.JMSException;
import javax.naming.InitalContext;
import java.util.Date;
import com.titan.processpayment.CreditCardDO;
public class JmsClient_ReservationProducer {
public static void main(String [] args) throws Exception {
InitialContext jndiContext = getInitialContext();
QueueConnectionFactory factory = (QueueConnectionFactory)
jndiContext.lookup("QueueFactoryNameGoesHere");
Queue reservationQueue = (Queue)
jndiContext.lookup("QueueNameGoesHere");
QueueConnection connect = factory.createQueueConneciton();
QueueSession session =
connect.createQueueSession(false,Session.AUTO_ACKNOWLEDGE);
QueueSender sender = session.createSender(reservationQueue);
Integer cruiseID = new Integer(1);
for(int i = 0; i < 100; i++){
MapMessage message = session.createMapMessage();
message.setStringProperty("MessageFormat","Version 3.4");
message.setInt("CruiseID",1);
message.setInt("CustomerID",i%10);
message.setInt("CabinID",i);
message.setDouble("Price", (double)1000+i);
// the card expires in about 30 days
Date expirationDate = new Date(System.currentTimeMillis()+43200000);
message.setString("CreditCardNum", "923830283029");
message.setLong("CreditCardExpDate", expirationDate.getTime());
message.setString("CreditCardType", CreditCardDO.MASTER_CARD);
sender.send(message);
}
connect.close();
}
public static InitialContext getInitialContext()
throws JMSException {
// create vendor-specific JNDI context here
}
}
You may have noticed that the JmsClient_ReservationProducer sets the CustomerID, CruiseID, and
CabinID as primitive int
values, but the ReservationProcessorBean reads
these values as java.lang.Integer types. This is
not a mistake. The MapMessage automatically
converts any primitive to its proper wrapper if that primitive is read using
MapMessage.getObject(). So, for example, a named
value that is loaded into a MapMessage using setInt() can be read as an Integer using getObject(). For
example, the following code sets a value as a primitive int and then accesses it as a java.long.Integer object:
MapMessage mapMsg = session.createMapMessage();
mapMsg.setInt("TheValue",3);
Integer myInteger = (Integer)mapMsg.getObject("TheValue");
if(myInteger.intValue() == 3 )
// this will always be true
The ticket message consumer
The JmsClient_TicketConsumer is
designed to consume all the ticket messages delivered by ReservationProcessor
EJB instances to the queue. It consumes the messages and prints out the
descriptions:
import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueConnection;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.Queue;
import javax.jms.QueueReceiver;
import javax.jms.JMSException;
import javax.naming.InitalContext;
import com.titan.travelagent.TicketDO;
public class JmsClient_TicketConsumer
implements javax.jms.MessageListener {
public static void main(String [] args) throws Exception {
new JmsClient_TicketConsumer();
while(true){Thread.sleep(10000);}
}
public JmsClient_TicketConsumer() throws Exception {
InitialContext jndiContext = getInitialContext();
QueueConnectionFactory factory = (QueueConnectionFactory)
jndiContext.lookup("QueueFactoryNameGoesHere");
Queue ticketQueue = (Queue)jndiContext.lookup("QueueNameGoesHere");
QueueConnection connect = factory.createQueueConneciton();
QueueSession session =
connect.createQueueSession(false,Session.AUTO_ACKNOWLEDGE);
QueueReceiver receiver = session.createReceiver(ticketQueue);
receiver.setMessageListener(this);
connect.start();
}
public void onMessage(Message message) {
try {
ObjectMessage objMsg = (ObjectMessage)message;
TicketDO ticket = (TicketDO)objMsg.getObject();
System.out.println("********************************");
System.out.println(ticket);
System.out.println("********************************");
} catch(JMSException jmsE) {
jmsE.printStackTrace();
}
}
public static InitialContext getInitialContext() throws JMSException {
// create vendor-specific JNDI context here
}
}
To make the ReservationProcessor EJB work with the two client
applications, JmsClient_ReservationProducer and
JmsClient_TicketConsumer, you must configure your
EJB container's JMS provider so that it has two queues: one for reservation
messages and another for ticket messages.
|
Some vendors may not pool MDB instances, but may instead create and destroy instances with each new message. This is an implementation-specific decision that should not impact the specified life cycle of the stateless bean instance. |
Just as the entity and session beans have well-defined life cycles, so does the MDB bean. The MDB instance's life cycle has two states: Does Not Exist and Method-Ready Pool. The Method-Ready Pool is similar to the instance pool used for stateless session beans. Like stateless beans, MDBs define instance pooling in their life cycles.
Figure 13-4 illustrates the states and transitions that an MDB instance goes through in its lifetime.
|
When an MDB instance is in the Does Not Exist state, it is not an instance in the memory of the system. In other words, it has not been instantiated yet.
MDB instances enter the Method-Ready Pool as the container needs them. When the EJB server is first started, it may create a number of MDB instances and enter them into the Method-Ready Pool. (The actual behavior of the server depends on the implementation.) When the number of MDB instances handling incoming messages is insufficient, more can be created and added to the pool.
When an instance transitions from the Does Not Exist state to
the Method-Ready Pool, three operations are performed on it. First, the bean
instance is instantiated when the container invokes the Class.newInstance() method on the MDB class. Second, the
setMessageDrivenContext() method is invoked by the
container providing the MDB instance with a reference to its EJBContext. The MessageDrivenContext reference may be stored in an
instance field of the MDB.
Finally, the no-argument ejbCreate()
method is invoked by the container on the bean instance. The MDB has only one
ejbCreate() method, which takes no arguments. The
ejbCreate() method is invoked only once in the life
cycle of the MDB.
|
The duration of an MDB instance's life is assumed to be very long. However, some EJB servers may actually destroy and create instances with every new message, making this strategy less attractive. Consult your vendor's documentation for details on how your EJB server handles stateless instances. |
MDBs are not subject to activation, so they can maintain open
connections to resources for their entire life cycles.
The ejbRemove() method should close any open
resources before the MDB is evicted from memory at the end of its life cycle.
|
Related Reading
|
Once an instance is in the Method-Ready Pool, it is ready to
handle incoming messages. When a message is delivered to an MDB, it is
delegated to any available instance in the Method-Ready Pool. While the
instance is executing the request, it is unavailable to process other
messages. The MDB can handle many messages simultaneously, delegating the
responsibility of handling each message to a different MDB instance. When a
message is delegated to an instance by the container, the MDB instance's MessageDrivenContext changes to reflect the new
transaction context. Once the instance has finished, it is immediately
available to handle a new message.
Bean instances leave the Method-Ready Pool for the Does Not
Exist state when the server no longer needs them. This occurs when the server
decides to reduce the total size of the Method-Ready Pool by evicting one or
more instances from memory. The process begins by invoking the ejbRemove() method on the instance. At this time, the
bean instance should perform any cleanup operations, such as closing open
resources. The ejbRemove() method is invoked only
once in the life cycle of an MDB instance--when it is about to transition to
the Does Not Exist state. During the ejbRemove()
method, the MessageDrivenContext and access to the
JNDI ENC are still available to the bean instance. Following the execution of
the ejbRemove() method, the bean is dereferenced
and eventually garbage collected.
View catalog information for Enterprise JavaBeans, 3rd Edition
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.