光辉飞翔

导航

 

Java消息服务是一个在 Java标准化组织(JCP)内开发的标准(代号JSR 914)。

2001年6月25日,Java消息服务发布JMS 1.0.2b,2002年3月18日Java消息服务发布 1.1,统一了消息域。

期待已久的Java EE 7终于到来了,JMS 2.0也随之而来,这是JMS十多年来的第一次升级。【http://www.infoq.com/cn/news/2013/06/JMS_2.0_Released】

JMS 2.0是Java EE 7平台的一部分,但是也可以作为单独的Java SE平台使用,不过并非所有特性都支持这两种模式。

---------------------------JSR 343: JavaTM Message Service 2.0--------------------------

jms 2.0的明细规范内容:https://jcp.org/en/jsr/detail?id=343 

 

---------------------------------------------【原著如下】------------------------------------------------------

What's New in JMS 2.0, Part One: Ease of Use

by Nigel Deakin

Learn how new ease-of-use features enable you to write fewer lines of code.

Published May 2013

This article, which is the first article in a two-part series, assumes a basic familiarity with Java Message Service (JMS) 1.1 and introduces some of the new ease-of-use features in JMS 2.0. In Part Two, we will look at new messaging features.

JMS 2.0, which was released in April 2013, is the first update to the JMS specification since version 1.1 was released in 2002. One might think that an API that has remained unchanged for so long has grown moribund and unused. However, if you judge the success of an API standard by the number of different implementations, JMS is one of the most successful APIs around.

In JMS 2.0, the emphasis has been on catching up with the ease-of-use improvements that have been made to other enterprise Java technologies. While technologies such as Enterprise JavaBeans or Java persistence are now much simpler to use than they were a decade ago, JMS had remained unchanged with a successful, but rather verbose, API.

The single biggest change in JMS 2.0 is the introduction of a new API for sending and receiving messages that reduces the amount of code a developer must write. For applications that run in a Java EE application server, the new API also supports resource injection. This allows the application server to take care of the creation and management of JMS objects, simplifying the application even further.

JMS 2.0 is part of the Java EE 7 platform and can be used in Java EE Web or EJB applications, or it can be used standalone in a Java SE environment. As I explain below, some of the features described here are available only in a standalone environment while others are available only in Java EE Web or EJB applications.

Simplified API

The new API is known as the simplified API. As the name suggests, it is intended to be simpler and easier to use than the existing JMS 1.1 API, which is (rather predictably) now referred to as the classic API.

The simplified API consists of three new interfaces: JMSContextJMSProducer, and JMSConsumer:

  • JMSContext replaces the separate Connection and Session objects in the classic API with a single object.
  • JMSProducer is a lightweight replacement for the MessageProducer object in the classic API. It allows message delivery options, headers, and properties to be configured using method chaining (sometimes known as a builder pattern).
  • JMSConsumer replaces the MessageConsumer object in the classic API and is used in a similar way.

Developers now have a choice as to whether to use the classic API (the familiar ConnectionSessionMessageProducer, andMessageConsumer objects of JMS 1.1) or the simplified API (the JMSContextJMSProducer, and JMSConsumer objects introduced in JMS 2.0).

The simplified API offers all the features of the classic API plus some additional features. The classic API is not deprecated and will remain part of JMS indefinitely.

Using the Simplified API to Send a Message

The JMS 1.1 classic API has been in use for over a decade and has proven its usefulness. In what ways is the JMS 2.0 simplified API better? The JMS 2.0 simplified API requires less code.

Listing 1 shows a simple example that uses the classic API to send a single text message.

public void sendMessageJMS11(ConnectionFactory connectionFactory, Queue queueString text) {
   try {
      Connection connection = connectionFactory.createConnection();
      try {
         Session session =connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
         MessageProducer messageProducer = session.createProducer(queue);
         TextMessage textMessage = session.createTextMessage(text);
         messageProducer.send(textMessage);
      } finally {
         connection.close();
      }
   } catch (JMSException ex) {
      // handle exception (details omitted)
   }
}

Listing 1

Now compare Listing 1 to Listing 2, which shows how we might do exactly the same thing using the simplified API in JMS 2.0:

public void sendMessageJMS20(ConnectionFactory connectionFactory, Queue queue, 
String text) {
   try (JMSContext context = connectionFactory.createContext();){
      context.createProducer().send(queue, text);
   } catch (JMSRuntimeException ex) {
      // handle exception (details omitted)
   }
}

Listing 2

As you can see, the amount of code we have to write is significantly reduced. Let's look at this in more detail.

  • Instead of creating separate Connection and Session objects, we create a single JMSContext object.
  • The JMS 1.1 version used a finally block to call close on the Connection after use. In JMS 2.0, the JMSContext object also has a close method that needs to be called after use. However, there's no need to explicitly call close from your code.JMSContext implements the Java SE 7 java.lang.AutoCloseable interface. This means that if we create the JMSContext in atry-with-resources block (also a new feature of Java SE 7), the close method will be called automatically at the end of the block without the need to explicitly add it to your code. 

    In fact, all JMS interfaces that have a close method have been extended to implement the java.lang.AutoCloseable interface, so they can all be used in try-with-resources blocks. This includes the Connection and Session interfaces as well asJMSContext. So even if you're using the classic API, you can still benefit from this feature. Note that because of this change, JMS 2.0 can be used only with Java SE 7.

  • When the JMS 1.1 version created the Session object, it passed in the parameters (false and Session.AUTO_ACKNOWLEDGE) to specify that we want to create a non-transacted session in which any received messages will be acknowledged automatically. In JMS 2.0, this is the default (for Java SE applications), so we don't need to specify any parameters. 

    If we wanted to specify one of the other session modes (local transaction, CLIENT_ACKNOWLEDGE, or DUPS_OK_ACKNOWLEDGE), we would pass in just a single parameter rather than two.

  • There's no need to create a TextMessage object and set its body to be the specified string. Instead, we simply pass the string into the send method. The JMS provider will automatically create a TextMessage and set its body to the supplied string.
  • The JMS 1.1 example provided a catch block for the JMSException that almost all methods throw. The JMS 2.0 simplified API example has a similar block, but it catches a JMSRuntimeException instead. 

    One feature of the simplified API is that its methods do not declare checked exceptions. If an error condition is encountered, aJMSRuntimeException is thrown. This new exception is a subclass of RuntimeException, which means it does not need to be explicitly caught by the calling method or declared in its throws clause. This contrasts with the classic API, in which almost every method is declared to throw a JMSException that the calling method must either catch or throw itself.

Both Listing 1 and Listing 2 show the ConnectionFactory and Queue objects being passed in as parameters. The way that these are obtained hasn't changed, so we won't cover this here or in the other listings in this article. Typically, they would be obtained by a JNDI lookup from a JNDI repository.

Using the Simplified API to Receive Messages Synchronously

Listing 3 shows a simple example that uses JMS 1.1 to receive a single TextMessage synchronously and extract its text.

public String receiveMessageJMS11(ConnectionFactory connectionFactory,Queue queue){
   String body=null;
   try {
      Connection connection = connectionFactory.createConnection();
      try {
         Session session =connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
         MessageConsumer messageConsumer = session.createConsumer(queue);
         connection.start();
         TextMessage textMessage = TextMessage)messageConsumer.receive();
         body = textMessage.getText();
      } finally {
         connection.close();
      }
   } catch (JMSException ex) {
      // handle exception (details omitted)
   }
   return body;
}

Listing 3

Listing 4 shows how we might do exactly the same thing using the simplified API in JMS 2.0:

public String receiveMessageJMS20(
ConnectionFactory connectionFactory,Queue queue){
   String body=null;
   try (JMSContext context = connectionFactory.createContext();){
      JMSConsumer consumer = session.createConsumer(queue);
      body = consumer.receiveBody(String.class);
   } catch (JMSRuntimeException ex) {
      // handle exception (details omitted)
   }
   return body;
}

Listing 4

As with sending a message, the amount of code we have to write is reduced. Some of the reasons are the same as in the previous example:

  • Instead of creating separate Connection and Session objects, we create a single JMSContext object.
  • We can create the JMSContext in a try-with-resources block so that it will be closed automatically at the end of the block. This removes the need to call close.
  • We don't need to specify that we want received messages to be acknowledged automatically, because that is the default.

Also, there are two additional ways in which JMS 2.0 reduces the amount of code needed to receive a message:

  • Whereas in JMS 1.1 we need to call connection.start() to start delivery of messages to the consumer, in the JMS 2.0 simplified API we don't: the connection is automatically started. (You can disable this behavior if you need to.)
  • There's no need to receive a Message object, cast it to a TextMessage, and then call getText to extract the message body. Instead, we call receiveBody, which returns the message body directly.

Using the Simplified API to Receive Messages Asynchronously

The previous examples showed how to receive messages synchronously by calling a method that blocked until a message was received or a timeout occurred.

If you need to receive messages asynchronously in a Java SE application, in JMS 1.1 you need to create a MessageConsumer object and then use the method setMessageListener to specify an object that implements the MessageListener interface. You then need to call connection.start() to start delivery of messages:

MessageConsumer messageConsumer = session.createConsumer(queue);
messageConsumer.setMessageListener(messageListener);
connection.start();

 

The JMS 2.0 simplified API code is similar. You need to create a JMSConsumer object and then use the method setMessageListenerto specify the object that implements the MessageListener interface. Message delivery is started automatically; there's no need to callstart.

JMSConsumer consumer = context.createConsumer(queue);
consumer.setMessageListener(messageListener);

 

Note that if you need to receive messages asynchronously in a Java EE 7 Web or EJB application then, as with previous versions of Java EE, you need to use a message-driven bean rather the setMessageListener method.

Injecting a JMSContext into a Java EE Application

If you're writing a Java EE Web or EJB application, then using the JMS 2.0 simplified API is even easier than in Java SE. This is because you can now "inject" the JMSContext into your code and leave it to the application server to work out when to create it and when to close it.

The following code is a fragment from a Java EE 7 session bean or servlet that injects a JMSContext and uses it to send a message:

@Inject @JMSConnectionFactory(
"jms/connectionFactory") private JMSContext context;

@Resource(lookup = "jms/dataQueue") private Queue dataQueue;

public void sendMessageJavaEE7(String body) {
   context.send(dataQueue, body);
}

 

As you can see, there is no code to create the JMSContext and no code to close it. Instead, we simply declare a field of typeJMSContext and add the annotations @Inject and @JMSConnectionFactory.

The @Inject annotation tells the container to create the JMSContext when it is needed. The @JMSConnectionFactory annotation tells the container the JNDI name of the ConnectionFactory that it should use.

If the injected JMSContext is used in a JTA transaction (whether container-managed or bean-managed), the JMSContext is considered to have transaction scope. This means that after the JTA transaction is committed, the JMSContext will be automatically closed.

If the injected JMSContext is used when there is no JTA transaction, the JMSContext is considered to have request scope. This means that the JMSContext will be closed when the request comes to an end. The length of a request is defined in the Contexts and Dependency Injection (CDI) specification, and it typically relates to an HTTP request from a client or a message received by a message-driven bean.

An injected JMSContext has some powerful features in addition to being created and closed automatically by the application server. The most important is that if a servlet calls a session bean, or one session bean calls another, and both use an injected JMSContext, then as long as the two injected JMSContext objects are defined in the same way (for example, they have the sameJMSConnectionFactory annotation), they will actually correspond to the same JMSContext object. This reduces the number of JMS connections used by the application.

Other API Simplifications in JMS 2.0

JMS 2.0 also provides several other simplifications.

New Method to Extract the Body Directly from a Message

A JMS message consists of three parts:

  • The message headers
  • The message properties
  • The message body

The type of the body varies with the message type: the body of a TextMessage is a String. The body of a BytesMessage is a byte array, and so on.

In JMS 1.1, the message body is obtained using methods specific to the message type, such as the getText method on TextMessage. However, when a message is received by an application, the JMS API always provides the message as a javax.jms.Message object, which needs to be cast to the appropriate subtype before the body can be obtained. This applies both when the message has been returned from a call to receive and when the message has been delivered asynchronously by a call to the onMessage method of aMessageListener.

For example, if you used the receive method to receive a TextMessage, you'd need to cast the returned object from a Message to aTextMessage and then call the getText method:

Message message = consumer.receive(1000); // returns a TextMessage
String body = ((TextMessage) message).getText();

 

JMS 2.0 has added a new method that makes it slightly simpler to extract the body of a message. This is the getBody method onMessage, and it is available to users of both the classic and simplified APIs. This method takes the expected body type as a parameter and does not require you to perform a cast on either the message or the body:

Message message = consumer.receive(1000); // returns a TextMessage
String body = message.getBody(String.class);

 

Let's look at how getBody simplifies the code needed to obtain the body of other message types.

If the message is a BytesMessage, JMS 1.1 provides several ways to extract the byte array from a BytesMessage. The simplest is to call the readBytes method on BytesMessage. This copies the bytes to the specified byte array.

Here's an example of a MessageListener that receives a BytesMessage and obtains the body as a byte array:

void onMessage(Message message){ // delivers a BytesMessage
   int bodyLength = ((BytesMessage)message).getBodyLength();
   byte[] bytes = new byte[bodyLength];
   int bytesCopied = ((BytesMessage)message).readBytes(bytes);
   ...

 

In JMS 2.0, the getBody method makes this much simpler:

void onMessage(Message message){ // delivers a BytesMessage
   byte[] bytes = message.getBody(byte[].class);
   ...

 

If the message is an ObjectMessage, in JMS 1.1 you need to call the getObject method on ObjectMessage and then cast the returned Serializable to the expected body type:

void onMessage(Message message){ // delivers an ObjectMessage
   MyObject body = (MyObject)((ObjectMessage) message).getObject();
   ...

 

Note the need to perform two casts. You need to cast the message from Message to ObjectMessage so you can call getObject. This returns the body as a Serializable, which you then need to cast to the actual type.

In JMS 2.0, you can do this without any casts:

void onMessage(Message message){ // delivers an ObjectMessage
   MyObject body = message.getBody(MyObject.class);
   ...

 

Finally, if the message is a MapMessage, the getBody method allows you to return the body as a Map:

Message message = consumer.receive(1000); // returns a MapMessage
Map body = message.getBody(Map.class);

 

The one message type for which getBody cannot be used is StreamMessage. This is because the stream typically consists of multiple objects that the application should read individually.

When using the getBody method, if the specified class does not match the body type, a MessageFormatException is thrown. A companion method, isBodyAssignableTo, has also been added to Message, and it can be used to test whether a subsequent call togetBody would be able to return the body of a particular Message object as a particular type. This is useful if more than one type of message is expected.

Methods to Receive a Message Body Directly

In JMS 1.1, an application consuming messages synchronously uses the receive()receive(timeout), or receiveNoWait()methods on MessageConsumer.

In JMS 2.0, applications using the simplified API can do this using the same methods on JMSConsumer.

These methods return a Message object from which the message body can be obtained. The getBody method described previously provides an easy way to obtain the body from this object.

Applications using the JMS 2.0 simplified API have an additional option. JMSConsumer provides three methods—receiveBody(class)receiveBody(class, timeout), and receiveBodyNoWait(class)—that will receive the next message synchronously and return its body. As with getBody, the expected class is passed in as a parameter.

So instead of using the code in Listing 5 or Listing 6, the application can use the single line shown in Listing 7.

JMSConsumer consumer = ...
Message message = consumer.receive(1000); // returns a TextMessage
String body = ((TextMessage) message).getText();

Listing 5

JMSConsumer consumer = ...
Message message = consumer.receive(1000); // returns a TextMessage
String body = message.getBody(String.class);

Listing 6

JMSConsumer consumer = ...
String body = consumer.receiveBody(String.class,1000);

Listing 7

The receiveBody methods can be used to receive any type of message except for StreamMessage (for the same reason that this message type does not support getBody) and Message (since it has no body), as long as the class of the expected body is known in advance.

These new methods have been added only to JMSConsumer. They have not been added to MessageConsumer. This means that this feature is available only to applications using the JMS 2.0 simplified API.

Furthermore, these methods do not provide access to the message headers or properties (such as the JMSRedelivered message header field or the JMSXDeliveryCount message property), so they should be used only if the application has no need to access them.

New Methods to Create a Session

In JMS 1.1, the following method on a javax.jms.Connection was used to create a javax.jms.Session, where the transacted parameter needed to be set to true or false and the acknowledgeMode parameter needed to be set toSession.AUTO_ACKNOWLEDGESession.CLIENT_ACKNOWLEDGE, or Session.DUPS_OK_ACKNOWLEDGE.

Session createSession(
  boolean transacted, int acknowledgeMode) throws JMSException

 

This has always been a rather confusing method for two main reasons:

  • It used two arguments to define a single aspect of the session.
  • In a Java EE transaction, both arguments were ignored anyway.

Let's consider these two problems in turn.

Two Arguments to Define the Same Thing

The first problem with the createSession method in JMS 1.1 is that it used two arguments to define what was, in practice, a single aspect of the session with four possibilities:

  • If the transacted parameter was set to false, the session was non-transacted and the acknowledgeMode parameter was used to specify which of three kinds of acknowledgment should be used when receiving messages.
  • If the transacted parameter was set to true, the acknowledgeMode parameter was ignored.

In addition to being unnecessarily complicated, this led to code that was potentially misleading, because if the transacted parameter was set to false, the user still had to set the acknowledgeMode parameter to some value even if it is ignored. For example, the following code was perfectly valid:

amb Session session = 
  connection.createSession(true,Session.AUTO_ACKNOWLEDGE);iguous

 

In a Java EE Transaction, Both Arguments Are Ignored Anyway

The second problem with the createSession method in JMS 1.1 is that in a Java EE Web or EJB application, if there is a current JTA transaction (as there is by default), both parameters to createSession are ignored anyway. However, since the API forced developers to specify two parameters, which led to highly misleading code, a user writing an EJB bean might write the following code in not realizing that the session would actually use the EJB's container-managed JTA transaction.

Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);

 

To address these problems, two new methods with the name createSession have been added to javax.jms.Connection. One has a single parameter and one has no parameters at all. We'll discuss these in turn.

JMS 2.0: createSession with One Parameter

In JMS 2.0, a second createSession method has been added to javax.jms.Connection. This has a single parameter,sessionMode:

Session createSession(int sessionMode) throws JMSException

 

In a normal Java SE environment, sessionMode can be set to Session.AUTO_ACKNOWLEDGESession.CLIENT_ACKNOWLEDGE,Session.DUPS_OK_ACKNOWLEDGE, or Session.TRANSACTED. In a Java EE transaction, sessionMode is ignored.

JMS 2.0: createSession with No Parameters

In a Java EE transaction, even passing a single parameter to createSession is misleading because the parameter is ignored if there is a Java EE transaction. To allow users to write code that is less misleading, a third createSession method has been added tojavax.jms.Connection that has no parameters:

Session createSession() throws JMSException

 

This method is particularly intended for use in a Java EE transaction, where there is no point in specifying a session mode because it is ignored. However, this method can also be used in a normal Java SE environment, in which case it is equivalent to callingcreateSession(Session.AUTO_ACKNOWLEDGE).

JMS 2.0: createSession with Two Parameters

The existing method createSession(boolean transacted,int acknowledgeMode) can still be used and will remain part of the API indefinitely. However, developers are encouraged to use the single- or no-parameter versions of this method instead.

Easier Resource Configuration in JMS 2.0

JMS 2.0 enables easier resource configuration in several ways.

Default Connection Factory in Java EE

Java EE 7 introduces a platform default JMS connection factory. This is a built-in connection factory that connects to the application server's built-in JMS provider.

Applications can obtain this connection factory by performing a JNDI lookup using the namejava:comp:DefaultJMSConnectionFactory without the need to previously create the connection factory using administrative tools:

@Resource(lookup="java:comp/DefaultJMSConnectionFactory") ConnectionFactory cf

 

This connection factory is intended for use by the many applications that use the built-in JMS provider and don't need to add any application-specific configuration.

When injecting a JMSContext into an application, the JMSConnectionFactory annotation is used to specify the connection factory to be used:

@Inject @JMSConnectionFactory("
  jms/connectionFactory") JMSContext context1;

If this annotation is omitted, the default connection factory will be used:

@Inject JMSContext context2; // uses the platform default connection factory

 

JMS Resource Definition Annotations in Java EE

Every JMS application starts with a connection factory (an object that implements javax.jms.ConnectionFactory) and at least one destination (an object that implements either javax.jms.Queue or javax.jms.Topic). The ConnectionFactory is the object that in JMS is used to create the connection to the JMS provider, and the Queue or Topic is the object that identifies the physical queue or topic that messages are being sent to or received from.

The way in which these objects are created, and the way in which they are configured, varies from one JMS provider to another. That's why JMS recommends that you use a separate, provider-specific tool to create, configure, and bind into a JNDI store the connection factories and destinations that your application needs. Your application can then use JNDI to look up these objects without needing to use any nonstandard code. In addition to keeping your application code portable, this also means you can write your code without needing to know details about how it will be deployed.

When configuring a ConnectionFactory, it is often necessary to know things such as the host name and port of the JMS server. When configuring a Queue or Topic object, it is usually necessary to know the physical name of the queue or topic. Creating yourConnectionFactoryQueue, and Topic objects separately from the application and storing them in JNDI allows these details to be defined by the deployer or administrator, not the developer.

Although this separation of code from configuration is essential in many enterprise environments, it can be an unwanted burden in simpler environments. In addition, if the application is being deployed into an automated platform as a service (PaaS) system, it might be desirable to automate the provisioning of the ConnectionFactoryQueue, and Topic objects required by the application.

In many Java EE applications, the default JMS connection factory that is now available in any Java EE 7 application server (which was described in the previous section) removes the need for any connection factories to be configured at all. However, for those cases when a specially configured connection factory is needed—and for queues and topics—Java EE 7 offers another new feature that allows these objects to be created using either annotations in the code, XML elements in the deployment descriptor, or a combination of both.

The main new annotations are javax.jms.JMSConnectionFactoryDefinition and javax.jms.JMSDestinationDefinition. These can be defined in any Java EE component class such as an EJB bean or servlet, as shown in Listing 8:

@JMSConnectionFactoryDefinition(
    name="java:global/jms/MyConnectionFactory",
    maxPoolSize = 30,
    minPoolSize= 20,
    properties = {
        "addressList=mq://localhost:7676",
        "reconnectEnabled=true"
    }
) 
@JMSDestinationDefinition(
    name = "java:global/jms/DemoQueue",
    interfaceName = "javax.jms.Queue",
    destinationName = "demoQueue"
  )
public class NewServlet extends HttpServlet {
  ...

Listing 8

If more than one connection factory or destination needs to be defined, these annotations need to be enclosed within aJMSConnectionFactoryDefinitions or JMSDestinationDefinitions annotation, as shown in Listing 9:

@JMSConnectionFactoryDefinitions({
    @JMSConnectionFactoryDefinition(
       name="java:global/jms/MyConnectionFactory1",
       maxPoolSize = 30,
       minPoolSize= 20,       
       properties = {
          "addressList=mq://localhost:7676",
          "reconnectEnabled=true"
       }
    ),
    @JMSConnectionFactoryDefinition(
       name="java:global/jms/MyConnectionFactory2",
       maxPoolSize = 30,
       minPoolSize= 20,
       properties = {
          "addressList=mq://localhost:7677",
          "reconnectEnabled=true"
       }
    ) 
})
@JMSDestinationDefinitions({
    @JMSDestinationDefinition(
       name="java:global/jms/DemoQueue1",
       interfaceName = "javax.jms.Queue",
       destinationName = "demoQueue1"
    ),
    @JMSDestinationDefinition(
       name="java:global/jms/DemoQueue2",
       interfaceName = "javax.jms.Queue",
       destinationName = "demoQueue2"
    ) 
})
public class NewServlet extends HttpServlet {
  ...

Listing 9

The JMSConnectionFactoryDefinition annotation defines a number of standard attributes that can be specified including name(which is the JNDI name), clientIduserpasswordmaxPoolSize, and minPoolSize. In addition, the properties attribute can be used to specify additional nonstandard properties that the application server might support. In Listing 8 and Listing 9, addressList andreconnectEnabled are examples of such nonstandard properties.

The JMSConnectionFactoryDefinition annotation defines a smaller number of standard attributes that can be specified includingname (which is the JNDI name) and destinationName (which is the provider-specific queue or topic name), as well as allowing the properties attribute to be used to specify additional nonstandard properties.

Connection factories and destinations defined in this way must be in the java:compjava:modulejava:app, or java:globalnamespaces, and they typically exist for as long as the application that defines them is deployed.

It is also possible to specify these definitions in the deployment descriptor file (for example, web.xml or ejb-jar.xml), as shown in Listing 10:

<jms-connection-factory>
   <name>java:global/jms/MyConnectionFactory</name>
   <max-pool-size>30</max-pool-size>
   <min-pool-size>20</min-pool-size>
   <property>
      <name>addressList</name>
      <value>mq://localhost:7676</value>
   </property>
   <property>
      <name>reconnectEnabled</name>
      <value>true</value>
   </property>    
</jms-connection-factory>

<jms-destination>
   <name>java:global/jms/DemoQueue</name>
   <interfaceName>javax.jms.Queue</interfaceName>
   <destinationName>demoQueue</destinationName> 
</jms-destination>

Listing 10

If desired, the developer can specify some of the required attributes in the annotation, with the deployer specifying the remaining attributes in the deployment descriptor. This can be useful in the case of attributes whose values are not known until deployment time.

In all the examples above, the application server is responsible for "provisioning" the JNDI resources that are defined in the annotations or deployment descriptor. However, it remains the responsibility of the deployer to ensure that the JMS server to which the connection factory refers is installed and available, and that the physical queues and topics themselves have been created.

Conclusion

In this article, we covered the ease-of-use features added to JMS 2.0, which enable developers to write significantly fewer lines of code. In Part Two, we will look at the new messaging features in JMS 2.0.

See Also


About the Author

Nigel Deakin, a Principal Member of Technical Staff at Oracle, was Specification Lead for JSR 343, Java Message Service 2.0. In addition to his responsibilities for leading the next versions of the JMS specification, he is a member of Oracle's JMS development team, working on Open Message Queue and the GlassFish application server. He has spoken recently at JavaOne in San Francisco, US, and at Devoxx in Antwerp, Belgium, and he is based in Cambridge, UK.

Join the Conversation

Join the Java community conversation on FacebookTwitter, and the Oracle Java Blog!

 

 

=================================================

What's New in JMS 2.0, Part Two—New Messaging Features

by Nigel Deakin

Learn how to take advantage of new messaging features in JMS 2.0.

Published June 2013

This article, which is the second in a two-part series, introduces some of the new messaging features introduced in Java Message Service (JMS) 2.0. It assumes a basic familiarity with JMS 1.1.

In Part One, we looked at the new ease-of-use features introduced in JMS 2.0. Here, we look at important new messaging features.

JMS 2.0, which was released in April 2013, is the first update to the JMS specification since version 1.1 was released in 2002. One might think that an API that has remained unchanged for so long has grown moribund and unused. However, if you judge the success of an API standard by the number of different implementations, JMS is one of the most successful APIs around.

In JMS 2.0, the emphasis has been on catching up with ease-of-use improvements that have been made to other enterprise Java technologies in recent years. The opportunity has been taken to introduce a number of new messaging features as well.

JMS 2.0 is part of the Java EE 7 platform and can be used in Java EE Web or EJB applications. It can also be used standalone in a Java SE environment. As I explain below, some of the features are available only in a standalone environment while others are available only in Java EE Web or EJB applications.

Here we discuss five important new messaging features in JMS 2.0.

Multiple Consumers Allowed on the Same Topic Subscription

In JMS 1.1, a subscription on a topic was not permitted to have more than one consumer at a time. This meant that the work of processing messages on a topic subscription could not be shared among multiple threads, connections, or Java Virtual Machines (JVMs), thereby limiting the scalability of the application. This restriction has been removed in JMS 2.0 by the introduction of a new kind of topic subscription called a shared subscription.

Let's review how topic subscriptions worked in JMS 1.1. In Listing 1, the createConsumer method on Session is used to create anondurable subscription on the specified topic (we'll discuss durable subscriptions in just a moment):

private void createUnsharedConsumer(ConnectionFactory connectionFactory, Topic topic) 
      throws JMSException {
   Connection connection = connectionFactory.createConnection();
   Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
   MessageConsumer messageConsumer = session.createConsumer(topic);
   connection.start();
   Message message = messageConsumer.receive(10000);
   while (message != null) {
      System.out.println("Message received: " + ((TextMessage) message).getText());
      message = messageConsumer.receive(10000);
   }
   connection.close();
}

Listing 1

In Listing 1, the consumer will receive a copy of every message sent to the topic. However, what if the application takes a long time to process each message? How do we make the application more scalable by sharing the work of processing these messages between, say, two JVMs, with one JVM processing some of the messages and the other JVM processing the remaining messages?

In JMS 1.1, there's no way to do this in a normal Java SE application. (In Java EE, you could do it using a pool of message-driven beans [MDBs]). If you use createConsumer to create a second consumer in a separate JVM (or a separate thread on the same JVM), each consumer will use a separate subscription, and so it will receive a copy of every message received by the topic. That's not what we want. If you think of a "subscription" as a logical entity that receives a copy of every message sent to the topic, then we want the two consumers to use the same subscription.

JMS 2.0 provides a solution. You can create a "shared" nondurable subscription using a new method: createSharedConsumer. This method is available both on Session (for applications using the classic API) and on JMSContext (for applications using the simplified API). Since the two JVMs need to be able to identify the subscription that they need to share, they need to supply a name to identify the shared subscription, as shown in Listing 2.

private void createSharedConsumer(ConnectionFactory connectionFactory, Topic topic) throws JMSException {
   Connection connection = connectionFactory.createConnection();
   Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
   MessageConsumer messageConsumer = session.createSharedConsumer(topic,"mySubscription");
   connection.start();
   Message message = messageConsumer.receive(10000);
   while (message != null) {
      System.out.println("Message received: " + ((TextMessage) message).getText());
      message = messageConsumer.receive(10000);
   }
   connection.close();
}

Listing 2

If you run the code in Listing 2 in two separate JVMs, each message sent to the topic will be delivered to one or the other of the two consumers. This allows them to share the work of processing messages from the subscription.

The same feature is available to applications that use a durable subscription. In JMS 1.1, a durable subscription was created using the method createDurableSubscriber on Session:

   MessageConsumer messageConsumer = session.createDurableSubscription(topic,"myDurableSub");

 

This creates a durable subscription called myDurableSub on the specified topic. However, as before, there is no way to share the work of processing the messages on this durable subscription between two JVMs or between two threads on the same JVM. Depending on exactly what you try to do, you'll either get a JMSException or two different subscriptions.

Once again, JMS 2.0 provides a solution to this problem. You can now create a "shared" durable subscription using the new methodcreateSharedDurableConsumer. This method is available both on Session (for applications using the classic API) and onJMSContext (for applications using the simplified API).

   MessageConsumer messageConsumer = session.createSharedDurableConsumer(topic,"myDurableSub");

 

In summary, then, whereas JMS 1.1 defined two different types of topic subscription, JMS 2.0 defines four types, all of which can be created using either the classic or simplified APIs:

  • Unshared nondurable subscriptions. These are available in both JMS 1.1 and JMS 2.0 and are created using createConsumer. They can have only a single consumer. Setting the client identifier is optional.
  • Unshared durable subscriptions. These are available in both JMS 1.1 and JMS 2.0 and are created usingcreateDurableSubscriber or (in JMS 2.0 only) createDurableConsumer. They can have only a single consumer. Setting the client identifier is compulsory, and the subscription is identified by the combination of the subscription name and client identifier.
  • Shared nondurable subscriptions. These are available in JMS 2.0 only and are created using createSharedConsumer. They can have any number of consumers. Setting the client identifier is optional. The subscription is identified by the combination of the subscription name and the client identifier, if it is set.
  • Shared durable subscriptions. These are available in JMS 2.0 only and are created using createSharedDurableConsumer. They can have any number of consumers. Setting the client identifier is optional. The subscription is identified by the combination of the subscription name and the client identifier, if it is set.

Delivery Delay

You can now specify a delivery delay on a message. The JMS provider will not deliver the message until after the specified delivery delay has elapsed.

If you're using the classic API, you need to set the delivery delay (in milliseconds) by calling setDeliveryDelay on theMessageProducer prior to sending the message, as shown in Listing 3.

private void sendWithDeliveryDelayClassic(ConnectionFactory connectionFactory,Queue queue) 
   throws JMSException {

   // send a message with a delivery delay of 20 seconds
   try (Connection connection = connectionFactory.createConnection();){
      Session session = con.createSession();
      MessageProducer messageProducer = session.createProducer(queue);
      messageProducer.setDeliveryDelay(20000);
      TextMessage textMessage = session.createTextMessage("Hello world");
      messageProducer.send(textMessage);
   }
}

Listing 3

If you're using the simplified API, you need to call setDeliveryDelay on the JMSProducer prior to sending the message. This method returns the JMSProducer object, which allows you to create the JMSProducer, set the delivery delay, and send the message all on the same line, as shown in Listing 4.

private void sendWithDeliveryDelaySimplified(ConnectionFactory connectionFactory,Queue queue)
   throws JMSException {

   // send a message with a delivery delay of 20 seconds
   try (JMSContext context = connectionFactory.createContext();){
      context.createProducer().setDeliveryDelay(20000).send(queue,"Hello world");
   }
}

Listing 4

Sending Messages Asynchronously

Another new feature of JMS 2.0 is the ability to send a message asynchronously.

This feature is available for applications running in Java SE or the Java EE application client container. It is not available to applications that run in the Java EE Web or EJB container.

Normally, when a persistent message is sent, the send method does not return until the JMS client has sent the message to the server and received a reply to notify the client that the message has been safely received and persisted. We call this a synchronous send.

JMS 2.0 introduces the ability to perform an asynchronous send. When a message is sent asynchronously, the send method sends the message to the server and then returns control to the application without waiting for a reply from the server. Instead of being blocked unproductively while the JMS client waits for a reply, the application can do something useful, such as sending a further message or performing some processing.

When a reply is received back from the server to indicate that the message has been received by the server and persisted, the JMS provider notifies the application by invoking the callback method onCompletion on an application-specified CompletionListenerobject.

There are two main ways in which you might use an asynchronous send in an application

  • To allow the application to do something else (such as update the display or write to a database) during the interval when it would otherwise be waiting for a reply from the server
  • To allow a large number of messages to be sent in succession without waiting for a reply from the server after each message

Listing 5 is an example of how you might implement the first of these using the classic API:

private void asyncSendClassic(ConnectionFactory connectionFactory,Queue queue)
   throws Exception {

   // send a message asynchronously
   try (Connection connection = connectionFactory.createConnection();){
      Session session = connection.createSession();
      MessageProducer messageProducer = session.createProducer(queue);
      TextMessage textMessage = session.createTextMessage("Hello world");
      CountDownLatch latch = new CountDownLatch(1);
      MyCompletionListener myCompletionListener = new MyCompletionListener(latch);
      messageProducer.send(textMessage,new MyCompletionListener(latch));
      System.out.println("Message sent, now waiting for reply");

      // at this point we can do something else before waiting for the reply
      // this is not shown here

      // now wait for the reply from the server	
      latch.await();

      if (myCompletionListener.getException()==null){
         System.out.println("Reply received from server");
      } else {
         throw myCompletionListener.getException();
      }
   }
}

Listing 5

The class MyCompletionListener used in Listing 5 is a separate class provided by the application, which implements thejavax.jms.CompletionListener interface, as shown in Listing 6:

class MyCompletionListener implements CompletionListener {

   CountDownLatch latch;
   Exception exception;
   
   public MyCompletionListener(CountDownLatch latch) {
      this.latch=latch;
   }

   @Override
   public void onCompletion(Message message) {
      latch.countDown();
   }

   @Override
   public void onException(Message message, Exception exception) {
      latch.countDown();
      this.exception=exception;
   }

   public Exception getException(){
      return exception;
   }
}

Listing 6

In Listing 6, we use a new method on MessageProducer to send a message without waiting for a reply from the server. This issend(Message message, CompletionListener listener). Using this method to send a message allows the application to do something else while the message is processed in the server. When the application is ready to continue, it uses ajava.util.concurrent.CountDownLatch to wait until the reply has been received from the server. When the reply is received, the application can then proceed with the same degree of confidence that the message has been successfully sent that it would have had after a normal synchronous send.

Sending a message asynchronously is slightly simpler if you are using the JMS 2.0 simplified API, as shown in Listing 7:

private void asyncSendSimplified(ConnectionFactory connectionFactory,Queue queue) 
   throws Exception {

   // send a message asynchronously
   try (JMSContext context = connectionFactory.createContext();){
      CountDownLatch latch = new CountDownLatch(1);
      MyCompletionListener myCompletionListener = new MyCompletionListener(latch);
      context.createProducer().setAsync(myCompletionListener).send(queue,"Hello world");
      System.out.println("Message sent, now waiting for reply");

      // at this point we can do something else before waiting for the reply
      // this is not shown here

      latch.await();
      if (myCompletionListener.getException()==null){
         System.out.println("Reply received from server");
      } else {
         throw myCompletionListener.getException();
      }
   }
 }

Listing 7

In this case, the method setAsync(CompletionListener listener) is called on the JMSProducer prior to calling send(Message message). And since the JMSProducer supports method chaining, you can do this both on the same line.

JMSXDeliveryCount

JMS 2.0 allows applications that receive a message to determine how many times the message has been redelivered. This information can be obtained from the message property JMSXDeliveryCount:

int deliveryCount = message.getIntProperty("JMSXDeliveryCount");

 

JMSXDeliveryCount is not a new property; it was defined in JMS 1.1. However, in JMS 1.1, it was optional for a JMS provider to actually set it, which meant application code that used it was not portable. In JMS 2.0, it becomes mandatory for JMS providers to set this property, allowing portable applications to make use of it.

So why might an application want to know how many times a message has been redelivered?

If a message is being redelivered, this means that a previous attempt to deliver the message failed for some reason. If a message is being redelivered repeatedly, the cause is likely to be a problem in the receiving application. Perhaps the application is able to receive the message but is unable to process it, and so it throws an exception or rolls back the transaction. If there is a prolonged reason why the message cannot be processed, such as the message being "bad" in some way, the same message will be redelivered over and over again, wasting resources and preventing subsequent "good" messages from being processed.

The JMSXDeliveryCount property allows a consuming application to detect that a message has been redelivered multiple times and is, therefore, "bad" in some way. The application can use this information to take some special action (instead of simply triggering yet another redelivery), such as consuming the message and sending it to a separate queue of "bad" messages for administrator action.

Some JMS providers already offer nonstandard facilities to detect messages that have been redelivered repeatedly and divert them to a dead-message queue. While JMS 2.0 defines how such messages should be handled, the JMSXDeliveryCount property allows applications to implement their own "bad" message handling code in a portable way.

Listing 8 shows a MessageListener that throws a RuntimeException to simulate an error in processing a "bad" message. TheMessageListener uses the JMSXDeliveryCount property to detect that a message has been redelivered ten times and take a different action.

class MyMessageListener implements MessageListener {

   @Override
   public void onMessage(Message message) {
      try {
         int deliveryCount = message.getIntProperty("JMSXDeliveryCount");
    if (deliveryCount<10){
       // now throw a RuntimeException 
            // to simulate a problem processing the message
       // the message will then be redelivered
       throw new RuntimeException("Exception thrown to simulate a bad message");
         } else {
       // message has been redelivered ten times, 
       // let's do something to prevent endless redeliveries
       // such as sending it to dead message queue
       // details omitted
    }
      } catch (JMSException e) {
         throw new RuntimeException(e);
      }
   }
}

Listing 8

MDB Configuration Properties

A Java EE application that needs to receive messages asynchronously does so using an MDB, which is configured by specifying a number of configuration properties.

Previous versions of Java EE were distinctly vague about how an MDB was configured. In EJB 3.1, the only configuration properties defined were:

  • acknowledgeMode (used only when transactions are bean-managed; can be set to either Auto-acknowledge or Dups-ok-acknowledge)
  • messageSelector
  • destinationType (can be set to either javax.jms.Queue or javax.jms.Topic)
  • subscriptionDurability (used only for topics; can be set to either Durable or NonDurable)

However, EJB 3.1 didn't define how the application should specify which queue or topic the MDB received messages from. It was left to the application server or resource adapter to define a nonstandard way to do this.

EJB 3.1 also didn't define—when messages were received from a topic and the subscriptionDurability property was set toDurable—how the subscription name and client identifier should be specified. And there was no standard way in EJB 3.1 to specify the connection factory that was used by the MDB to create its connection to the JMS server.

These rather surprising limitations are all addressed in the latest version of Java EE. EJB 3.2 (part of Java EE 7) defines the following additional configuration properties:

  • destinationLookup: The JNDI name of an administratively defined Queue or Topic object that represents the queue or topic from which the MDB will receive messages
  • connectionFactoryLookup: The JNDI name of an administratively defined ConnectionFactory object that the MDB will use to connect to the JMS provider
  • clientId: A client identifier used when the MDB connects to the JMS provider
  • subscriptionName: A durable subscription name used when subscriptionDurability is set to Durable

Most application servers supported clientId and subscriptionName anyway, so defining these as standard is simply formalizing existing practice.

Of course, it was always possible to configure the queue or topic used by a JMS MDB, and many (but not all) application servers provided a way to specify the connection factory. However, the way this was done was nonstandard and varied from one application server to another. Application servers remain free to continue to support these nonstandard mechanisms. However, you can be confident that applications that use destinationLookup and connectionFactoryLookup will work with multiple application servers.

Listing 9 shows a JMS MDB that consumes messages from a durable subscription on a topic and uses the new standard properties:

@MessageDriven(activationConfig = { 
   @ActivationConfigProperty(
      propertyName = "connectionFactoryLookup", propertyValue = "jms/MyConnectionFactory"),
   @ActivationConfigProperty(
      propertyName = "destinationLookup", propertyValue = "jmq/PriceFeed"), 
   @ActivationConfigProperty(
      propertyName = "destinationType ", propertyValue = "javax.jms.Topic "),
   @ActivationConfigProperty(
      propertyName = "subscriptionDurability ", propertyValue = "Durable"), 
   @ActivationConfigProperty(
      propertyName = "subscriptionName", propertyValue = "MySub"), 
   @ActivationConfigProperty(
      propertyName = "clientId", propertyValue = "MyClientId") }) 
   
public class MyMDB implements MessageListener {
   public void onMessage(Message message) {
      ...

Listing 9

Conclusion

All of the five features described above lead to easier messaging for Java developers. Taken together with the ease-of-use features discussed in Part One, they represent a major step forward for JMS 2.0—which should continue to flourish as one of the most successful APIs in the Java landscape.

See Also

About the Author

Nigel Deakin, a Principal Member of Technical Staff at Oracle, was Specification Lead for JSR 343, Java Message Service 2.0. In addition to his responsibilities for leading the next versions of the JMS specification, he is a member of Oracle's JMS development team, working on Open Message Queue and the GlassFish application server. He has spoken recently at JavaOne in San Francisco, US, and at Devoxx in Antwerp, Belgium, and he is based in Cambridge, UK.

Join the Conversation

Join the Java community conversation on FacebookTwitter, and the Oracle Java Blog!

posted on 2018-01-15 20:17  光辉飞翔  阅读(554)  评论(0编辑  收藏  举报