Thursday, 3 March 2011

Get started with Spring JMS using ActiveMQ

Messaging is a mechanism to create and exchange data/information/objects within/between applications. An application that generates some useful data can send it's messages (objects) to an agent and these messages (objects) can then be read from another application. JMS is a Java API that enables applications to be able to communicate with each other using the mechanism described above.

You can read more about JMS here.

In this article, we will see how you can quickly start using JMS using the Spring JMS framework. Spring provides template based APIs which greatly simplify the use of JMS. A lot of work is done behind the scenes which saves the developer time to concentrate on the business requirement.

We will take a look at the important Spring JMS classes and apis as we go through an example. So let's get started!



Step 1: Download and install the JMS provided (ActiveMQ)
The first thing you would need is a JMS provider (which is an implementation of the JMS interface for a MOM - Message Oriented Middleware). There are a lot of providers available, such as Apache's ActiveMQ, IBM's WebSphere M, JBoss messaging, RabbitMQ, Oracle AQ and many others.

For the purpose of this tutorial we will be using ActiveMQ. You can download the zip file and install it using the instructions documented here. Start the provider (open a command prompt to the bin folder of the installation and type 'activemq').

To test the setup, browse to : http://localhost:8161/admin/

To be able to send and receive messages asynchronously, we would first need a queue, which is a destination for the messages. Let's create a fresh queue for our sample application.
Go to the 'Queues' menu on the top of the admin page, and create a new queue called 'orderQueue'.


Step 2: Create a Spring project
You can create a spring project manually or use the STS (SpringSource Tool Suite) like i am using. This IDE helps you get started with Spring projects within no time.
Create a 'Spring Utility Project' using 'File -> New -> Spring Template Project -> Simple Spring Utility Project'.

Type the project name , say 'OrderApp' and choose a base package say - com.test.orderapp

This project type uses maven for dependency management. We would need to add a few dependencies for using spring-jms and activemq.

Open the 'pom.xml' at the root of the project and add the following dependencies:
      <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jms</artifactId>
            <version>${spring.framework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-all</artifactId>
            <version>5.4.2</version>
        </dependency>
Note: Update the version for activemq as per the installation you have used in Step 1.

Step 3: Configure beans in application context
Edit the Spring context file app-context.xml at \src\main\resources\META-INF\spring and add the following to it:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:context="http://www.springframework.org/schema/context"
     xmlns:jms="http://www.springframework.org/schema/jms"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/jms
                           http://www.springframework.org/schema/jms/spring-jms.xsd
                           http://activemq.apache.org/schema/core
                           http://activemq.apache.org/schema/core/activemq-core.xsd">

    <context:component-scan base-package="com.test.orderapp" />
    <context:annotation-config/>

    <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
        <property name="brokerURL">
            <value>tcp://localhost:61616</value>
        </property>
    </bean>
   
    <bean id="destination" class="org.apache.activemq.command.ActiveMQQueue">
        <constructor-arg value="orderQueue"/>
    </bean>
   
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="defaultDestination" ref="destination" />
    </bean>
</beans>
Let's take a closer look at the beans we've defined in the spring xml.
1. connectionFactory:
A connectionFactory is required to create a connection to the jms provider i.e. activemq. The javax.jms.ConnectionFactory creates a javax.jms.Connection object, which then creates a javax.jmx.Session object which is used for the message exchange between the application and the jms provider. You need to specify the brocker URL , i.e. the URL at which the jms provider is listening.

2. destination:
The destination specifies the queue name to which the messages will be sent. Messages will be stored in and consumed from the destination specified. Destinations can, in general, be of two types: Queue and Topic. Queues represent a point-to-point messaging mechanism whereas Topics represent a publisher-subscriber sort of messaging where a single message is sent to multiple consumers. (we will be working with only queues in this tutorial)

3. jmsTemplate:
This is the class that does most of the jms related work, it creates connection factories, creates Sessions, takes care of the transactions, provides apis for creating and sending messages of different types. The jmsTemplate bean here takes a reference to the connectionFactory and a defaultDestination to which it would direct the messages. If you don't specify any defaultDestination, you can pass the destination in the 'send' methods that jmsTemplate provides. The important methods provided by jmsTemplate are : send, receive, convertAndSend and receiveAndConvert

Step 4: Create the domain class
We will be creating a simple domain class called 'Order' in a package com.test.orderapp.domain as follows:
 package com.test.orderapp.domain;
public class Order {

    private int orderId;
    private int customerId;
    private double price;
    private String orderCode;
  
    public Order(int orderId, int customerId, double price, String orderCode){
        this.orderId = orderId;
        this.customerId = customerId;
        this.price = price;
        this.orderCode = orderCode;
    }
  
    public int getOrderId() {
        return orderId;
    }
    public void setOrderId(int orderId) {
        this.orderId = orderId;
    }
    public int getCustomerId() {
        return customerId;
    }
    public void setCustomerId(int customerId) {
        this.customerId = customerId;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    public String getOrderCode() {
        return orderCode;
    }
    public void setOrderCode(String orderCode) {
        this.orderCode = orderCode;
    }
}
Step 5: Write the code to send the objects

We will create a simple service class that creates new orders given some information and then sends them.
package com.test.orderapp.producer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.test.orderapp.domain.Order;

@Service("orderService")
public class OrderService {
    static int orderSequence = 1;
   
    @Autowired
    private OrderSender orderSender;
    public void setOrderSender(OrderSender orderSender){
        this.orderSender = orderSender;
    }
   
    public void sendOrder(int customerId, double price)
    {
        Order order = new Order(orderSequence, 2, price, "ordercd"+ orderSequence++);
        orderSender.sendOrder(order);
    }
}
Nothing special about the service class. We are wiring the  OrderSender class into the service class.
The orderSender class is jms aware and has all the logic to convert and send the domain object as a jms message.

OrderSender.java :-
package com.test.orderapp.producer;

import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import com.test.orderapp.domain.Order;

public class OrderSender {
   
    @Autowired
    private JmsTemplate jmsTemplate;
   
    public void sendOrder(final Order order){
        jmsTemplate.send(
        new MessageCreator() {
          public Message createMessage(Session session) throws JMSException {
               MapMessage mapMessage = session.createMapMessage();
               mapMessage.setInt("orderId", order.getOrderId());
               mapMessage.setInt("customerId", order.getCustomerId());
               mapMessage.setDouble("price", order.getPrice());
               mapMessage.setString("orderCode", order.getOrderCode());
               return mapMessage;
          }
        }
        );
        System.out.println("Order sent - id: "+ order.getOrderId());
    }
}
The OrderSender uses the JmsTemplate's send method to send the object. Notice the use of the MessageCreator class. You can send different types of messages via JMS eg: TextMessage, ObjectMessage, MapMessage (all part of the java.jms package) The most preferred way to send complex domain objects is to use MapMessage to which you can add all the fields of your domain object as key-value pairs.  The MessageCreator class is a callback interface for creating a message using the Session object that is passed as a parameter to the createMessage method.
Also note, that we are not specifying the destination in the send method since we have defined a defaultDestination for the jmsTemplate in the spring context. In case you are not going the default way, you can pass a string representing the destination as the first parameter to the jmsTemplate.send() method
for eg:      
          jmsTemplate.send( "orderQueue",     new MessageCreator() ..... )

There are other ways to send messages such as MessageConverter which can also be used to convert between Java objects and JMS messages. The convertAndSend and receiveAndConvert methods of the jmsTemplate class delegate the conversion to the MessageConverter interface. We are not using MessageConverter in this tutorial. Some other time maybe...

Let's update the app-context.xml file to declare two additional bean definitions:
<bean id="orderService" class="com.test.orderapp.source.OrderService" />
<bean id="orderSender" class="com.test.orderapp.source.OrderSender" /
Step 6: Test the message producer
Let's check if we are actually able to send any messages to the ActiveMQ 'orderQueue'. We will create a simple TestJms class as follows:
package com.test.orderapp.runner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.test.orderapp.producer.OrderService;

public class TestJms {
    public static void main(String[] args) {
       
        ApplicationContext ctx = new ClassPathXmlApplicationContext("app-context.xml");
        OrderService orderService = (OrderService) ctx.getBean("orderService");
       
        for(int i =1; i<=5; i++)
            orderService.sendOrder(1+i, 10.0D+i);
    }
}

At this point, if you run the TestJms class, 5 messages would be sent to the 'orderQueue' queue. If you browse to the http://localhost:8161/admin/queues.jsp page you should see 5 messages enqueued on orderQueue.


Step 7: Consuming JMS Messages


Listening to the jms messages is in fact easier than sending them. All we need to do is define a jms Listener Container in the spring context to which we pass the connectionFactory bean which specifies the jms provider, a reference to the destination which specifies the queue name and we also specify here, which method from which bean needs to be called when a message is recieved.



Add the following to the app-context.xml file:
<jms:listener-container  connection-factory="connectionFactory">
     <jms:listener destination="orderQueue" ref="orderListener" method="orderReceived" />
</jms:listener-container>
Create a class OrderListener as follows:
package com.test.orderapp.listener;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.test.orderapp.domain.Order;

@Component
public class OrderListener {

    public void orderReceived(Map<String, Object> message) throws Exception {
        int orderId = (Integer) message.get("orderId");
        int customerId = (Integer) message.get("customerId");
        double price = (Double) message.get("price");
        String orderCode = (String) message.get("orderCode");
        Order customer = new Order(orderId, customerId, price, orderCode);
        System.out.println("Order received: "+ orderId + ", customerId: "+ customerId + ", price: "+ price);
      }
}
Simple.

Let's run the TestJms class once again, this time we should see some prints from the OrderListener class too.
Pretty cool, isn't it!

Well, that brings us to the end of the introductory article to Spring JMS, i hope you can now appreciate how much easier it is to write JMS based applications using Spring.

Hope you find this article useful.
Happy coding!

Download the source code of the sample application : OrderApp.rar




22 comments:

  1. Hey, thanks.. happy to know it helped!

    ReplyDelete
  2. Hello there,

    Awesome post! By the way, is there an email address I can contact you in person ?

    ReplyDelete
  3. Hi Illias,thanks for appreciating..
    Can i have your email address so i can email you mine :D

    ReplyDelete
  4. awesome post...thanks for taking time to share this with the world....

    ReplyDelete
  5. Hey thanks, so glad u found it useful :)

    ReplyDelete
  6. Can you explain the same example with "Topic" i.e.
    single message is sent to multiple consumers

    ReplyDelete
  7. I am getting nullpointerexception while running the OrderListener from TestJms main method.Can you please help me.

    ReplyDelete
  8. Hi there, First of all thanks for the nice post,
    I am able to send the message, and i can see that in the queue (using the web console)< but not able to consume the message >>

    can you please help

    ReplyDelete
  9. I was having problems consuming the messages also. There was a problem with the active MQ 5.5.0 library that i was using. I changed this to use the following in the pom.


    org.apache.activemq
    activemq-all
    5.4.2


    This will work with the 5.5 Active MQ broker. All worked okay after this. Thanks for the great post.

    ReplyDelete
  10. This comment has been removed by the author.

    ReplyDelete
  11. Hi!

    I'm a community curator at DZone.com and I really was impressed by your blog and the interesting Spring Integration topics you write about. We’re in the works of putting up a new ‘Enterprise Integration Zone.’ I'm positive our community would be interested to hear about some of your experiences and expertise.

    I was going to ask you if you would be interested in giving me permission to republish one of your recent posts to benefit our audience and give some exposure to your content.

    If you're interested, I'd also like to let you know about our MVB (most valuable blogger) program, which is now at 400 strong. Here are the details about that: www.dzone.com/aboutmvb With the quality of writing displayed in your blog, I'd be honored to invite you into our program. You get a shiny web-badge :)

    Thanks!
    Prasant Lokinendi
    Community Curator
    ploki [at] dzone [dot] com

    ReplyDelete
  12. a quick look on JMS programming using Topic and Queue in JMS Provider (ACTIVEMQ) JMS Spring

    ReplyDelete
  13. Thank you, it helped me a lot.

    ReplyDelete
  14. thank you , I look for method of ActiveMQ installation .
    I found this is link : http://www.jmkg.co.uk/2010/08/31/installing-activemq-on-ubuntu/

    but I have this pb when I open http://localhost:8161/

    Error 404 - Not Found.

    No context on this server matched or handled this request.
    Contexts known to this server are:
    /admin ---> o.e.j.w.WebAppContext{/admin,file:/home/wa/stage2012/jms/activemq/apache-activemq-5.7.0/webapps/admin/}
    /demo ---> o.e.j.w.WebAppContext{/demo,file:/home/wa/stage2012/jms/activemq/apache-activemq-5.7.0/webapps/demo/}
    /fileserver ---> o.e.j.w.WebAppContext{/fileserver,file:/home/wa/stage2012/jms/activemq/apache-activemq-5.7.0/webapps/fileserver/}

    ReplyDelete
  15. Hi,
    Thanks for this tutorial,
    I followed your steps, when I tried to run the class : TestJms.java

    I got this error :

    21:47:19,919 INFO t.support.ClassPathXmlApplicationContext: 456 - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@c7e553: startup date [Tue Nov 13 21:47:19 EST 2012]; root of context hierarchy
    21:47:20,028 INFO eans.factory.xml.XmlBeanDefinitionReader: 315 - Loading XML bean definitions from class path resource [app-context.xml]
    Exception in thread "main" org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from class path resource [app-context.xml]; nested exception is java.io.FileNotFoundException: class path resource [app-context.xml] cannot be opened because it does not exist
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:341)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:302)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:143)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:178)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:149)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:212)
    at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:126)
    at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:92)
    at org.springframework.context.support.AbstractRefreshableApplicationContext.refreshBeanFactory(AbstractRefreshableApplicationContext.java:130)
    at org.springframework.context.support.AbstractApplicationContext.obtainFreshBeanFactory(AbstractApplicationContext.java:467)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:397)
    at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:139)
    at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:83)
    at com.test.orderapp.runner.TestJms.main(TestJms.java:9)
    Caused by: java.io.FileNotFoundException: class path resource [app-context.xml] cannot be opened because it does not exist

    ReplyDelete
    Replies
    1. modify
      1. TestJms.java : ClassPathXmlApplicationContext("/META-INF/spring/app-context.xml");
      2. app-context.xml

      <bean id="orderSender" class="com.test.orderapp.producer.OrderSender" /

      Delete
  16. Hi,
    Nice tutorial, I imported it in my STS, but the pom.xml has an error that I did not understand, please your help is appreciated :

    Failure to find com.springsource.bundlor:com.springsource.bundlor.maven:pom:1.0.0.RELEASE in http://
    repository.codehaus.org/ was cached in the local repository, resolution will not be reattempted until the update
    interval of Codehaus has elapsed or updates are forced

    ReplyDelete
  17. Nice one.

    I have one suggestion, if we configure listener / consumption as a new project. It will give more clarity on real time consumption.

    ReplyDelete