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:
You can also perform transparent remoting with OSGi with either ECF or CXF.
Post a Comment