This exercise is an extension of the preceding one.
You’ll add a
message-driven
bean (MDB), ReservationProcessor
, which plays the
same role as the TravelAgent EJB but receives its booking orders
through a JMS queue instead of synchronous RMI invocations.
To test the MDB, you’ll build a new client application that makes multiple reservations in batch, using a JMS queue that’s bound to the MDB. You’ll also build a second client application that listens on another queue to receive booking confirmations.
Along the way, you’ll learn how to create a new JMS queue in JBoss and configure a message-driven bean (MDB).
Because the exercise uses the ProcessPayment EJB used in recent
exercises, the database must contain the PAYMENT
table. The createdb
and dropdb
Ant targets, Java code, and clients here have been borrowed from
exercise 12_1.
If you haven’t already dropped the
PAYMENT
table after running the examples in
Exercise 12.1, do so now by running the dropdb
Ant
target.
C:workbookex12_2>ant dropdb Buildfile: build.xml prepare: compile: dropdb: [java] Looking up home interfaces.. [java] Dropping database table... BUILD SUCCESSFUL
Then re-create the PAYMENT
database table by
running the createdb
Ant target:
C:workbookex12_2>ant createdb Buildfile: build.xml prepare: compile: ejbjar: createdb: [java] Looking up home interfaces.. [java] Creating database table...
On the JBoss console, the following lines are displayed:
INFO [STDOUT] Creating table PAYMENT... INFO [STDOUT] ...done!
If you’re having trouble creating the database, shut
down JBoss. Then run the Ant build target
clean.db
. This will remove all database files and
allow you to start fresh.
The persistence of all other entity beans used in this exercise is managed by the container, so it will create the needed tables for them automatically.
This exercise requires two different JMS queues, one for the ReservationProcessor MDB and one to receive booking confirmations.
Adding new JMS queues to JBoss is much like adding new JMS topics. As in the preceding exercise, you have two options, one involving a configuration file, the other the JMX HTTP connector.
The most common way to set up a
JMS queue is to use an XML configuration
file. This part of the exercise shows you how to write a JMX MBean
definition for a new JMS queue. You can find the JMX configuration
file in ex12_2/src/resources/services
.
<server>
<mbean code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,
name=titan-ReservationQueue">
<depends optional-attribute-name="DestinationManager"
>jboss.mq:service=DestinationManager</depends>
</mbean>
<mbean code="org.jboss.mq.server.jmx.Queue"
name="jboss.mq.destination:service=Queue,
name=titan-TicketQueue">
<depends optional-attribute-name="DestinationManager"
>jboss.mq:service=DestinationManager</depends>
</mbean>
</server>
Recall that each set of MBeans must be defined within a
<server>
tag and each MBean declared in an
<mbean>
tag. Because this exercise requires
two different queues, we’ve defined two MBeans. The
MBean class that represents a JMS queue is
org.jboss.mq.server.jmx.Queue
. Its
name
property specifies the name of the JMS queue
to be created, such as titan-ReservationQueue
and
titan-TicketQueue
.
Remember also that the application server must deploy the
DestinationManager
MBean before any queue or topic
is deployed. This dependency is declared within the
<depends>
tag in
jbossmq-titanqueues-service.xml
. JBoss will take
care of satisfying this dependency and make sure the
titan-ReservationQueue
and
titan-TicketQueue
will not be started until the
DestinationManager
MBean has finished initializing
and is ready to provide services to new queues and topics. Copying
this file into the JBoss deploy directory will hot-deploy these JMS
queues and make them ready for use.
To deploy jbossmq-titanqueues-service.xml
, run
the make-queues
Ant target:
C:workbookex12_2>ant make-queues
Buildfile: build.xml
make-queues:
[copy] Copying 1 file to C:jboss-4.0
serverdefaultdeploy
On the server side, the following lines are displayed:
[titan-ReservationQueue]Bound to JNDI name: queue/titan-ReservationQueue
[titan-TicketQueue]Bound to JNDI name: queue/titan-TicketQueue
You must deploy the XML file containing the queues before you deploy the JAR containing your beans (see below). If you deploy your EJB JAR first, JBoss detects that the MDB’s expected queue does not exist and creates it dynamically. Then, when you try to deploy the XML file that contains the queues, an exception arises, and you’ll be told you’re trying to create a queue that already exists.
Add
each of the new JMS queues through
the JMX HTTP connector the same way you added the JMS topic in the
preceding exercise, with one obvious difference: instead of using the
createTopic( )
operation of the JBossMQ server, use
the createQueue( )
operation.
Remember that queues and topics created in the JMX HTTP Connector live only until the application server is shut down.
The ejb-jar.xml
file for this exercise is based
on the one for Exercise 12.1. The only notable difference is the
addition of the new ReservationProcessor MDB.
<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>
The MDB descriptor specifies container-managed transactions and
automatic acknowledgement of messages, and that messages will be
received from a queue rather than a topic. The descriptor also
contains a <message-selector>
tag that
allows the MDB to receive only those messages that conform to a
specified format. Then a set of <ejb-ref>
entries identifies all the beans that ReservationProcessor beans will
use during their execution:
<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-link>ProcessPaymentEJB</ejb-link> </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-link>CustomerEJB</ejb-link> </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-link>CruiseEJB</ejb-link> </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-link>CabinEJB</ejb-link> </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-link>ReservationEJB</ejb-link> </ejb-local-ref> <security-identity> <run-as><role-name>everyone</role-name></run-as> </security-identity>
Because the MDB will send a confirmation message to a queue once the
booking has been successful, it needs a reference to a
javax.jms.QueueConnectionFactory
, specified in the
<resource-ref>
at the end of the MDB
descriptor:
<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>
Note this difference from the preceding exercise: while this bean
does send messages to a queue, its descriptor does not contain a
<resource-env-ref>
entry that refers to the
destination queue. Why not? In Exercise 12.1, the destination was
fixed at deployment time, but in this exercise the destination is not
fixed and not even known by the MDB. It is the client application
that knows the destination, and transmits it to the MDB by
serializing the JMS queue object as part of the JMS message.
No modifications have been made to the CMP entity beans, so the
jbosscmp-jdbc.xml
file is unchanged.
The jboss.xml
file does need modification to
take the new ReservationProcessor EJB into account.
<message-driven> <ejb-name>ReservationProcessorEJB
</ejb-name> <destination-jndi-name >queue/titan-ReservationQueue
< /destination-jndi-name> <resource-ref> <res-ref-name>jms/QueueFactory
</res-ref-name> <jndi-name>java:/JmsXA
</jndi-name> </resource-ref> </message-driven>
The
<destination-jndi-name>
tag maps the MDB to an existing JMS
destination in the deployment environment. You should recognize the
name of one of the two JMS queues you just created:
titan-ReservationQueue
.
The
<resource-ref>
tag maps the ConnectionFactory name
used by the ReservationProcessor EJB to an actual factory in the
deployment environment. This mapping is identical to the one in the
exercise for the TravelAgent EJB.
Perform the following steps:
Open a command prompt or shell terminal and change to the
ex12_2
directory created by the extraction
process
Set the JAVA_HOME
and
JBOSS_HOME
environment variables to point to where
your JDK and JBoss 4.0 are installed. Examples:
Windows:C:workbookex12_2> set JAVA_HOME=C:jdk1.4.2 C:workbookex12_2> set JBOSS_HOME=C:jboss-4.0
|
Unix:$ export JAVA_HOME=/usr/local/jdk1.4.2 $ export JBOSS_HOME=/usr/local/jboss-4.0
|
Add ant
to your execution path.
Windows:C:workbookex12_2> set PATH=..antin;%PATH%
|
Unix:$ export PATH=../ant/bin:$PATH
|
Perform the build by typing ant
.
As in the last exercise, you will see titan.jar
rebuilt, copied to the JBoss deploy
directory,
and redeployed by the application server.
In this exercise, you’ll use two client applications at the same time. The producer generates large numbers of JMS messages reporting passage bookings, destined for the ReservationProcessor MDB EJB. The consumer listens to a JMS queue for messages confirming the bookings, and displays them as they come in.
The producer first gets the cruise ID and the number of bookings from the command line.
public static void main (String [] args) throws Exception { if (args.length != 2) throw new Exception ("Usage: java JmsClient_ReservationProducer <CruiseID> <count>"); Integer cruiseID = new Integer (args[0]); int count = new Integer (args[1]).intValue ( );
The producer then looks up a
QueueConnectionFactory
and two JMS queues from the
JBoss naming service. The first queue is the one bound to the
ReservationProcessor MDB, to which passage booking messages will be
sent. The second is not used directly, as you’ll see
later.
QueueConnectionFactory factory = (QueueConnectionFactory) jndiContext.lookup ("ConnectionFactory
"); Queue reservationQueue = (Queue) jndiContext.lookup ("queue/titan-ReservationQueue
"); Queue ticketQueue = (Queue) jndiContext.lookup ("queue/titan-TicketQueue
"); QueueConnection connect = factory.createQueueConnection ( ); QueueSession session = connect.createQueueSession (false,Session.AUTO_ACKNOWLEDGE); QueueSender sender = session.createSender (reservationQueue);
The client application is now ready to send count
booking messages in batch. Among other chores, it has looked up the
ticket queue, the JMS queue that the ReservationProcessor MDB will
use to send confirmation messages.
For each booking, it then creates a JMS
MapMessage
, assigns the ticket queue into the
message’s JMSReplyTo
property,
and sets the booking data: Cruise ID, Customer ID, Cabin ID, price,
credit card number, and expiration date, and so on. Note that only
basic data types such as String
and
int
can be stored in a
MapMessage
:
for (int i = 0; i < count; i++) { MapMessage message =session.createMapMessage
( ); // Used in ReservationProcessor to send Tickets back out message.setJMSReplyTo
(ticketQueue); message.setStringProperty ("MessageFormat", "Version 3.4"); message.setInt ("CruiseID", cruiseID.intValue ( )); // either Customer 1 or 2, all we've got in database message.setInt ("CustomerID", i%2 + 1); // cabins 100-109 only message.setInt ("CabinID", i%10 + 100); message.setDouble ("Price", (double)1000 + i); // the card expires in about 30 days Date expDate = new Date (System.currentTimeMillis ( ) + 30*24*60*60*1000L); message.setString ("CreditCardNum", "5549861006051975"); message.setLong ("CreditCardExpDate", expDate.getTime ( )); message.setString ("CreditCardType", CreditCardDO.MASTER_CARD); System.out.println ("Sending reservation message #" + i); sender.send (message); } connect.close ( ); }
One interesting property that’s set in the JMS
message header is MessageFormat
. Recall that the
<message-selector>
tag in the MDB deployment
descriptor used this property to specify a constraint on the messages
the MDB is to receive.
Once all messages are sent, the application closes the connection and terminates. Because messages are sent asynchronously, the application may terminate before the ReservationProcessor EJB has processed all of the messages in the batch.
The consumer application is very similar to the client application in Exercise 12.1. This time, though, it will subscribe not to a topic but to a queue.
To receive JMS messages, the client application class implements the
javax.jms.MessageListener
interface, which defines
the onMessage( )
method. The main method simply
creates an instance of the class and uses a trick to make the main
thread wait indefinitely:
public class JmsClient_TicketConsumer
implements javax.jms.MessageListener
{
public static void main (String [] args) throws Exception
{
new JmsClient_TicketConsumer
( );
while(true) { Thread.sleep (10000); }
}
The constructor is very simple JMS code that subscribes the client application to the JMS queue and waits for incoming messages:
public JmsClient_TicketConsumer ( ) throws Exception { Context jndiContext = getInitialContext ( ); QueueConnectionFactory factory = (QueueConnectionFactory) jndiContext.lookup ("ConnectionFactory
"); Queue ticketQueue = (Queue) jndiContext.lookup ("queue/titan-TicketQueue
"); QueueConnection connect = factory.createQueueConnection ( ); QueueSession session = connect.createQueueSession (false,Session.AUTO_ACKNOWLEDGE); QueueReceiver receiver = session.createReceiver (ticketQueue); receiver.setMessageListener (this); System.out.println ("Listening for messages on titan- TicketQueue..."); connect.start ( ); }
When a message arrives in the queue, the consumer’s
onMessage( )
method is called. The method simply
displays the content of the ticket:
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 displayed) { displayed.printStackTrace ( ); } }
When you redeployed titan.jar, JBoss dropped and
recreated the database tables, destroying any existing content, so
you must repopulate the database. Have Ant execute the
run.client_112a
target.
The run.client_112a
target originated in Exercise
11.2, but we’ve duplicated it in the
ex12_2
directory for your convenience.
C:workbookex12_2>ant run.client_112a Buildfile: build.xml prepare: compile: ejbjar: run.client_112a: [java] Calling TravelAgentBean to create sample data.. [java] All customers have been removed [java] All cabins have been removed [java] All ships have been removed [java] All cruises have been removed [java] All reservations have been removed [java] Customer with ID 1 created (Burke Bill) [java] Customer with ID 2 created (Labourey Sacha) [java] Created ship with ID 101... [java] Created ship with ID 102... [java] Created cabins on Ship A with IDs 100-109 [java] Created cabins on Ship B with IDs 200-209 [java] Created Alaska Cruise with ID 0 on ShipA [java] Created Norwegian Fjords Cruise with ID 1 on ShipA [java] Created Bermuda or Bust Cruise with ID 2 on ShipA [java] Created Indian Sea Cruise with ID 3 on ShipB [java] Created Australian Highlights Cruise with ID 4 on ShipB [java] Created Three-Hour Cruise with ID 5 on ShipB [java] Made reservation for Customer 1 on Cruise 0 for Cabin 103 [java] Made reservation for Customer 1 on Cruise 5 for Cabin 208 [java] Made reservation for Customer 2 on Cruise 1 for Cabin 105 [java] Made reservation for Customer 2 on Cruise 5 for Cabin 202 BUILD SUCCESSFUL
At this point, you’re going to launch both the
client that sends booking messages and the client that receives the
tickets as passage confirmations. Launch the consumer
first by invoking the Ant target
run.client_122
:
C:workbookex12_2>ant run.client_122 Buildfile: build.xml prepare: compile: ejbjar: run.client_122: [java] Listening for messages on titan-TicketQueue...
Now start the producer, adhering to the following usage:
BookInBatch <cruiseID> <count>
where cruiseID
is the ID of a Cruise in the
database (created when you invoked the
run.client_112a
Ant target) and
count
is the number of passages to book.
Book 100 passages on the Alaskan Cruise:
C:workbookex12_2>BookInBatch 0 100 Buildfile: build.xml prepare: compile: ejbjar: run.bookinbatch: [java] Sending reservation message #0 [java] Sending reservation message #1 [java] Sending reservation message #2 [java] Sending reservation message #3 ... [java] Sending reservation message #98 [java] Sending reservation message #99
Shortly after the producer starts, the consumer, which has been patiently listening to its JMS queue for booking confirmations, will display:
run.client_122: [java] Listening for messages on titan-TicketQueue... [java]********************************
[java]Bob Smith has been booked for the Alaska Cruise cruise
on ship Nordic Prince.
[java]Your accommodations include Suite 100 a 1 bed cabin on
deck level 1.
[java]Total charge = 1000.0
[java]********************************
[java] ******************************** [java] Joseph Stalin has been booked for the Alaska Cruise cruise on ship Nordic Prince. [java] Your accommodations include Suite 101 a 1 bed cabin on deck level 1. [java] Total charge = 1001.0 [java] ******************************** [java] ******************************** [java] Bob Smith has been booked for the Alaska Cruise cruise on ship Nordic Prince. [java] Your accommodations include Suite 102 a 1 bed cabin on deck level 1. [java] Total charge = 1002.0 [java] ******************************** ... [java] ******************************** [java] Joseph Stalin has been booked for the Alaska Cruise cruise on ship Nordic Prince. [java] Your accommodations include Suite 109 a 1 bed cabin on deck level 1. [java] Total charge = 1099.0 [java] ********************************
Note that because the booking confirmation messages are queued, you could start the consumer much later than the producer, rather than before. The confirmation messages sent by the ReservationProcessor MDB would then be stored on the server until the client application starts and begins to listen to the queue.