13 March 2011

Transparent Asynchronous Remoting via JMS

The Scenario


A client needs to invoke a service interface with the following restrictions:
  • The service implementation is running on a remote machine.
  • This fact is transparent to the client, i.e. any service method invocation is just like a local method invocation.
  • The service methods are executed asychronously on the server, method invocations on the client return immediately (fire-and-forget).
  • All service methods have void return types.
  • The service invocations shall be transported via JMS.


Background


The post is a sequel to my previous article on Transparent Asynchronous Remoting, the only difference in this scenario is the addition of the JMS requirement.

This sounds more like an implementation detail than a requirement, but in fact there can be good reasons for mandating the use of JMS, e.g.
  • You want to decouple service providers and clients: a client may invoke a service method even when the service provider is temporarily not available.
  • The system's communication infrastructure is based on JMS anyway.
  • You want to monitor the communication.

The Solution with Apache Camel, Spring and Glassfish MQ

There is no out-of-the box solution for this scenario either in Java EE 6 or in Spring.

Java EE 6 has an @Async annotation for asynchronous invocations of remote EJBs, but there is no way to use a JMS transport simply by configuration.

Spring Remoting provides transparent proxies for remote services using various transports, including JMS, but these proxies are always synchronous.

I've tried to solve this problem by modifying or extending some of the Spring JMS helper classes, but this turned out to be not quite trivial. The next thing I tried was Spring Integration, but this seems to be lacking the transparent proxy feature and only provides a partial solution for services with single-parameter methods.

In the end, I turned to Apache Camel, which solves the problem rather nicely, once you find the relevant bits of example code and documentation. camel-example-spring-jms was a good starting point. The setup explained in the rest of this post differs from the Camel example in the following points:
  • The communication is asynchronous.
  • The Spring contexts are defined by  Java Configuration instead of XML.
  • The JMS provider is Glassfish MQ, not Active MQ.
  • Apache Commons Logging is replaced by slf4j and logback.
  • Dependency injection is based on the JSR-330 @Inject annotation.

Overview


Apache Camel can be regarded as a giant plumbing framework for message-based communication between endpoints of all sorts. You create endpoints supporting a given communication protocol, you connect them with routes, and you may insert filters to modify the messages on their way.

Camel directly supports Spring beans as endpoints and uses proxies to adapt method invocations to Camel messages and vice versa. Hence, the setup for our scenario is rather similar to Spring Remoting.

On the server side, the service implementations are wrapped in proxies which are connected to JMS queues by Camel routes.

On the client side, you create proxies for the remote services and inject them into your application classes. The proxies are directly connected to JMS service URLs.

In addition, you need a JMS provider. The original Camel/Spring/JMS example uses an ActiveMQ broker embedded into the server. I'm using a stand-alone Glassfish MQ broker, just to show that Camel also works with other JMS implementations.

The API


Following the tradition of "Hello World" examples, we create a HelloService to say hello to a given person, and we add a GoodbyeService.

package com.blogspot.hwellmann.camel.greeter.api;

@InOnly 
public interface HelloService {
    void sayHello(Person person);
}

package com.blogspot.hwellmann.camel.greeter.api;

@InOnly 
public interface GoodbyeService {
    void sayGoodbye(Person person);
}

@InOnly is a Camel annotation indicating that a given method or, in this case, all methods of the given interface, take input parameters only and that the caller shall not wait for any reply. Thanks to this annotation,  our scenario is indeed asynchronous.

By default, Camel uses standard Java serialization for transporting method parameters, so all parameter objects need to be Serializable:

package com.blogspot.hwellmann.camel.greeter.api;

import java.io.Serializable;

public class Person implements Serializable {

    private static final long serialVersionUID = 1L;

    private String firstName;
    private String lastName;
     
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // ...getters and setters...
}

The Server


package com.blogspot.hwellmann.camel.greeter.server;

public class HelloServiceImpl implements HelloService {

    public void sayHello(Person person) {
        try {
            Thread.sleep(1000);
            System.out.println(String.format("Hello, %s %s!", 
                    person.getFirstName(), person.getLastName()));
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The GoodbyeService is just the same. Now here is the Spring configuration for the server:

<?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" 
  xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <context:annotation-config  />
  <bean class="com.blogspot.hwellmann.camel.greeter.server.GreeterServerSpringConfig"/>

</beans>

package com.blogspot.hwellmann.camel.greeter.server;

@Configuration
public class GreeterServerSpringConfig {
    
    @Inject
    private ApplicationContext applicationContext;

    @Bean
    public ConnectionFactory cf() throws JMSException {
        com.sun.messaging.ConnectionFactory cf = new com.sun.messaging.ConnectionFactory();
        cf.setProperty(ConnectionConfiguration.imqAddressList, "localhost:7676");
        return cf;
    }
    
    @Bean
    public HelloService helloService() {
        return new HelloServiceImpl();
    }
    
    @Bean
    public GoodbyeService goodbyeService() {
        return new GoodbyeServiceImpl();
    }
    
    @Bean
    public JmsComponent jms() throws JMSException {
        JmsComponent jms = new JmsComponent();
        jms.setConnectionFactory(cf());
        return jms;
    }
    
    @Bean
    public RouteBuilder serverRoutes() {
        return new ServerRoutes();
    }
    
    @Bean
    public CamelContext camelContext() throws Exception {
        CamelContextFactoryBean factory = new CamelContextFactoryBean();
        factory.setApplicationContext(applicationContext);
        factory.setId("jms-server");
        SpringCamelContext context = factory.getContext();
        serverRoutes().addRoutesToCamelContext(context);
        return context;
    }    
}

The ServerRoutes bean defines two simple Camel routes connecting the JMS queues to our service implementations:

package com.blogspot.hwellmann.camel.greeter.server;

import org.apache.camel.builder.RouteBuilder;

public class ServerRoutes extends RouteBuilder {
    
    @Override
    public void configure() throws Exception {
        from("jms:queue:hello").to("helloService");
        from("jms:queue:goodbye").to("goodbyeService");
    }
}

Camel has its own org.apache.camel.spring.Main class which by default creates a Spring application context from all XML files found in META-INF/spring on the classpath. This is enough to launch the server.

In addition, you need to launch the Glassfish MQ message broker on the default port 7676. To do so, you can simply start a complete Glassfish instance, which has an embedded MQ broker, or you may use a standalone Glassfish MQ broker by running GLASSFISH_HOME/mq/bin/imqbrokerd, where GLASSFISH_HOME refers to your Glassfish installation directory. (Tested with Glassfish 3.1 and Glassfish 3.0.1).

The Client


This is the client for saying hello and goodbye to three persons:

package com.blogspot.hwellmann.camel.greeter.client;

public class GreeterClient implements Runnable {
    
    private static Logger log = LoggerFactory.getLogger(GreeterClient.class);

    @Inject
    private HelloService helloProxy;
    
    @Inject
    private GoodbyeService goodbyeProxy;
    
    private Person donald = new Person("Donald", "Duck");
    private Person mickey = new Person("Mickey", "Mouse");
    private Person charlie = new Person("Charlie", "Brown");

    public void run() {
        log.info("invoking greeter");
        
        helloProxy.sayHello(donald);
        helloProxy.sayHello(mickey);
        helloProxy.sayHello(charlie);

        goodbyeProxy.sayGoodbye(donald);
        goodbyeProxy.sayGoodbye(mickey);
        goodbyeProxy.sayGoodbye(charlie);

        log.info("done");
    }
        
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(GreeterClientSpringConfig.class);
        GreeterClient client = context.getBean(GreeterClient.class);
        client.run();
    }    
}


Look Mum, no XML! The Spring application context for the client is completely defined in a Java configuration class:

package com.blogspot.hwellmann.camel.greeter.client;

@Configuration
public class GreeterClientSpringConfig {
    
    @Inject
    private ApplicationContext applicationContext;

    @Bean
    public ConnectionFactory cf() throws JMSException {
        com.sun.messaging.ConnectionFactory cf = new com.sun.messaging.ConnectionFactory();
        cf.setProperty(ConnectionConfiguration.imqAddressList, "localhost:7676");
        return cf;
    }
    
    @Bean
    public GreeterClient greeterClient() {
        return new GreeterClient();
    }
    
    @Bean
    public JmsComponent jms() throws JMSException {
        JmsComponent jms = new JmsComponent();
        jms.setConnectionFactory(cf());
        return jms;
    }
    
    @Bean
    public HelloService helloProxy() throws Exception {
        CamelProxyFactoryBean factory = new CamelProxyFactoryBean();
        factory.setServiceInterface(HelloService.class);
        factory.setServiceUrl("jms:queue:hello");
        factory.setCamelContext(camelContext());
        factory.afterPropertiesSet();
        return (HelloService) factory.getObject();
    }
    
    @Bean
    public GoodbyeService goodbyeProxy() throws Exception {
        CamelProxyFactoryBean factory = new CamelProxyFactoryBean();
        factory.setServiceInterface(GoodbyeService.class);
        factory.setServiceUrl("jms:queue:goodbye");
        factory.setCamelContext(camelContext());
        factory.afterPropertiesSet();
        return (GoodbyeService) factory.getObject();
    }
    
    @Bean
    public CamelContext camelContext() throws Exception {
        CamelContextFactoryBean factory = new CamelContextFactoryBean();
        factory.setApplicationContext(applicationContext);
        factory.setId("jms-client");
        factory.afterPropertiesSet();
        return (CamelContext) factory.getObject();
    }
}


The POM


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.blogspot.hwellmann</groupId>
  <artifactId>spring-camel-jms</artifactId>
  <version>0.0.1</version>
  <name>Spring Camel JMS Example</name>
  <properties>
    <camel.version>2.6.0</camel.version>
    <spring.version>3.0.5.RELEASE</spring.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-core</artifactId>
      <version>${camel.version}</version>
      <exclusions>
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging-api</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-jms</artifactId>
      <version>${camel.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
      <exclusions>
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.apache.servicemix.bundles</groupId>
      <artifactId>org.apache.servicemix.bundles.cglib</artifactId>
      <version>2.2_1</version>
    </dependency>    
    <dependency>
      <groupId>org.apache.geronimo.specs</groupId>
      <artifactId>geronimo-atinject_1.0_spec</artifactId>
      <version>1.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.6.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>1.6.0</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>0.9.28</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>0.9.28</version>
    </dependency>
    <dependency>
      <groupId>com.sun.messaging.mq</groupId>
      <artifactId>imq</artifactId>
      <version>4.4.2</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Running the example


Now if you start the broker, the server and the client in this order, you'll see the following output from the client...

20:21:46.789 [main] INFO  c.b.h.c.greeter.client.GreeterClient - invoking greeter
20:21:47.016 [main] INFO  c.b.h.c.greeter.client.GreeterClient - done

...and from the server:

20:21:47.976 [DefaultMessageListenerContainer-1] INFO  c.b.h.c.g.server.HelloServiceImpl - Hello, Donald Duck!
20:21:47.995 [DefaultMessageListenerContainer-1] INFO  c.b.h.c.g.server.GoodbyeServiceImpl - Goodbye, Donald Duck!
20:21:48.998 [DefaultMessageListenerContainer-1] INFO  c.b.h.c.g.server.HelloServiceImpl - Hello, Mickey Mouse!
20:21:49.003 [DefaultMessageListenerContainer-1] INFO  c.b.h.c.g.server.GoodbyeServiceImpl - Goodbye, Mickey Mouse!
20:21:50.005 [DefaultMessageListenerContainer-1] INFO  c.b.h.c.g.server.HelloServiceImpl - Hello, Charlie Brown!
20:21:50.009 [DefaultMessageListenerContainer-1] INFO  c.b.h.c.g.server.GoodbyeServiceImpl - Goodbye, Charlie Brown!

The client terminates before the server has even processed the first message. By default, Camel generates a single-threaded consumer for each queue, so the order of the Hellos is preserved, but they get processed in parallel with the Goodbyes.

Conclusion


Using either Java EE 6 or Spring 3, the standard framework APIs are not sufficient to implement transparent asynchronous communication via JMS.

However, with Apache Camel on top of Spring 3, a plug-and-play solution is easy to set up.

4 comments:

Claus Ibsen said...

Hi Harald

Nice blog. I have added a link to it from the Camel articles page (a link collection of Camel related blogs etc.)

Regards

Claus Ibsen
Camel committer

Unknown said...

Great blog. Nicely done. Btw, I'm curious. What you describe is great for 'oneway' operations but have you thought about how you might extend what is described in your blog with Camel, Spring, and JMS to handle asynchronous responses?

Grzegorz S said...

I have a problem running this example, i am getting this exception :
Uncategorized exception occured during JMS processing; nested exception is com.sun.messaging.jms.JMSException: [C4003]: Error occurred on connection creation [localhost:7676]. - cause: java.net.ConnectException: Connection refused: connect


What can be a cause?

Harald Wellmann said...

@Grzegorz: Is your broker running? Can you connect via "telnet localhost 7676"?