ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

EJB Message-Driven Beans
Pages: 1, 2, 3, 4, 5, 6, 7

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.

Diagram
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.

Pages: 1, 2, 3, 4, 5, 6, 7

Next Pagearrow