It has always been possible in JBoss to send JMS messages from EJB session and entity beans, either through JBossMQ or another JMS provider. This is done through normal JMS programming. But programming JMS like this is in some ways outside the EJB contract, since the JMS stuff never gets handled as a container-managed resource. This means, for example, that your JMS code is never enlisted in any transaction management and that you cannot pool your JMS sessions and therefore might run out of them.
Starting with EJB v2.0 and J2EE v1.3, there is, however, the requirement that the access to JMS connections and sessions be done as true J2EE resources, much like a DataSource. This means that a J2EE server that is compliant with the 1.3 specification must be able to handle JMS as a managed resource. JBoss version 2.4.0 is compliant in this respect, but not previous versions of JBoss.
[2.4.0. What does this mean to you? It means that from JBoss version 2.4.0 on, you have to define your JMS resources in the deployment descriptor, look them up as you do with other resources and use them as resources too. The container will then pool your JMS resources for you and it will enlist them with any ongoing or to be started transactions, much like using a DataSource.
The new resource management of JMS is based on a JMS resource adapter compatible with the J2EE Connector architecture (the first of its kind ;-)). The resource adapter is included in the distribution, in file deploy/lib/jms-ra.rar, but generally you don't have to worry about it because it is automatically deployed and configured in jboss.jcml for distributed transactions.
The management of JMS as a resource is available to all EJB types, session, entity and message-driven, but not (as far as I know) from the web container. The reason behind this is that the web container does not know how to handle transactions.
To use JMS as a resource, two things need to be done. One has two write resource-compatible JMS code and the resource must be added to the deployment descriptors. Let's work through all this with a simple example.]
The example is based on a simple pattern. A session bean receives invocations containing messages. We might think that processing these messages requires too much time. Therefore, we want to handle them asynchronously: instead of letting the session bean process them, it simply passes them on to a JMS destination and it may therefore return immediately. Another reason could be that we want multiple systems to be able to act on the messages, so they get sent to a topic open up for many subscribers.
As in every case when handling resources, we need a name to look the resource up; after creation, the container will bind the resource to that name. For JMS (as opposed to a DataSource) we need access to two resources, a connection factory and a destination, so we will need two names:
Figure 6.77. [2.4.0] Name giving for looking up JMS resources
/** * Name used to lookup TopicConnectionFactory */ private static final String CONNECTION_JNDI = "java:comp/env/jms/MyTopicConnection"; /** * Name used to lookup topic destination */ private static final String TOPIC_JNDI = "java:comp/env/jms/TopicName";
The lookup of these resources will be done during the creation of the bean, much like what happens with a DataSource. What's different in JMS from the handling of a DataSource is that the JMS connection (not the factory) is what is similar to the DataSource and the JMS session is like a JDBC connection. Therefore, we create the JMS connection also when the bean is created and we hold on to it as long as the bean is alive:
Figure 6.78. [2.4.0] Looking up JMS resources (connection factory and destination)
public void ejbCreate() { try { Context context = new InitialContext(); // Lookup the topic topic = (Topic)context.lookup(TOPIC_JNDI); // Lookup the connection factory TopicConnectionFactory factory = (TopicConnectionFactory)context.lookup(CONNECTION_JNDI); topicConnection = factory.createTopicConnection(); // Keep both around } catch (Exception ex) { // JMSException or NamingException could be thrown ex.printStackTrace(); throw new EJBException(ex.toString()); } }
In our example bean we have a method, hello(), that takes a text message in the form of a String object. This is the method the remote clients invoke. Every time the bean receives a message, it packs it into a JMS message and sends it along on its way to the destination. This looks very much like normal JMS code indeed:
Figure 6.79. [2.4.0] Publishing messages using JMS resources
TopicPublisher topicPublisher = null; TextMessage message = null; // Create a session topicSession = topicConnection.createTopicSession(true, Session.AUTO_ACKNOWLEDGE); // Create a publisher topicPublisher = topicSession.createPublisher(topic); // Create a message message = topicSession.createTextMessage(); message.setText(msg); // Publish it System.out.println("Publishing message " + msg); topicPublisher.publish(message);
Since we are now working in a transacted and pooled environment, we have to write the code carefully. We handle errors in our code by rolling back the transaction.
Figure 6.80. [2.4.0] Marking for rollback if there are exceptions
try { //... application code } catch (JMSException ex) { ex.printStackTrace(); ctx.setRollbackOnly(); throw new EJBException(ex.toString()); }
We also have to be careful to close the session, in case an exception occurs, in a finally block.
Figure 6.81. [2.4.0] Closing JMS resources properly
} finally { // ALWAYS close the session. It's pooled, so do not worry. if (topicSession != null) { try { topicSession.close(); } catch (Exception e) { e.printStackTrace(); } } }
Here it is the complete example bean:
Figure 6.82. [2.4.0] Bean example of using JMS as a resource, from TopicHelloBean.java in directory org/jboss/docs/jms/ra/bean
package org.jboss.docs.jms.ra.bean; import java.rmi.RemoteException; import javax.ejb.SessionBean; import javax.ejb.SessionContext; import javax.ejb.EJBException; import javax.naming.InitialContext; import javax.naming.Context; import javax.jms.TopicConnectionFactory; import javax.jms.TopicConnection; import javax.jms.TopicSession; import javax.jms.TopicPublisher; import javax.jms.Topic; import javax.jms.Session; import javax.jms.TextMessage; import javax.jms.JMSException; import org.jboss.docs.jms.ra.interfaces.*; /** * Hello bean, send a message to the configured topic. * The JMS stuff is configured via the deployment descriptor. * * The TopicConnection is comparable to the JDBC DataSource, * and the TopicSession to the JDBC Connection. * * @author Peter Antman * @version $Revision: 1.1.1.1 $ */ public class TopicHelloBean implements SessionBean { /** * Name used to lookup TopicConnectionFactory */ private static final String CONNECTION_JNDI = "java:comp/env/jms/MyTopicConnection"; /** * Name used to lookup topic destination */ private static final String TOPIC_JNDI = "java:comp/env/jms/TopicName"; private SessionContext ctx = null; private Topic topic = null; private TopicConnection topicConnection = null; public TopicHelloBean() { } public void setSessionContext(SessionContext ctx) { this.ctx = ctx; } public void ejbCreate() { try { Context context = new InitialContext(); // Lookup the topic topic = (Topic)context.lookup(TOPIC_JNDI); // Lookup the connection factory TopicConnectionFactory factory = (TopicConnectionFactory)context.lookup(CONNECTION_JNDI); topicConnection = factory.createTopicConnection(); // Keep both around } catch (Exception ex) { // JMSException or NamingException could be thrown ex.printStackTrace(); throw new EJBException(ex.toString()); } } /** * Send a message with a message nr in property MESSAGE_NR */ public void hello(String msg) { sendMessage(msg); } public void ejbRemove() throws RemoteException { if (topicConnection != null) { try { // Remember to close the connection when the bean is destroyed topicConnection.close(); } catch (Exception e) { e.printStackTrace(); } } } public void ejbActivate() {} public void ejbPassivate() {} /** * Help method to send message via JMS */ private void sendMessage(String msg) { TopicSession topicSession = null; try { TopicPublisher topicPublisher = null; TextMessage message = null; // Create a session topicSession = topicConnection.createTopicSession(true, Session.AUTO_ACKNOWLEDGE); // Create a publisher topicPublisher = topicSession.createPublisher(topic); // Create a message message = topicSession.createTextMessage(); message.setText(msg); // Publish it System.out.println("Publishing message " + msg); topicPublisher.publish(message); } catch (JMSException ex) { ex.printStackTrace(); ctx.setRollbackOnly(); throw new EJBException(ex.toString()); } finally { // ALWAYS close the session. It's pooled, so do not worry. if (topicSession != null) { try { topicSession.close(); } catch (Exception e) { e.printStackTrace(); } } } } }
Here there are also the remote and home interfaces:
Figure 6.83. [2.4.0] Remote interface for JMS resource example, from Hello.java in directory org/jboss/docs/jms/ra/interfaces
package org.jboss.docs.jms.ra.interfaces; import javax.ejb.EJBObject; import java.rmi.RemoteException; /** * Remote interface for Hello bean. * * @author Peter Antman * @version $Revision: 1.1.1.1 $ */ public interface Hello extends EJBObject { /** * Send a message to the configured topic * (jms/TopicName in deployment descriptor) */ public void hello(String msg) throws RemoteException; }
Figure 6.84. [2.4.0] Home interface for JMS resource example, from HelloHome.java in directory org/jboss/docs/jms/ra/interfaces
package org.jboss.docs.jms.ra.interfaces; import java.rmi.RemoteException; import javax.ejb.EJBHome; import javax.ejb.CreateException; /** * Home interface for Hello bean. * * @author Peter Antman * @version $Revision: 1.1.1.1 $ */ public interface HelloHome extends EJBHome { Hello create() throws RemoteException, CreateException; }
We also need a client to send messages to the session bean, which is standard client EJB programming.
Figure 6.85. [2.4.0] Client for JMS resource example, from HelloClient.java in directory org/jboss/docs/jms/ra
package org.jboss.docs.jms.ra; import org.jboss.docs.jms.ra.interfaces.Hello; import org.jboss.docs.jms.ra.interfaces.HelloHome; import javax.naming.InitialContext; import javax.naming.Context; /** * Client that send hello messages to a session bean * (which does JMS through the resource adapter). * * @author Peter Antman * @version $Revision: 1.1.1.1 $ */ public class HelloClient { /** * The bean JNDI name */ private String beanJNDI; /** * The Hello bean remote reference */ private Hello hello; /** * @param beanJNDI bean where to send the message */ public HelloClient(String beanJNDI) throws Exception { this.beanJNDI = beanJNDI; setUp(); } /** * Helper to get the remote reference to the bean */ protected void setUp() throws Exception { // Get Hello bean Context context = new InitialContext(); HelloHome helloH = (HelloHome)context.lookup(beanJNDI); hello = helloH.create(); } /** * Say hallo to the bean */ public void hello(String msg) throws Exception { System.out.println("Saying hello"); hello.hello(msg); } public static void main(String[] args) { try { HelloClient c = new HelloClient("TopicHello"); c.hello("Hello topic"); } catch(Exception ex) { System.out.println("Error: " + ex); ex.printStackTrace(); } } } // HelloClient
The next step is to write the deployment descriptors for the bean. In the standard ejb-jar.xml we must add stanzas for the resources. The res-ref-name element must contain the name used in the bean to lookup the resource:
Figure 6.86. [2.4.0] resource-ref stanzas in ejb-jar.xml for JMS resource example
<resource-ref> <description>A Topic ConnectionFactory</description> <res-ref-name>jms/MyTopicConnection</res-ref-name> <res-type>javax.jms.TopicConnectionFactory</res-type> <res-auth>Container</res-auth> </resource-ref> <resource-ref> <description>A Topic </description> <res-ref-name>jms/TopicName</res-ref-name> <res-type>javax.jms.Topic</res-type> <res-auth>Container</res-auth> </resource-ref>
In jboss.xml we must also add resource-manager stanzas to point out the real resources:
Figure 6.87. [2.4.0] resource-managers stanza in jboss.xml for JMS resource example
<resource-managers> <resource-manager> <res-name>topicfactoryref</res-name> <res-jndi-name>java:/JmsXA</res-jndi-name> </resource-manager> <resource-manager> <res-name>topicref</res-name> <res-jndi-name>topic/testTopic</res-jndi-name> </resource-manager> </resource-managers>
The most magical line here is the java:/JmsXA contents of the res-jndi-name element, which points to the J2EE Connector-compliant JMS resource adapter. Also in jboss.xml, we must add the definition of the session EJB, as always:
Figure 6.88. [2.4.0] Bean configuration in jboss.xml for JMS resource example
<session> <ejb-name>TopicHello</ejb-name> <jndi-name>TopicHello</jndi-name> <configuration-name>Standard Stateless SessionBean</configuration-name> <resource-ref> <res-ref-name>jms/MyTopicConnection</res-ref-name> <resource-name>topicfactoryref</resource-name> </resource-ref> <resource-ref> <res-ref-name>jms/TopicName</res-ref-name> <resource-name>topicref</resource-name> </resource-ref> </session>
Here there are the complete listings for the deployment descriptors:
Figure 6.89. [2.4.0] JMS resource example ejb-jar.xml, from TopicHello-ejb-jar.xml in directory org/jboss/docs/jms/resources
<?xml version="1.0" encoding="Cp1252"?> <ejb-jar> <description>Topic Hello</description> <display-name>TopicHelloBean</display-name> <enterprise-beans> <session> <display-name>TopicHello</display-name> <ejb-name>TopicHello</ejb-name> <home>org.jboss.docs.jms.ra.interfaces.HelloHome</home> <remote>org.jboss.docs.jms.ra.interfaces.Hello</remote> <ejb-class>org.jboss.docs.jms.ra.bean.TopicHelloBean</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> <resource-ref> <description>A Topic ConnectionFactory</description> <res-ref-name>jms/MyTopicConnection</res-ref-name> <res-type>javax.jms.TopicConnectionFactory</res-type> <res-auth>Container</res-auth> </resource-ref> <resource-ref> <description>A Topic </description> <res-ref-name>jms/TopicName</res-ref-name> <res-type>javax.jms.Topic</res-type> <res-auth>Container</res-auth> </resource-ref> </session> </enterprise-beans> <assembly-descriptor/> </ejb-jar>
Figure 6.90. [2.4.0] JMS resource example jboss.xml, from TopicHello24-jboss.xml in directory org/jboss/docs/jms/resources
<?xml version="1.0" encoding="Cp1252"?> <jboss> <secure>false</secure> <resource-managers> <resource-manager> <res-name>topicfactoryref</res-name> <res-jndi-name>java:/JmsXA</res-jndi-name> </resource-manager> <resource-manager> <res-name>topicref</res-name> <res-jndi-name>topic/testTopic</res-jndi-name> </resource-manager> </resource-managers> <enterprise-beans> <session> <ejb-name>TopicHello</ejb-name> <jndi-name>TopicHello</jndi-name> <configuration-name>Standard Stateless SessionBean</configuration-name> <resource-ref> <res-ref-name>jms/MyTopicConnection</res-ref-name> <resource-name>topicfactoryref</resource-name> </resource-ref> <resource-ref> <res-ref-name>jms/TopicName</res-ref-name> <resource-name>topicref</resource-name> </resource-ref> </session> </enterprise-beans> </jboss>
It is possible to run the example with the Ant target:
A nice twist to the example above is how easy it is to reconfigure it to send messages to a remote JBoss server. You basically have to do two things.
First, you need to add two things to jboss.xml: The remote JMS provider and a JMS resource adapter that uses the remote JMS provider. When defining the remote JMS provider you should fill in the ProviderUrl property. For JBoss version 2.4.0 and for the URL linutv1.annons.dn.se:1099, it might look like this:
Figure 6.92. [2.4.0] Defining a remote JMSProviderLoader in jboss.jcml, from TopicHello24-Remote-snippet-jboss.jcml in directory org/jboss/docs/jms/resources
<mbean code="org.jboss.jms.jndi.JMSProviderLoader" name=":service=JMSProviderLoader,name=MyRemoteLoader"> <attribute name="ProviderName">MyRemoteProvider</attribute> <attribute name="ProviderUrl">linutv1.annons.dn.se:1099</attribute> <attribute name="QueueFactoryRef">XAQueueConnectionFactory</attribute> <attribute name="TopicFactoryRef">XATopicConnectionFactory</attribute> <attribute name="ProviderAdapterClass">org.jboss.jms.jndi.JBossMQProvider</attribute> </mbean>
[2.4.1. In this version it looks slightly different.]
Figure 6.93. [2.4.1] Defining a remote JMSProviderLoader in jboss.jcml
<!-- This must go into jboss.jcml --> <!-- to get remote version to work --> <mbean code="org.jboss.jms.jndi.JMSProviderLoader" name=":service=JMSProviderLoader,name=MyRemoteLoader"> <attribute name="ProviderName">MyRemoteProvider</attribute> <attribute name="ProviderUrl">linutv1.annons.dn.se:1099</attribute> <attribute name="QueueFactoryRef">XAConnectionFactory</attribute> <attribute name="TopicFactoryRef">XAConnectionFactory</attribute> <attribute name="ProviderAdapterClass">org.jboss.jms.jndi.JBossMQProvider</attribute> </mbean>
You then need to add a JMS resource adapter that uses the remote provider. We give it a name, in this case RemoteJmsXA, and we configure the adapter property JmsProviderAdapterJNDI to point to our newly-defined remote provider java:MyRemoteProvider.
Figure 6.94. [2.4.0] Defining a remote JMS Resource Adapter in jboss.jcml, from TopicHello24-Remote-snippet-jboss.jcml in directory org/jboss/docs/jms/resources
<!-- JMS XA Resource adapter, --> <!-- use this to get transacted JMS in beans --> <mbean code="org.jboss.resource.ConnectionFactoryLoader" name="JCA:service=ConnectionFactoryLoader,name=RemoteJmsXA"> <attribute name="FactoryName">RemoteJmsXA</attribute> <attribute name="RARDeployerName">JCA:service=RARDeployer</attribute> <attribute name="ResourceAdapterName">JMS Adapter</attribute> <attribute name="ConnectionManagerFactoryName">MinervaXACMFactory</attribute> <!-- See the documentation for the specific connection manager implementation you are using for the properties you can set --> <attribute name="ConnectionManagerProperties"> <attribute name="Properties"> JmsProviderAdapterJNDI=java:MyRemoteProvider # Pool type - uncomment to force, otherwise it is the default #PoolConfiguration=per-factory # Connection pooling properties - see # org.opentools.minerva.pool.PoolParameters MinSize=0 MaxSize=10 Blocking=true GCEnabled=false IdleTimeoutEnabled=false InvalidateOnError=false TrackLastUsed=false GCIntervalMillis=120000 GCMinIdleMillis=1200000 IdleTimeoutMillis=1800000 MaxIdleTimeoutPercent=1.0 </attribute> <!-- Principal mapping configuration --> <attribute name="PrincipalMappingClass">org.jboss.resource.security.ManyToOnePrincipalMapping</attribute> <attribute name="PrincipalMappingProperties"> </attribute> </mbean>
The second thing to do is to rewrite the jboss.xml deployment descriptor. We now let the res-jndi-name property for the topicfactoryref resource point to our defined resource adapter java:/RemoteJmsXA, and we point the destination resource to the JNDI namespace of our remote server, jnp://linutv1.annons.dn.se:1099/topic/testTopic. Here it is the complete descriptor file:
Figure 6.95. [2.4.0] jboss.xml for remote version of JMS resource example, from TopicHello24-Remote-jboss.xml in directory org/jboss/docs/jms/resources
<?xml version="1.0" encoding="Cp1252"?> <jboss> <secure>false</secure> <resource-managers> <resource-manager> <res-name>topicfactoryref</res-name> <res-jndi-name>java:/RemoteJmsXA</res-jndi-name> </resource-manager> <resource-manager> <res-name>topicref</res-name> <res-jndi-name>jnp://linutv1.annons.dn.se:1099/topic/testTopic</res-jndi-name> </resource-manager> </resource-managers> <enterprise-beans> <session> <ejb-name>TopicHello</ejb-name> <jndi-name>TxTopicHello</jndi-name> <configuration-name>Standard Stateless SessionBean</configuration-name> <resource-ref> <res-ref-name>jms/MyTopicConnection</res-ref-name> <resource-name>topicfactoryref</resource-name> </resource-ref> <resource-ref> <res-ref-name>jms/TopicName</res-ref-name> <resource-name>topicref</resource-name> </resource-ref> </session> </enterprise-beans> </jboss>
Is there no way of getting JMS to work as a resource in JBoss 2.2.x then? Well, it depends. Probably, it is possible to deploy the resource adapter for JMS in JBoss version 2.2.x, but that is an advanced and untried subject. Go ahead and test it and report whether it works. However, there is at least one more thing you can do to a least ease the transition from JBoss release 2.2.x to later ones. You may actually code your beans as if they are using JMS as a resource.
It is very important to understand that even if you do this you will not see the correct behavior of getting the transactions enlisted. The only benefit for you is that you will get it all when upgrading to at least version 2.4.0 and without a lot of code or descriptor file rewriting.
So, how is it done? The first thing you need to do, in comparison to the example compatible with version 2.4.0, is to make a small change in the code where the JMS session is created. You need to mark the session as not transacted:
Figure 6.96. Creating a session for JMS resource emulation, from TopicHelloBean22.java in directory org/jboss/docs/jms/ra/bean
topicSession = topicConnection.createTopicSession( // NOT transacted false, Session.AUTO_ACKNOWLEDGE);
To be completely honest, it is actually possible to use this code with the JMS resource adapter in version 2.4.0 too, since the topic connection will not care whether you mark your JMS session as transacted or not when running with XA transactions. By writing your code like this it will work in version 2.2.x and later.
However, you are forced to change the deployment descriptor file. In version 2.4.0 we pointed the connection factory resource to the JMS resource adapter. In 2.2.x you must point it directly to a ConnectionFactory. It is not possible to use the res-jndi-name element, either; use the res-url element, instead. This is how it might look:
Figure 6.97. JMS resource emulation in jboss.xml
<?xml version="1.0" encoding="Cp1252"?> <jboss> <secure>false</secure> <resource-managers> <resource-manager> <res-name>topicfactoryref</res-name> <res-url>TopicConnectionFactory</res-url> </resource-manager> <resource-manager> <res-name>topicref</res-name> <res-url>topic/testTopic</res-url> </resource-manager> </resource-managers> <enterprise-beans> <session> <ejb-name>TopicHello</ejb-name> <jndi-name>TopicHello</jndi-name> <configuration-name>Standard Stateless SessionBean</configuration-name> <resource-ref> <res-ref-name>jms/MyTopicConnection</res-ref-name> <resource-name>topicfactoryref</resource-name> </resource-ref> <resource-ref> <res-ref-name>jms/TopicName</res-ref-name> <resource-name>topicref</resource-name> </resource-ref> </session> </enterprise-beans> </jboss>
You may test the example through Ant:
Наши друзья |