11.2. Elements of Messaging with JMSThe principal players in a JMS system are messaging clients, message destinations, and a JMS-compatible messaging provider . Messaging clients produce and consume messages. Typically, messaging takes place asynchronously; a client produces a message and sends it to a message destination, and some time later another client receives the message. Message clients can be implemented using JMS, or they can use a native messaging API to participate in the messaging system. If a native message client (e.g., a client using the native IBM MQ Series APIs) produces a message to a message destination, a JMS connection to the native message system is responsible for retrieving the message, converting it into the appropriate JMS message representation, and delivering it to any relevant JMS-based clients. Message destinations are places to and from which JMS clients send and receive messages. Message destinations are created within a JMS provider that manages all of the administrative and runtime functions of the messaging system. At a minimum, a JMS provider allows you to specify a network address for a destination, allowing clients to find the destination on the network. But providers may also support other administrative options on destinations, such as persistence options, resource limits, and the like. 11.2.1. Messaging Styles: Point-to-Point and Publish-SubscribeGenerally speaking, asynchronous messaging usually comes in two flavors: a message can be addressed and sent to a single receiver (point-to-point), or a message can be published to a particular channel or topic and any receiver that subscribes to that channel will receive the message (publish-subscribe). These two messaging styles have analogies at several levels in the distributed computing "stack," all the way from the network level (standard TCP packet delivery versus multicast networking) to the application level (email versus newsgroups).Figure 11-1 depicts the two message models supported by JMS, as well as the key interfaces that come into play in a JMS context. We discuss the specifics of these interfaces later in the chapter. Most messaging providers support one or both of these messaging styles, so JMS provides support for them both in its API. JMS includes a set of generic messaging interfaces, described next. Each style of messaging is supported by specialized subclasses of these generic interfaces. Figure 11-1. JMS message models11.2.2. Key JMS InterfacesThe following key interfaces represent the concepts that come into play in any JMS client application, whether it is using point-to-point or publish-subscribe messaging:
11.2.3. A Generic JMS ClientA JMS client follows the same general sequence of operations, regardless of whether it's using point-to-point or publish-subscribe messaging, or both. We'll walk through these steps here, using the point-to-point JMS interfaces to demonstrate. For the most part, the same pseudocode can be used with the publish-subscribe interfaces by just substituting Topic for Queue in the code samples in this section. 11.2.3.1. General setupThe very first step for a JMS client is to get a reference to an InitialContext for the JNDI service of the JMS provider. Full details on the various options for obtaining a JNDI Context can be found in Chapter 9, but in general, the client will create an InitialContext using a set of Properties that specify the location and type of the JNDI service associated with the JMS provider: Properties props = ...; Context ctx = new InitialContext(props); Next, the JMS client needs to acquire a ConnectionFactory from the JMS provider using a JNDI lookup. The client would have to know what name the JMS provider used to publish the ConnectionFactory in JNDI space. Here, we look up a QueueConnectionFactory registered in JNDI under the name jms/someQFactory: QueueConnectionFactory qFactory = (QueueConnectionFactory)ctx.lookup("jms/someQFactory"); An administrator would have to set up this ConnectionFactory on the JMS provider and associate it with this JNDI name on the server. The client also uses JNDI to find Destinations published by the JMS provider. Here, we look up a Queue published under the JNDI name jms/someQ: Queue queue = (Queue)ctx.lookup("jms/someQ"); Once we have a ConnectionFactory and one or more Destinations to talk to, we need to create a Connection with the JMS provider. This Connection is the conduit through which messages will be physically sent and received. A Connection has to be started before messages can be received through it, but a Connection can always be used to send messages, regardless of whether it's started or stopped. Normally, a client won't start( ) a Connection until it's ready to receive and process messages. Here, we use our QueueConnectionFactory to create a QueueConnection and defer starting it until we create a MessageConsumer to receive messages: QueueConnection qConn = qFactory.createQueueConnection(...); 11.2.3.2. Client identifiersWhen a client makes a connection to a JMS provider, a client identifier is associated with the client. The client identifier is used to maintain state on the JMS provider on behalf of the client, and the state data can persist beyond the lifetime of a client connection. The server-side state can be retrieved for the client when it reconnects using the same client ID. The only client state information defined by the JMS specification is durable topic subscriptions (described in "Durable Subscriptions" later in this chapter), but a JMS provider may support its own state information on behalf of clients as well. Only one client is allowed to be associated with a client ID (and its state information) on the JMS provider, so only a single connection with a given client ID can be made to a JMS provider at any given time. The JMS client identifier can be set in two ways. A client can set a client ID on any Connections that it makes with the JMS provider, using the Connection.setClientID( ) method: qConn.setClientID("client-1"); Again, only a single connection with a given client ID is allowed at any given time. If a client with this same client ID (even this one) already has a connection with the client ID, then an InvalidClientIDException will be thrown when setClientID( ) is called. Alternatively, a ConnectionFactory can be configured on the JMS provider with a client ID that is applied to any Connections that are created through it. The ConnectionFactory interface doesn't provide a facility for the client to set the factory's client ID; this is a function that would have to be provided in the JMS provider's administrative interface. A ConnectionFactory with a preset client ID is, by definition, intended to be used by a single client. 11.2.3.3. Authenticated connectionsWhen a client creates a connection, it has the option to provide a username and password that will be authenticated by the JMS provider. This is done using overloaded versions of the createXXXConnection( ) methods on a ConnectionFactory. We can create an authenticated QueueConnection, for example, with a call like this: QueueConnection authQConn = qFactory.createQueueSession("JimFarley", "myJMSPassword"); If this is successful, the client will operate under the given principal name and be given the appropriate rights. JMS providers aren't required to support authentication of connections. If a JMS provider does support authenticated connections, the principals and access rights will be administered on the JMS server. 11.2.3.4. SessionsOnce a connection to the JMS provider is established, we need to create one or more Sessions to be used to send and receive messages. Again, Sessions are a single-threaded context for handling messages, so we need a separate Session for each concurrent thread that we plan to use for messaging. Sessions are created from Connections. Here, we create a QueueSession from our QueueConnection: QueueSession qSess = qConn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE); When creating either QueueSessions or TopicSessions, two arguments are used to create the Session. The first is a boolean flag indicating whether we want the Session to be transacted. (See "Transactional Messaging" later in this chapter for details on transactional sessions .) The second argument indicates how we want the Session to acknowledge received messages with the JMS provider. The three options for the acknowledge mode of a Session are specified using static final values on the Session class:
11.2.3.5. Sending messagesMessages are sent to Destinations using MessageProducers, which are created from Sessions. When they are created, producers are associated with a Destination, and any Messages sent using the producer are delivered to that Destination using the Connection from which the Session was generated. In a point-to-point context, the message producers are QueueSenders, generated from QueueSessions using the Queue the sender should point to: QueueSender qSender = qSess.createSender(queue); Once a producer has been created, the client needs to create and initialize Messages to be sent. Messages are also created from a Session. Here, we create a TextMessage from our QueueSession and set its text body to be some interesting text we want to send to the Queue: TextMessage tMsg = qSess.createTextMessage( ); tMsg.setText("The sky is blue."); To actually send the message, we simply invoke the appropriate method on our MessageProducer. Here, we call send( ) on our QueueSender: qSender.send(tMsg); Note that it's not necessary to ensure that the underlying Connection (from which we generated our Session) is started in order to send messages. Starting the Connection is required only to commence delivery of messages from the Destination to the client. 11.2.3.6. Receiving messagesReceiving messages involves creating a MessageConsumer that is associated with a particular Destination. This establishes a consumer with the JMS provider, and the provider is responsible for delivering any appropriate messages that arrive at the Destination to the new consumer. MessageConsumers are also generated from Sessions, in order to associate them with a serialized flow of messages. In a point-to-point context, a QueueReceiver is generated from a QueueSession using its createReceiver( ) methods. Here, we simply create a new receiver tied to our Queue. Other options for creating QueueReceivers are discussed in "Point-to-Point Messaging" later in this chapter. QueueReceiver qReceiver = qSess.createReceiver(queue); By creating a MessageConsumer, all we've done is tell the JMS provider that we want to receive messages from a particular Destination. We haven't specified what to do with the Messages on the client side. Since JMS is an asynchronous message delivery system, it uses the same listener pattern that is used in Swing GUI programming or JavaBeans event handling (two other asynchronous event contexts). Messages in JMS are processed using MessageListeners. A client needs to implement a MessageListener with an onMessage( ) method that does something useful with the Messages coming from the Destination. Example 11-1 shows a basic MessageListenera TextLogger that simply prints the contents of any TextMessages it encounters. Example 11-1. Simple MessageListener implementationimport javax.jms.*; public class TextLogger implements MessageListener { // Default constructor public TextLogger( ) {} // Message handler public void onMessage(Message msg) { // If it's a text message, print it to stdout if (msg instanceof TextMessage) { TextMessage tMsg = (TextMessage)msg; try { System.out.println("Received message: " + tMsg.getText( )); } catch (JMSException je) { System.out.println("Error retrieving message text: " + je.getMessage( )); } } // For other types of messages, print an error else { System.out.println("Unsupported message type encountered."); } } } Once a MessageListener has been defined, the client needs to create one and register it with a MessageConsumer. In our running example, we create one of our TextLoggers and associate it with our QueueReceiver using its setMessageListener( ) method: MessageListener listener = new TextLogger( ); qReceiver.setMessageListener(listener); It's important to remember that no messages will be delivered over our underlying Connection until it's been started. In our running example, we created our QueueConnection but never started it, so we do that now to start delivery of Messages to our QueueReceiver, and from there to our TextLogger listener: qConn.start( ); 11.2.3.7. Temporary destinationsA client can create its own temporary destinations, which are Destinations that are visible only to the Connections that created them, and that live only for the duration of the Connection used to create them. Although a temporary destination lives only for the life of the Connection it was created from, it is created using methods on the Session. For example, to create a TemporaryQueue: Queue tempQueue = qSession.createTemporaryQueue( ); Temporary destinations can be used, for example, to receive responses to messages that are sent with a JMSReplyTo header: TextMessage request = qSession.createTextMessage( ); request.setJMSReplyTo(tempQueue); They can also be used to exchanging asynchronous messages between threads in the same client. 11.2.3.8. Cleaning upConnections and Sessions require resources to be allocated by the JMS provider (similar to how JDBC connections use up resources on an RDBMS), so it's a good idea to free them up explicitly when you are done with them. Sessions are closed by simply calling close( ) on them: qSess.close( ); When a Session is closed, all MessageConsumers and MessageProducers associated with it are rendered unusable. If you try to use them to communicate with the JMS provider, they will throw an IllegalStateException. A call to Session.close( ) will block until any pending processing of incoming Messages (e.g., a MessageListener's onMessage( ) method) is complete. Closing a Session doesn't close the underlying Connection from which it came. You can close one Session and open up another one as long as the Connection is active. To close a Connection and free up its server-side resources, call its close( ) method: qConn.close( ); All Sessions (and, subsequently, all of their consumers and producers) generated from a Connection become unusable once it is closed. The call to Connection.close( ) will block until incoming Message processing has completed on all of the Sessions associated with it. |