6.8. Message-Driven BeansMessage-driven EJBs are components that are managed by an EJB container like other EJBs, but they are invoked asynchronously by clients using JMS messages rather than method calls. In terms of their runtime lifecycle and container management, message-driven beans are closest in nature to stateless session beans. They are invoked indirectly and asynchronously, so they don't maintain conversational state on behalf of clients, and they can be pooled by the container to handle incoming messages. From the client's perspective, a message-driven bean is seen as simply another JMS destination with which to exchange messages. Clients do not even know that an EJB container is involvedthey simply perform the usual steps to establish a session with the JMS queue or topic and exchange messages with it. We only touch on these JMS-specific details herefor more details, see Chapter 11. Message-driven EJBs are the simplest to implement since they require only a bean implementation class. The only client interaction with them is through JMS messages, so there's no need for client interfaces or home interfaces. A message-driven bean must implement both the MessageDrivenBean interface from the EJB API as well as the MessageListener interface from JMS. The bean's lifecycle is managed by the EJB container, including its association with a JMS destination. While a simple, standalone JMS MessageListener needs to be programmatically associated with its message destination, the EJB container associates message-driven beans with their destinations automatically, using the information provided in the bean's deployment descriptor. Once a message-driven bean is activated, it receives messages from its designated JMS destination and responds to them through its onMessage( ) method, just like any JMS MessageListener. As an example, suppose we want to expose the profile management capabilities of our Profile EJB to messaging clients. We may decide to do this if we plan to support clients that can't utilize direct EJB calls or that have a need for asynchronous calls. We can use a message-driven EJB to do this. Our message-driven EJB can run within our EJB container along with our Profile EJB and act as a proxy between the Profile EJB and messaging clients. The full source for our message-driven ProfileProxyBean can be found in the source code bundle for the book, in the file com/oreilly/jent/ejb/messageDriven/ProfileProxyBean.java. Message-driven beans require the following EJB container callbacks :
Our ProfileProxyBean includes these methods, of course. The ejbRemove( ) and setMessageDrivenContext( ) methods are fairly simple: ejbRemove( ) doesn't need to do anything in our case because we don't have any resources to clean up, and setMessageDrivenContext( ) simply stores the context in an instance member variable. The ejbCreate( ) method is a little more interesting because it needs to initialize a connection to the home interface for the Profile entity EJB: public void ejbCreate ( ) throws CreateException { System.out.println("Create called on ProfileProxyBean."); try { Context ctx = new InitialContext( ); mProfileHome = (ProfileHome)PortableRemoteObject.narrow( ctx.lookup("ejb/ProfileHome"), ProfileHome.class); } catch (NamingException ne) { throw new CreateException("Failed to locate home interface: " + ne.getMessage( )); } } A message-driven bean also implements the javax.jms.MessageListener interface and therefore needs to have an onMessage( ) method for dealing with incoming messages. In our ProfileProxyBean, we expect to receive a MapMessage containing name/value pairs representing entries to be set on a particular profile. public void onMessage(Message msg) { if (msg instanceof MapMessage) { MapMessage mMsg = (MapMessage)msg; try { String name = mMsg.getString("OWNER"); if (name != null) { Profile prof = mProfileHome.findByPrimaryKey(name); Enumeration eNames = mMsg.getMapNames( ); while (eNames.hasMoreElements( )) { String eName = (String)eNames.nextElement( ); String eVal = mMsg.getString(eName); if (!eName.equals("OWNER")) { prof.setEntry(eName, eVal); } } } } catch (JMSException je) { System.out.println("JMS error processing message to ProfileProxy: " + je.getMessage( )); } catch (RemoteException re) { System.out.println("Remote exception while accessing profile: " + re.getMessage( )); } catch (FinderException fe) { System.out.println("Failed to find Profile named in message: " + fe.getMessage( )); } } else { System.out.println("Non-MapMessage received by ProfileProxy, type = " + msg.getClass( ).getName( )); } } The name of the profile owner is expected in the OWNER property on the MapMessage. In the onMessage( ) method, we get the name of the target profile, then use findByPrimaryKey( ) on the ProfileHome to get a reference to the Profile to be updated. Next, we get a list of the property names present in the MapMessage and set the corresponding entry on the Profile according to the values found in the message. 6.8.1. Deploying Message-Driven EJBsDeploying our message-driven bean is the same general procedure as for session and entity beans. We need to provide a <message-driven> element in an ejb-jar.xml file that describes our bean. In this case, we would use an entry such as the following: <ejb-jar ...> <enterprise-beans> ... <!-- A message-driven proxy for the Profile beans --> <message-driven> <ejb-name>MessageDrivenProfileProxy</ejb-name> <ejb-class> com.oreilly.jent.ejb.messageDriven.ProfileProxyBean </ejb-class> <transaction-type>Container</transaction-type> <message-destination-type> javax.jms.Queue </message-destination-type> </message-driven> ... </enterprise-beans> ... </ejb-jar> The <message-driven> element lists the name used to refer to the bean (<ejb-name>), the class of the bean implementation (<ejb-class>), whether we want the transactions managed by the bean or the container (<transaction-type>), and what type of JMS destination the bean should be connected to (<message-destination-type>). 6.8.2. Message-Driven ClientsA client of a message-driven EJB simply connects to the JMS destination for the bean and exchanges messages with it: it doesn't need to use any EJB API calls at all. A client that wanted to use our message-driven Profile bean would use basic JMS code such as the following to update a user's profile: Context context = new InitialContext(...); // Look up the JMS queue connection factory, make a connection, start it QueueConnectionFactory qFactory = (QueueConnectionFactory)context.lookup( "java:comp/env/jms/jent-EJB-connFactory"); QueueConnection qConn = qFactory.createQueueConnection( ); qConn.start( ); // Look up the JMS message queue for the message-driven bean, and // create a session with it Queue profQueue = (Queue)context.lookup("jms/ProfileProxyQueue"); QueueSession qSession = qConn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE); // Create a sender QueueSender sender = qSession.createSender(profQueue); // Create a MapMessage MapMessage msg = qSession.createMapMessage( ); // Set the name for the target profile msg.setString("OWNER", "Kaitlyn"); // Set an entry value msg.setString("favoriteColor", "green"); // Send the message sender.send(msg); The first half of this code segment is simply establishing a session with the message destination. Here, we've assumed that our message-driven bean has been associated with a message queue stored in JNDI under the name java:comp/env/jms/jent-EJB-connFactory. In the last few lines of this client code, we construct a MapMessage, set the OWNER field in the map to be the name of the owner of the target profile, and then set one or more profile entries to be used to update the profile. |