Message-Driven Beans

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

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.

The ReservationProcessor EJB processing reservations

Figure 13-3. The ReservationProcessor EJB processing reservations

The ReservationProcessorBean Class

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 */ }
    }
}

MessageDrivenBean interface

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.

MessageDrivenContext

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.

MessageListener interface

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: Workflow and integration for B2B

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.

Sending messages from a message-driven bean

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

}

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.[51] 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.

XML deployment descriptor

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.

<message-selector>

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.[52] 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.

Message selector examples

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>

<acknowledge-mode>

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.

<message-driven-destination>

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.

The ReservationProcessor clients

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.

The ticket message consumerPlease refer to Workbook Exercise 13.2, The Message-Driven Bean. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks.

The Life Cycle of a Message-Driven Bean

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.[53]

Figure 13-4 illustrates the states and transitions that an MDB instance goes through in its lifetime.

MDB life cycle

Figure 13-4. MDB life cycle

Does Not Exist

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.

The Method-Ready Pool

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.

Transitioning to the Method-Ready 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.

MDBs are not subject to activation, so they can maintain open connections to resources for their entire life cycles.[54] The ejbRemove() method should close any open resources before the MDB is evicted from memory at the end of its life cycle.

Life in the Method-Ready Pool

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.

Transitioning out of the Method-Ready Pool: The death of an MDB instance

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.



[51] 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.

[52] Message selectors are also based on message headers, which are outside the scope of this chapter.

[53] 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.

[54] 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.

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

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