Message-driven beans (MDBs) are stateless, server-side, transaction-aware components for processing asynchronous messages delivered via the Java Message Service. While a message-driven bean is responsible for processing messages, its container manages the component’s environment, including transactions, security, resources, concurrency, and message acknowledgment. It’s particularly important to note that the container manages concurrency. The thread-safety provided by the container gives MDBs a significant advantage over traditional JMS clients, which must be custom-built to manage resources, transactions, and security in a multithreaded environment. An MDB can process hundreds of JMS messages concurrently 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 EJB deployment descriptor, it does not have EJB object or home interfaces. These 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. These messages might come from another application in the enterprise or from an application in some other organization—perhaps another travel agent. 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 12-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; it
is similar to the business logic developed in the
bookPassage( )
method of the TravelAgent EJB in
Chapter 11. 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. Like stateless session beans, MDBs do not have
conversational state and are not specific to a single JMS client; MDB
instances process messages from many different clients. Instead, they
are tied to the specific topic or queue from which they receive
messages.
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 work 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 that
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; it 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 15.
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. This method processes the JMS
messages received by a bean.
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, the
developers 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
the MDB 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. It turns out that this was a good plan.
As we’ll see later in this chapter, EJB 2.1 lets you
use MDBs with non-JMS messaging systems that use a different
messaging interface.
The onMessage( )
method is where all the business logic goes. As messages arrive, the
container passes them to the MDB 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 taskflow that was
used by the TravelAgent EJB in Chapter 11. 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);
Like a session bean, the MDB can access any other entity or session bean and use that bean to complete a task. An 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.
An
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.createQueueConnection( ); QueueSession session = connect.createQueueSession(true,0); QueueSender sender = session.createSender(queue); ObjectMessage message = session.createObjectMessage( ); message.setObject(ticket); sender.send(message); connect.close( ); }
Every message type has two parts: a message header and a message body
(a.k.a. the payload). The message header
contains routing information and may also have properties for message
filtering and other attributes. One of these attributes may be
JMSReplyTo
. The message’s sender
may set the
JMSReplyTo
attribute to any destination
accessible to its JMS provider.[39] 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. 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 are described in EJB deployment descriptors the same as entity and session beans. They can be deployed alone, but it’s more often deployed with the other enterprise beans that it references. For example, the ReservationProcessor EJB uses the local interfaces of the Customer, Cruise, and Cabin beans, so all four beans would have to be deployed in the same JAR.
The way EJB 2.1 defines the properties of message processing for
MDB is
significantly different than in EJB 2.0. EJB 2.0 defined a few
JMS-specific elements, which have been abandoned in EJB 2.1 so that
the MDB deployment descriptor can represent Connector-based MDBs as
well as JMS-based MDBs. Since Connector-based MDBs
don’t necessarily use JMS as the message service,
the
<activation-config>
element was introduced to describe the
bean’s messaging properties. The
<activation-config>
elements are shown in
bold in the following listing.
<enterprise-beans> ... <message-driven> <ejb-name>ReservationProcessorEJB</ejb-name> <ejb-class> com.titan.reservationprocessor.ReservationProcessorBean </ejb-class> <messaging-type>javax.jms.MessageListener</messaging-type> <transaction-type>Container</transaction-type> <message-destination-type> javax.jms.Queue </message-destination-type> <activation-config> <activation-property> <activation-config-property-name>destinationType </activation-config-property-name> <activation-config-property-value>javax.jms.Queue </activation-config-property-value> <activation-property> <activation-property> <activation-config-property-name>messageSelector </activation-config-property-name> <activation-config-property-value>MessageFormat = 'Version 3.4' </activation-config-property-value> <activation-property> <activation-property> <activation-config-property-name>acknowledgeMode </activation-config-property-name> <activation-config-property-value>Auto-acknowledge </activation-config-property-value> <activation-property> </activation-config> <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>
The property names and values used in the
<activation-config>
to describe the
messaging service vary depending on the type of message service used,
but EJB 2.1 defines a set of fixed properties for JMS-based
message-driven beans. These properties are
acknowledgeMode
,
messageSelector
,
destinationType
, and
subscriptionDurablity
. These properties are also
used by EJB 2.0 deployment descriptors, so we’ll
discuss them in the next section.
In addition to the <activation-config>
element, EJB 2.1 introduces the
<messaging-type>
and
<message-destination-type>
elements. 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 does not define
component interfaces (local or remote). MDBs do not have remote or
local interfaces, so these definitions aren’t
needed.
Here is the deployment descriptor for MDBs in EJB 2.0:
<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. Like session beans, an MDB
defines an <ejb-name>
,
<ejb-class>
, and
<transaction-type>
; unlike other kinds of
beans, an MDB never defines local or remote component interfaces.
MDBs do not have remote or local interfaces, so these definitions
aren’t needed.
An MDB can declare a 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.[40] These conditional
expressions use Boolean logic to declare which messages should be
delivered. In EJB 2.1, a message selector is declared using standard
property name, messageSelector
, in an activation
configuration element:
<activation-property> <activation-config-property-name>messageSelector
</activation-config-property-name> <activation-config-property-value>MessageFormat = 'Version 3.4'
</activation-config-property-value> <activation-property>
In EJB 2.0, a message selector is declared using the
<message-selector>
element:
<message-selector>MessageFormat = 'Version 3.4'</message-selector>
Message selectors are based on message properties. Message properties
are additional headers that can be assigned to a message; they allow
vendors and developers to attach information to a message that
isn’t part of the message’s body.
The Message
interface provides several 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, adding a MessageFormat
to each reservation
message allows us to 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.
Here’s 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.
A JMS acknowledgment 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 when it receives a message. Acknowledging a message tells the JMS provider that an MDB container has received and processed the message. Without an acknowledgment, the JMS provider does not know whether the MDB container has received the message, and unwanted redeliveries 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.
In EJB 2.1, the acknowledgment mode is set using the standard
acknowledgeMode
activation configuration property,
as shown in the following XML snippet:
<activation-property> <activation-config-property-name>acknowledgeMode
</activation-config-property-name> <activation-config-property-value>Auto-acknowledge
</activation-config-property-value> <activation-property>
In EJB 2.0, the acknowledgment mode is set using a special
<acknowledge-mode>
element, as shown in the
following XML snippet:
<acknowledge-mode>Auto-acknowledge</acknowledge-mode>
Two values can be specified for acknowledgment 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 can 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.
Having said all of this, the acknowledgement mode is ignored most of
the time—in fact, it is ignored unless the MDB executes with
bean-managed transactions, or with the container-managed transaction
attribute NotSupported
(see Chapter 15). In all other cases, transactions are
managed by the container, and acknowledgment takes place within the
context of the transaction. If the transaction succeeds, the message
is acknowledged. If the transaction fails, the message is not
acknowledged. When using container-managed transactions with a
Required
transaction attribute, the acknowledgment
mode is usually not specified; however, it is included in the
deployment descriptor for the sake of discussion.
The
<messaging-type>
element declares the messaging
interfaces used by the MDB:
<messaging-type>javax.jms.MessageListener</messaging-type>
For JMS-based MDBs, the messaging interface is always going to be
javax.jms.MessageListener
, but for other
Connector-based MDBs it might be something completely different. If
the <messaging-type>
element is omitted, the
type is assumed to be javax.jms.MessageListener
.
The
<message-destination-type>
element indicates the type of
destination from which the MDB receives messages. The allowed values
for JMS-based MDBs are javax.jms.Queue
and
javax.jms.Topic
. A Connector-based MDB might use
some other type. The value must always be a fully qualified class
name.
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-destination-type> javax.jms.Queue </message-destination-type>
When the MDB is deployed, the deployer maps the MDB so that it listens to a real queue on the network.
You may have noticed that the
<message-destination-type>
and the
destinationType
configuration property specify the
same thing. This seems redundant, and it is for JMS-based
MDBs—but for Connector-based MDBs, it is not.
That’s because Connector-based MDBs have completely
different activation configuration properties than a JMS-based MDB.
It’s important that the
<message-destination-type>
be specified for
both JMS-based and Connector-based MDBs.
The <message-driven-destination>
element
indicates 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 point-to-point 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 maps the MDB so that it listens to a real queue on the network.
In EJB 2.1 and EJB 2.0, when a JMS-based MDB uses a
javax.jms.Topic
, the deployment descriptor must
declare whether the subscription is
Durable
or
NonDurable
. A Durable
subscription outlasts an MDB container’s connection
to the JMS provider, so if the EJB server suffers a partial failure,
shuts down, or otherwise disconnects from the JMS provider, the
messages that it would have received are not lost. The provider
stores any messages that are delivered while the container is
disconnected; the messages are delivered to the container (and from
there, to the MDB) when the container reconnects. This behavior is
commonly referred to as store-and-forward
messaging
.
Durable
MDBs are tolerant of disconnections,
whether intentional or the result of a partial failure.
If the subscription is NonDurable
, any messages
the bean would have received while it was disconnected are lost.
Developers use NonDurable
subscriptions when it is
not critical for all messages to be processed. Using a
NonDurable
subscription improves the performance
of the JMS provider but significantly reduces the reliability of the
MDBs.
In EJB 2.1, durability is declared using the standard
subscriptionDurability
activation configuration
property:
<activation-property> <activation-config-property-name>subscriptionDurability
</activation-config-property-name> <activation-config-property-value>Durable
</activation-config-property-value> <activation-property>
In EJB 2.0, durability is declared by the
<subscription-durability>
element within the
<message-driven-destination>
element:
<message-driven-destination> <destination-type>javax.jms.Topic
</destination-type> <subscription-durability>Durable
</subscription-durability> </message-driven-destination>
When the destination type is javax.jms.Queue
, as
is the case in the ReservationProcessor EJB, durability is not a
factor because of the nature of 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 both the EJB 2.1 and EJB 2.0 deployment
descriptors 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
JmsClient_ReservationProducer
sends 100 reservation requests very quickly. The speed with which it
sends these messages forces many containers to use multiple MDB
instances to process them. 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.createQueueConnection( ); 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 } }
Note 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
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.createQueueConnection( ); 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.
Exercise 12.2 in the Workbook shows how to deploy these examples in the JBoss EJB container.
[39] In EJB 2.0, 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. In EJB 2.1, you can use the Unified API
for both publish-and-subscribe and point-to-point messaging.
[40] Message selectors are also based on message headers, which are outside the scope of this chapter.