To illustrate how point-to-point messaging works, we will use a
simple decoupled request/reply
example where a QBorrower
class makes
a simple mortgage loan request to a QLender
class using point-to-point messaging.
The QBorrower
class sends the loan
request to the QLender
class using a
LoanRequest
queue, and based on certain business
rules, the QLender
class sends a
response back to the QBorrower
class
using a LoanResponseQ
queue indicating whether the
loan request was approved or denied. Since the QBorrower
is interested in finding out right
away whether the loan was approved or not, once the loan request is
sent, the QBorrower
class will block
and wait for a response from the QLender
class before proceeding. This simple
example models a typical messaging request/reply scenario.
Before looking at the code, let’s look at how the application
works. As with the Chat
application, the QBorrower
class
and QLender
class both include a
main()
method so they can be run as
a standalone Java application. To keep the code vendor-agnostic, both
classes need the connection factory name and queue names when starting
the application. The QLender
class
is executed from the command line as follows:
java ch04.p2p.QLender ConnectionFactory RequestQueue
where ConnectionFactory
is
the name of the queue connection factory defined in your JMS provider
and RequestQueue
is the name of the
queue that the QLender
class should
be listening on to receive loan requests. As you’ll see later in this
chapter, the QBorrower
sends the destination for the
response message in the JMSReplyTo
header property, which is why you do not need to specify it when
starting the QLender
class.
The QBorrower
class can be
executed in the same manner in a separate command window:
java ch04.p2p.QBorrower ConnectionFactory RequestQueue ReplyQueue
where ConnectionFactory
is
the name of the queue connection factory defined in your JMS provider,
RequestQueue
is the name of the
queue that the QBorrower
class
should send loan requests to, and ReplyQueue
is the name of the queue that the
QBorrower
class should use to
receive the results from the QLender
class.
You will also need to define a jndi.properties file in your classpath that contains the JNDI connection information for the JMS provider. The jndi.properties file contains the initial context factory class, provider URL, username, and password needed to connect to the JMS server. Each vendor will have a different context factory class and URL name for connecting to the server. You will need to consult the documentation of your specific JMS provider or Java EE container to obtain these values. We have included the steps for configuring ActiveMQ to run the examples in this chapter in Appendix D.
The QBorrower
and QLender
classes both require the queue
connection factory name and queue names to run. We have chosen to name
the connection factory QueueCF
, and
the loan request and loan response queues LoanRequestQ
and LoanResponseQ
, respectively. These JNDI
resources are typically configured in the JMS provider XML
configuration files or configuration screens. You will need to consult
your JMS provider documentation on how to configure these resources
(please refer to Appendix D for the specific
configuration settings for ActiveMQ used to run the examples in this
chapter).
You can run the QBorrower
and
QLender
classes by entering the
following two commands in separate command windows:
java ch04.p2p.QLender QueueCF LoanRequestQ java ch04.p2p.QBorrower QueueCF LoanRequestQ LoanResponseQ
When the QBorrower
class
starts, you will be prompted to enter a salary amount and the
requested loan amount. When you press enter, the QBorrower
class will send the salary and
loan amount to the QLender
class
via the LoanRequestQ
queue, wait
for the response on the LoanResponseQ
queue, and display whether the
loan was approved or denied:
QBorrower Application Started Press enter to quit application Enter: Salary, Loan_Amount e.g. 50000, 120000 > 80000, 200000 Loan request was Accepted! > 50000, 300000 Loan request was Declined >
Here’s what happened. The QBorrower
sent the salary ($80,000) and the
loan amount ($200,000) to the LoanRequestQ
queue, then blocked and waited
for a response from the QLender
class. The QLender
class received
the request on the LoanRequestQ
queue, applied the simple business logic based on the salary to loan
ratio, and sent back the response on the LoanResponseQ
queue. The message was then
received by the QBorrower
class, and the contents of
the return message displayed on the console. This interaction is
illustrated in Figure 4-3.
The rest of this chapter examines the source code for the
QBorrower
and QLender
classes, and covers several advanced
subjects related to the point-to-point messaging model.
The QBorrower
class is
responsible for sending a loan request message to a queue containing a
salary and loan amount. The class is fairly straightforward: the
constructor establishes a connection to the JMS provider, creates a
QueueSession
, and gets the request
and response queues using a JNDI lookup. The main
method instantiates the QBorrower
class and, upon receiving a salary
and loan amount from standard input, invokes the sendLoanRequest
method to send the message
to the queue. Here is the listing for the QBorrower
class in its entirety. We will be
examining the JMS aspects of this class in detail after the full
listing:
package ch04.p2p; import java.io.*; import java.util.StringTokenizer; import javax.jms.*; import javax.naming.*; public class QBorrower { private QueueConnection qConnect = null; private QueueSession qSession = null; private Queue responseQ = null; private Queue requestQ = null; public QBorrower(String queuecf, String requestQueue, String responseQueue) { try { // Connect to the provider and get the JMS connection Context ctx = new InitialContext(); QueueConnectionFactory qFactory = (QueueConnectionFactory) ctx.lookup(queuecf); qConnect = qFactory.createQueueConnection(); // Create the JMS Session qSession = qConnect.createQueueSession( false, Session.AUTO_ACKNOWLEDGE); // Lookup the request and response queues requestQ = (Queue)ctx.lookup(requestQueue); responseQ = (Queue)ctx.lookup(responseQueue); // Now that setup is complete, start the Connection qConnect.start(); } catch (JMSException jmse) { jmse.printStackTrace(); System.exit(1); } catch (NamingException jne) { jne.printStackTrace(); System.exit(1); } } private void sendLoanRequest(double salary, double loanAmt) { try { // Create JMS message MapMessage msg = qSession.createMapMessage(); msg.setDouble("Salary", salary); msg.setDouble("LoanAmount", loanAmt); msg.setJMSReplyTo(responseQ); // Create the sender and send the message QueueSender qSender = qSession.createSender(requestQ); qSender.send(msg); // Wait to see if the loan request was accepted or declined String filter = "JMSCorrelationID = '" + msg.getJMSMessageID() + "'"; QueueReceiver qReceiver = qSession.createReceiver(responseQ, filter); TextMessage tmsg = (TextMessage)qReceiver.receive(30000); if (tmsg == null) { System.out.println("QLender not responding"); } else { System.out.println("Loan request was " + tmsg.getText()); } } catch (JMSException jmse) { jmse.printStackTrace(); System.exit(1); } } private void exit() { try { qConnect.close(); } catch (JMSException jmse) { jmse.printStackTrace(); } System.exit(0); } public static void main(String argv[]) { String queuecf = null; String requestq = null; String responseq = null; if (argv.length == 3) { queuecf = argv[0]; requestq = argv[1]; responseq = argv[2]; } else { System.out.println("Invalid arguments. Should be: "); System.out.println ("java QBorrower factory requestQueue responseQueue"); System.exit(0); } QBorrower borrower = new QBorrower(queuecf, requestq, responseq); try { // Read all standard input and send it as a message BufferedReader stdin = new BufferedReader (new InputStreamReader(System.in)); System.out.println ("QBorrower Application Started"); System.out.println ("Press enter to quit application"); System.out.println ("Enter: Salary, Loan_Amount"); System.out.println("\ne.g. 50000, 120000"); while (true) { System.out.print("> "); String loanRequest = stdin.readLine(); if (loanRequest == null || loanRequest.trim().length() <= 0) { borrower.exit(); } // Parse the deal description StringTokenizer st = new StringTokenizer(loanRequest, ",") ; double salary = Double.valueOf(st.nextToken().trim()).doubleValue(); double loanAmt = Double.valueOf(st.nextToken().trim()).doubleValue(); borrower.sendLoanRequest(salary, loanAmt); } } catch (IOException ioe) { ioe.printStackTrace(); } } }
The main
method of the
QBorrower
class accepts three
arguments from the command line: the JNDI name of the queue connection
factory, the JNDI name of the loan request queue, and finally, the
JNDI name of the loan response queue where the response from the
QLender
class will be received.
Once the input parameters have been validated, the QBorrower
class is instantiated and a loop
is started that reads the salary and loan amount into the class from
the console:
String loanRequest = stdin.readLine();
The salary and loan amount input data is then parsed, and
finally the sendLoanRequest
method
invoked. The input loop continues until the user presses enter on the
console without entering any data:
if (loanRequest == null || loanRequest.trim().length() <= 0) { borrower.exit(); }
Now let’s look at the JMS portion of the code in detail,
starting with the constructor and ending with the sendLoanRequest
method.
In the QBorrower
class
example, all of the JMS initialization logic is handled in the
constructor. The first thing the constructor does is establish a
connection to the JMS provider by creating an InitialContext
:
Context ctx = new InitialContext();
The connection information needed to connect to the JMS
provider is specified in the jndi.properties
file located in the classpath (see Appendix D
for an example). Once we have a JNDI context
, we can get the QueueConnectionFactory
using the JNDI
connection factory name passed into the constructor arguments. The
QueueConnectionFactory
is then
used to create the QueueConnection
using a factory method on
the QueueConnectionFactory
:
QueueConnectionFactory qFactory = (QueueConnectionFactory) ctx.lookup(queuecf); qConnect = qFactory.createQueueConnection();
Alternatively, you can pass a username and password into the
createQueueConnection
method as
String
arguments to perform basic
authentication on the connection. A JMSSecurityException
will be thrown if the
user fails to authenticate:
qConnect = qFactory.createQueueConnection("system", "manager");
At this point a connection is created to the JMS provider.
When the QueueConnection
is
created, the connection is initially in stopped
mode. This means you can send messages to the queue, but
no message consumers (including the QBorrower
class, which is also a message
consumer) may receive messages from this connection until it is
started.
The QueueConnection
object
is used to create a JMS Session
object (specifically, a QueueSession
), which is the working thread
and transactional unit of work in JMS. Unlike JDBC, which requires a
connection for each transactional unit of work, JMS uses a single
connection and multiple Session
objects. Typically, applications will create a single JMS Connection
on application startup and
maintain a pool of Session
objects for use whenever a message needs to be produced or
consumed.
The QueueSession
object is
created through a factory object on the QueueConnection
object. The QueueConnection
variable is declared
outside of the constructor in our example so that the connection can
be closed in the exit
method of
the QBorrower
class. It is
important to close the connection after it is no longer being used
to free up resources. Closing
the Connection
object also closes
any open Session
objects
associated with the connection. The statement in the constructor to
create the QueueSession
is as
follows:
qSession = qConnect.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
Notice that the createQueueSession
method takes two
parameters. The first parameter indicates whether the QueueSession
is transacted or not. A value
of true
indicates that the
session is transacted, meaning that messages sent to queues during
the lifespan of the QueueSession
will not be
delivered to the receivers until the commit
method is invoked on the QueueSession
. Likewise, invoking the
rollback
method on the QueueSession
will remove any
messages sent during the transacted session. The second parameter indicates the
acknowledgment mode. The three possible
values are Session.AUTO_ACKNOWLEDGE
,
Session.CLIENT_ACKNOWLEDGE
, and
Session.DUPS_OK_ACKNOWLEDGE
. The
acknowledgment mode is ignored if the session is transacted.
Acknowledgment modes are discussed in more detail in Chapter 7.
The next two lines in the constructor perform a JNDI lookup to
the JMS provider to obtain the administered destinations. In our
case, the JMS destination is cast to a Queue
. The argument supplied to each of
the lookup
methods is a String
value containing the JNDI name of
the queues we are using in the class:
requestQ = (Queue)ctx.lookup(requestQueue); responseQ = (Queue)ctx.lookup(responseQueue);
The final line of code starts the connection, allowing messages to be received on this connection. It is generally a good idea to perform all of your initialization logic before starting the connection:
qConnect.start();
Interestingly enough, you do not need to start the connection if all you are doing is sending messages. However, it is generally advisable to start the connection to avoid future issues if there is a chance the connection may be shared or request/reply processing added to the sender class.
Another useful thing you can obtain from the JMS Connection
is the metadata about the
connection. Invoking the getMetaData
method on the Connection
object gives you a ConnectionMetaData
object that provides
useful information such as the JMS version, JMS provider name, JMS
provider version, and the JMSX property name extensions supported by
the JMS provider:
import java.util.Enumeration; import javax.jms.ConnectionMetaData; import javax.jms.JMSException; import javax.jms.QueueConnection; import javax.jms.QueueConnectionFactory; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; public class MetaData { public static void main(String[] args) { try { Context ctx = new InitialContext(); QueueConnectionFactory qFactory = (QueueConnectionFactory) ctx.lookup("QueueCF"); QueueConnection qConnect = qFactory.createQueueConnection(); ConnectionMetaData metadata = qConnect.getMetaData(); System.out.println("JMS Version: " + metadata.getJMSMajorVersion() + "." + metadata.getJMSMinorVersion()); System.out.println("JMS Provider: " + metadata.getJMSProviderName()); System.out.println("JMSX Properties Supported: "); Enumeration e = metadata.getJMSXPropertyNames(); while (e.hasMoreElements()) { System.out.println(" " + e.nextElement()); } } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } } }
For example, invoking the previous code using the ActiveMQ open source JMS provider will yield the following results:
JMS Version: 1.1 JMS Provider: ActiveMQ JMSX Properties Supported: JMSXGroupID JMSXGroupSeq JMSXDeliveryCount JMSXProducerTXID
This information can be logged on application startup, indicating the JMS provider and version numbers. It is particularly useful for products or applications that may use multiple providers.
Once the QBorrower
class is
initialized, the salary and loan amounts are entered through the
command line. At this point, the sendLoanRequest
method is invoked from the
main
method to send the loan
request to the queue and wait for the response from the QLender
class. At the start of this
method, we chose to create a MapMessage
but we could have used any of
the five JMS message types:
MapMessage msg = qSession.createMapMessage(); msg.setDouble("Salary", salary); msg.setDouble("LoanAmount", loanAmt); msg.setJMSReplyTo(responseQ);
Notice that the JMS message is created from the Session
object via a factory method
matching the message type. Instantiating a new JMS message object
using the new
keyword will not
work; it must be created from the Session
object. After creating and loading
the message object, we are also setting the JMSReplyTo
message header property to the
response queue, which further decouples the producer from the
consumer. The practice of setting the JMSReplyTo
header property in the message
producer as opposed to specifying the reply-to queue in the message
consumer is a standard practice when using the request/reply
model.
After the message is created, we then create the QueueSender
object, specifying the queue
we wish to send messages to, and then send the message using the
send
method:
QueueSender qSender = qSession.createSender(requestQ); qSender.send(msg);
There are several overridden send
methods available in the QueueSender
object. The one we are using
here accepts only the JMS message object as the single argument. The
other overridden methods allow you to specify the Queue
, the delivery mode, the message
priority, and finally the message expiry. Since we are not
specifying any of the other values in the example just shown, the
message priority is set to normal (4), the delivery mode is set to
persistent messages (DeliveryMode.PERSISTENT
), and the message
expiry (time to live) is set to 0, indicating that the message will
never expire. All of these parameters can be overridden by using one
of the other send
methods.
Once the message has been sent, the QBorrower
class will block and wait for a
response from the QLender
on
whether the loan was approved or denied. The first step in this
process is to set up a message selector so that we can correlate the
response message with the one we sent. This is necessary because
there may be many other loan requests being sent to and from the
loan request queues while we are making our loan request. To make
sure we get the proper response back, we would use a technique
called message correlation. Message correlation
is required when using the request/reply model of point-to-point
messaging where the queue is being shared by multiple producers and
consumers (see Message Correlation for more
details):
String filter = "JMSCorrelationID = '" + msg.getJMSMessageID() + "'"; QueueReceiver qReceiver = qSession.createReceiver(responseQ, filter);
Notice we specify the filter when creating the QueueReceiver
, indicating that we only want to receive messages when the
JMSCorrelationID
is equal to the
original JMSMessageID
. Now that we have a
QueueReceiver
, we can invoke the
receive
method to do a blocking
wait until the response message is received. In this case, we are
using the overridden receive
method that accepts a timeout value in milliseconds:
TextMessage tmsg = (TextMessage)qReceiver.receive(30000); if (tmsg == null) { System.out.println("QLender not responding"); } else { System.out.println("Loan request was " + tmsg.getText()); }
It is a good idea to always specify a reasonable timeout value
on the receive
method; otherwise,
it will sit there and wait forever (in effect, the application would
“hang”). Specifying a reasonable timeout value allows the
request/reply sender (in this case the QBorrower
) to take action in the event the
message has not been delivered in a timely fashion or there is a
problem on the receiving side (in this case the QLender
). If a timeout condition does
occur, the message returned from the receive
method will be null
. Note that it is the entire message
object that is null
, not just the
message payload. The receive
method returns a Message
object.
If the message type is known, then you can cast the return message
as we did in the preceding code example. However, a more failsafe
technique would be to check the return Message
type using the instanceof
keyword as indicated here:
Message rmsg = qReceiver.receive(30000); if (rmsg == null) { System.out.println("QLender not responding"); } else { if (rmsg instanceof TextMessage) { TextMessage tmsg = (TextMessage)rmsg; System.out.println("Loan request was " + tmsg.getText()); } else { throw new IllegalStateException("Invalid message type); } }
Notice that the message received does not need to be of the
same message type as the one sent. In the example just shown, we
sent the loan request using a MapMessage
, yet we received the response
from the receiver as a TextMessage
. While you could potentially
increase the level of decoupling between the sender and receiver by
including the message type as part of the application properties of
the message, you would still need to know how to interpret the
payload in the message. For example, with a StreamMessage
or BytesMessage
you would still need to know
the order of data being sent so that you could in turn read it in
the proper order and data type. As you can guess, because of the
“contract” of the data between the sender and receiver, there is
still a fair amount of coupling in the point-to-point model, at
least from the payload perspective.
The role of the QLender
class
is to listen for loan requests on the loan request queue, determine if
the salary meets the necessary business requirements, and finally send
the results back to the borrower. Notice that the QLender
class is structured a bit
differently from the QBorrower
class. In our example, the QLender
class is referred to as a message listener and,
as such, implements the javax.jms.MessageListener
interface and
overrides the onMessage()
method.
Here is the complete listing for the QLender
class:
package ch04.p2p; import java.io.*; import javax.jms.*; import javax.naming.*; public class QLender implements MessageListener { private QueueConnection qConnect = null; private QueueSession qSession = null; private Queue requestQ = null; public QLender(String queuecf, String requestQueue) { try { // Connect to the provider and get the JMS connection Context ctx = new InitialContext(); QueueConnectionFactory qFactory = (QueueConnectionFactory) ctx.lookup(queuecf); qConnect = qFactory.createQueueConnection(); // Create the JMS Session qSession = qConnect.createQueueSession( false, Session.AUTO_ACKNOWLEDGE); // Lookup the request queue requestQ = (Queue)ctx.lookup(requestQueue); // Now that setup is complete, start the Connection qConnect.start(); // Create the message listener QueueReceiver qReceiver = qSession.createReceiver(requestQ); qReceiver.setMessageListener(this); System.out.println("Waiting for loan requests..."); } catch (JMSException jmse) { jmse.printStackTrace(); System.exit(1); } catch (NamingException jne) { jne.printStackTrace(); System.exit(1); } } public void onMessage(Message message) { try { boolean accepted = false; // Get the data from the message MapMessage msg = (MapMessage)message; double salary = msg.getDouble("Salary"); double loanAmt = msg.getDouble("LoanAmount"); // Determine whether to accept or decline the loan if (loanAmt < 200000) { accepted = (salary / loanAmt) > .25; } else { accepted = (salary / loanAmt) > .33; } System.out.println("" + "Percent = " + (salary / loanAmt) + ", loan is " + (accepted ? "Accepted!" : "Declined")); // Send the results back to the borrower TextMessage tmsg = qSession.createTextMessage(); tmsg.setText(accepted ? "Accepted!" : "Declined"); tmsg.setJMSCorrelationID(message.getJMSMessageID()); // Create the sender and send the message QueueSender qSender = qSession.createSender((Queue)message.getJMSReplyTo()); qSender.send(tmsg); System.out.println("\nWaiting for loan requests..."); } catch (JMSException jmse) { jmse.printStackTrace(); System.exit(1); } catch (Exception jmse) { jmse.printStackTrace(); System.exit(1); } } private void exit() { try { qConnect.close(); } catch (JMSException jmse) { jmse.printStackTrace(); } System.exit(0); } public static void main(String argv[]) { String queuecf = null; String requestq = null; if (argv.length == 2) { queuecf = argv[0]; requestq = argv[1]; } else { System.out.println("Invalid arguments. Should be: "); System.out.println ("java QLender factory request_queue"); System.exit(0); } QLender lender = new QLender(queuecf, requestq); try { // Run until enter is pressed BufferedReader stdin = new BufferedReader (new InputStreamReader(System.in)); System.out.println ("QLender application started"); System.out.println ("Press enter to quit application"); stdin.readLine(); lender.exit(); } catch (IOException ioe) { ioe.printStackTrace(); } } }
The QLender
class is what is
referred to as an asynchronous message listener,
meaning that unlike the prior QBorrower
class it will not block when
waiting for messages. This is evident from the fact that the QLender
class implements the MessageListener
interface and overrides the
onMessage
method.
The main
method of the
QLender
class validates the
command-line arguments and invokes the constructor by instantiating
a new QLender
class. It then keeps
the primary thread alive until the enter key is pressed on the command
line.
The constructor in the QLender
class works much in the same way as
the QBorrower
class. The first part
of the constructor establishes a connection to the provider, does a
JNDI lookup to get the queue, creates a QueueSession
, and starts the
connection:
... // Connect to the provider and get the JMS connection Context ctx = new InitialContext(); QueueConnectionFactory qFactory = (QueueConnectionFactory) ctx.lookup(queuecf); qConnect = qFactory.createQueueConnection(); // Create the JMS Session qSession = qConnect.createQueueSession( false, Session.AUTO_ACKNOWLEDGE); // Lookup the request queue requestQ = (Queue)ctx.lookup(requestQueue); // Now that setup is complete, start the Connection qConnect.start(); ...
Once the connection is started, the QLender
class can begin to receive messages.
However, before it can receive messages, it must be registered by the
QueueReceiver
as a message
listener:
QueueReceiver qReceiver = qSession.createReceiver(requestQ); qReceiver.setMessageListener(this);
At this point, a separate listener thread is started. That
thread will wait until a message is received, and upon receipt of a
message, will invoke the onMessage
method of the listener class. In this case, we set the message
listener to the QLender
class using
the this
keyword in the setMessageListener
method. We could have
easily delegated the messaging
work to another class that implemented the MessageListener
interface:
qReceiver.setMessageListener(someOtherClass);
When a message is received on the queue specified in the
createReceiver
method, the listener
thread will asynchronously invoke the onMessage
method of the listener class (in
our case, the QLender
class is also
the listener class). The onMessage
method first casts the message to a MapMessage
(the message type we are
expecting to receive from the borrower). It then extracts the salary
and loan amount requested from the message payload, checks the salary
to loan amount ratio, then determines whether to accept or decline the
loan request:
... public void onMessage(Message message) { try { boolean accepted = false; // Get the data from the message MapMessage msg = (MapMessage)message; double salary = msg.getDouble("Salary"); double loanAmt = msg.getDouble("LoanAmount"); // Determine whether to accept or decline the loan if (loanAmt < 200000) { accepted = (salary / loanAmt) > .25; } else { accepted = (salary / loanAmt) > .33; } System.out.println("" + "Percent = " + (salary / loanAmt) + ", loan is " + (accepted ? "Accepted!" : "Declined")); ...
Again, to make this more failsafe, it would be better to check
the JMS message type using the instanceof
keyword in the event another
message type was being sent to that queue:
if (message instanceof MapMessage) { //process request } else { throw new IllegalArgumentException("unsupported message type"); }
Once the loan request has been analyzed and the results
determined, the QLender
class needs
to send the response back to the borrower. It does this by first
creating a JMS message to send. The response message does not need to
be the same JMS message type as the loan request message that was
received by the QLender
. To
illustrate this point the QLender
returns a TextMessage
back to the
QBorrower
:
TextMessage tmsg = qSession.createTextMessage(); tmsg.setText(accepted ? "Accepted!" : "Declined");
The next statement sets the JMSCorrelationID
, which is the JMS header
property that is used by the QBorrower
class to filter incoming response
messages:
tmsg.setJMSCorrelationID(message.getJMSMessageID());
Message correlation is discussed in more detail in the next section of this chapter.
Once the message is created, the onMessage
method then sends the message to
the response queue specified by the JMSReplyTo
message header property. As you
may remember, in the QBorrower
class we set the JMSReplyTo
header
property when sending the original loan request. The QLender
class can now use that property as
the destination to send the response message to:
QueueSender qSender = qSession.createSender((Queue)message.getJMSReplyTo()); qSender.send(tmsg);
Get Java Message Service, 2nd Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.