22 January 2011

Transparent Asynchronous Remoting

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 Solution with Java EE 6


With Java EE 6, the solution is quite simple, thanks to the @Asynchronous annotation introduced in EJB 3.1. You simply create a remote session bean and add this annotation to any business method or to the entire class. The container will then execute the given methods asynchronously.

Patrick Champion has a complete example in his blog, so there's no need for me to provide any sample code.

When your client is also a Java EE 6 application, you can simply @Inject the service interface and use a @Produces annotation on an @EJB reference somewhere else to direct the client to the appropriate implementation.

In addition, to avoid hardcoding the service URL in your client, you should define a local JNDI alias in your Java EE 6 container for the address of the remote service implementation, so you can move the remote implementation to another host without recompiling your client.

The Solution with Spring 3.0


Spring does not have an out-of-the box solution for this scenario. Spring Remoting provides transparent proxies for remote services, but these proxies are always synchronous. Since Spring 3.0, there annotation support for asynchronous execution, but this only applies to local beans.

However, it is not hard to combine these two features with some glue code to implement a solution for our scenario.



Overview


Spring Remoting requires you to do some plumbing both on the server and client sides. On the server side, you expose your service beans wrapped in a HttpInvokerServiceExporter. On the client side, you need to configure a local proxy for each remote service using an HttpInvokerProxyFactoryBean.

Now this HttpInvokerProxyFactoryBean lets you provide a custom HttpInvokerRequestExecutor, so the idea is to build an AsyncHttpRequestExecutor which delegates the remote invocation to an AsyncHttpRequestWorker annotated with @Async, so that the remote invocation will indeed be executed asynchronously.

Here is an example with two remote services - I'm using two services to make clear which of the beans and helper objects need to be constructed per service and which can be shared. The application contexts will use Java configuration to minimize the amount of XML.



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.greeter.api;

public interface HelloService {

    void sayHello(Person person);
}

package com.blogspot.hwellmann.greeter.api;

public interface GoodbyeService {

    void sayGoodbye(Person person);
}

Note that Spring Remoting uses standard Java serialization for transporting method parameters, so all parameter objects need to be Serializable:

package com.blogspot.hwellmann.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.greeter.server;

import com.blogspot.hwellmann.greeter.api.HelloService;
import com.blogspot.hwellmann.greeter.api.Person;


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 left as an exercise to the reader ;-) 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.greeter.server.GreeterServerSpringConfig"/>

</beans>

package com.blogspot.hwellmann.greeter.server;

import java.io.IOException;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.blogspot.hwellmann.async.HttpServiceExporterFactoryBean;
import com.blogspot.hwellmann.greeter.api.GoodbyeService;
import com.blogspot.hwellmann.greeter.api.HelloService;
import com.sun.net.httpserver.HttpServer;

@Configuration
@SuppressWarnings("restriction")
public class GreeterServerSpringConfig {
    
    
    @Bean
    public HelloService helloService() {
        return new HelloServiceImpl();
    }
    
    @Bean
    public GoodbyeService goodbyeService() {
        return new GoodbyeServiceImpl();
    }
    
    @Bean
    public HttpServer httpServer() throws IOException {
        HttpServiceExporterFactoryBean factory = new HttpServiceExporterFactoryBean();
        factory.setPort(9999);
        
        factory.add(HelloService.class, helloService());
        factory.add(GoodbyeService.class, goodbyeService());

        factory.afterPropertiesSet();
        return factory.getObject();
    }    
}

I'm using a simple standalone HTTP server for this example. The Spring reference manual shows how to configure a dispatcher servlet instead when the services are running in a web container anyway.

The Client


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

package com.blogspot.hwellmann.greeter.client;

import javax.inject.Inject;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import com.blogspot.hwellmann.greeter.api.GoodbyeService;
import com.blogspot.hwellmann.greeter.api.HelloService;
import com.blogspot.hwellmann.greeter.api.Person;

public class GreeterClient implements Runnable {

    @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() {
        System.out.println("invoking greeter");
        
        helloProxy.sayHello(donald);
        helloProxy.sayHello(mickey);
        helloProxy.sayHello(charlie);
        
        goodbyeProxy.sayGoodbye(donald);
        goodbyeProxy.sayGoodbye(mickey);
        goodbyeProxy.sayGoodbye(charlie);

        System.out.println("done");
    }
    
    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = new GenericXmlApplicationContext("/META-INF/spring/client-context.xml");
        GreeterClient client = context.getBean(GreeterClient.class);
        client.run();
    }    
}

And here is the Spring Configuration:

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

  <context:annotation-config  />
  <task:annotation-driven proxy-target-class="true"/>
  <bean class="com.blogspot.hwellmann.greeter.client.GreeterClientSpringConfig"/>

</beans>

Note the <task:annotation-driven> element which is required to enable the @Async annotation.

package com.blogspot.hwellmann.greeter.client;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean;
import org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor;

import com.blogspot.hwellmann.async.AsyncHttpRequestExecutor;
import com.blogspot.hwellmann.async.AsyncHttpRequestWorker;
import com.blogspot.hwellmann.greeter.api.GoodbyeService;
import com.blogspot.hwellmann.greeter.api.HelloService;

@Configuration
public class GreeterClientSpringConfig {
    
    private <T> T createHttpInvokerProxy(Class<T> service, String url) {
        HttpInvokerProxyFactoryBean factory = new HttpInvokerProxyFactoryBean();
        factory.setServiceInterface(service);
        factory.setServiceUrl(url);
        factory.setHttpInvokerRequestExecutor(asyncHttpRequestExecutor());
        factory.afterPropertiesSet();
        return service.cast(factory.getObject());        
    }
    
    private <T> T createHttpInvokerProxy(Class<T> service) {
        String url = "http://remotehost:9999/remoting/" + service.getName();
        return createHttpInvokerProxy(service, url);
    }
    
    @Bean
    public HelloService helloProxy() {
        return createHttpInvokerProxy(HelloService.class);
    }
    
    @Bean
    public GoodbyeService goodbyeProxy() {
        return createHttpInvokerProxy(GoodbyeService.class);
    }
    
    @Bean
    public SimpleHttpInvokerRequestExecutor asyncHttpRequestExecutor() {
        return new AsyncHttpRequestExecutor();
    }
    
    @Bean
    public AsyncHttpRequestWorker asyncWorker() {
        return new AsyncHttpRequestWorker();
    }

    @Bean
    public GreeterClient greeterClient() {
        return new GreeterClient();
    }    
}

You'll probably want to use a PropertyPlaceholderConfigurer to inject the remote host name from a properties file.

The Glue


Here is a helper class reducing the amount of boilerplate code for exporting your services on the server:

package com.blogspot.hwellmann.async;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter;
import org.springframework.remoting.support.SimpleHttpServerFactoryBean;

import com.sun.net.httpserver.HttpHandler;

@SuppressWarnings("restriction")
public class HttpServiceExporterFactoryBean extends SimpleHttpServerFactoryBean {

    public static final String DEFAULT_CONTEXT_ROOT = "/remoting/";
    
    private Map<String, HttpHandler> contexts = new HashMap<String, HttpHandler>();
    
    private String contextRoot = DEFAULT_CONTEXT_ROOT;

    
    public <T> void add(Class<T> klass, T service) {

        SimpleHttpInvokerServiceExporter exporter = new SimpleHttpInvokerServiceExporter();
        exporter.setService(service);
        exporter.setServiceInterface(klass);
        exporter.afterPropertiesSet();

        contexts.put(contextRoot + klass.getName(), exporter);
    }
    
    @Override
    public void afterPropertiesSet() throws IOException {
        setContexts(contexts);
        super.afterPropertiesSet();
    }
    
    public void setContextRoot(String contextRoot) {
        this.contextRoot = contextRoot;
        if (! contextRoot.endsWith("/")) {
            this.contextRoot += "/";
        }
    }
}


This is the AsyncHttpRequestExecutor:

package com.blogspot.hwellmann.async;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

import javax.inject.Inject;

import org.springframework.remoting.httpinvoker.HttpInvokerClientConfiguration;
import org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor;
import org.springframework.remoting.support.RemoteInvocationResult;

public class AsyncHttpRequestExecutor extends SimpleHttpInvokerRequestExecutor {

    @Inject
    private AsyncHttpRequestWorker worker;
    
    @Override
    public RemoteInvocationResult doExecuteRequest(HttpInvokerClientConfiguration config,
            ByteArrayOutputStream baos) throws IOException, ClassNotFoundException {
        worker.executeAsyncRequest(config, baos);
        return new RemoteInvocationResult(null);
    }    
}

And this is the worker which gets proxied by Spring to run asynchronously:
package com.blogspot.hwellmann.async;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

import org.springframework.remoting.httpinvoker.HttpInvokerClientConfiguration;
import org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor;
import org.springframework.scheduling.annotation.Async;

public class AsyncHttpRequestWorker extends SimpleHttpInvokerRequestExecutor {

    @Async
    public void executeAsyncRequest(HttpInvokerClientConfiguration config,
            ByteArrayOutputStream baos) {
        try {
            super.doExecuteRequest(config, baos);
        }
        catch (IOException exc) {
            exc.printStackTrace();
        }
        catch (ClassNotFoundException exc) {
            exc.printStackTrace();
        }
    }
}

That's all. Starting the server and then the client, you'll see the following output from the client...

invoking greeter
done

...and from the server:

Hello, Mickey Mouse!
Bye, Charlie Brown!
Bye, Mickey Mouse!
Hello, Donald Duck!
Hello, Charlie Brown!
Bye, Donald Duck!

The execution order on the server is indeterminate.

Traditional Approaches


In existing software systems, it is not uncommon to see a similar scenario implemented by means of a JMS message queue.

Now there's nothing wrong with JMS, but I think it's simply overkill for our simple remoting scenario, where persistence or multiple subscribers are not required. Besides, JMS does not address transparency, you have to marshall your method parameters into messages and vice versa.

The Lingo project used to provide transparent proxies on top of JMS, but I would not recommend it. Lingo has not been maintained since 2006, the Maven artifacts are unusable due to a broken POM referencing snapshot artifacts, and it has dependencies on outdated Spring versions, so if your own system uses any recent Spring version, you'll have to patch Lingo due to some API changes in Spring.

Apache Camel is sometimes perceived as an up-to-date alternative to Lingo, but in fact it does not provide transparent proxies.

As a sidenote, Javalobby has an interesting discussion thread on Why are Java developers ignorant of JMS and messaging in general?, which is five years old but still worth reading.

Conclusion


Using either Java EE 6 or Spring 3, the standard framework APIs are sufficient to implement transparent asynchronous communication with a remote system. Third-party frameworks or additional systems like a JMS server are not required.

While Java EE 6 has a standard solution for this scenario, there is no direct equivalent in Spring 3. Anybody still claiming that Spring is "simpler than Java EE"?

1 comment:

Tisham Dhar said...

You can also perform transparent remoting with OSGi with either ECF or CXF.