If I export a service via RMI with RmiServiceExporter
, the return values of the service calls are serialized and sent over the wire to the client. But unless I have a simple service that returns simply data, this is not very useful.
I want to be able to call methods on this return value as well, and have them run on the server, not on the client. Essentially, I want a proxy to the return value to created on the client. Is there any way to do this with RmiServiceExporter
(or another remoting option supported by Spring)?
I tried to implement some auto wrapping of return values, and this is what I came up with:
On the server side:
class RmiReturnValueStubbingExporter extends RmiServiceExporter {
/* we can't use the Spring implementation of RmiInvocationHandler
* because it's package protected */
private class RmiReturnValueInvocationWrapper implements RmiInvocationHandler {
private final Object wrappedObject;
private RmiReturnValueInvocationWrapper(Class interf, Object wrappedObject) {
this.wrappedObject = createProxyFor(interf, wrappedObject);
}
@Override
public String getTargetInterfaceName() throws RemoteException {
return null;
}
@Override
public Object invoke(RemoteInvocation invocation) throws
RemoteException, NoSuchMethodException, IllegalAccessException,
InvocationTargetException {
return RmiReturnValueStubbingExporter.this.invoke(
invocation, this.wrappedObject);
}
}
@Override
protected Object invoke(RemoteInvocation invocation, Object targetObject)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Object ret = maybeWrap(super.invoke(invocation, targetObject));
if (ret instanceof Remote) {
try {
UnicastRemoteObject.exportObject((Remote) ret, 0);
} catch (RemoteException e) {
throw new InvocationTargetException(e);
}
}
return ret;
}
private Object maybeWrap(Object superValue) {
if (superValue instanceof OntologyTerm) {
return new RmiReturnValueInvocationWrapper(
OntologyTerm.class, superValue);
} else if (superValue instanceof List) {
return new RmiReturnValueInvocationWrapper(
List.class, superValue);
} else {
return superValue;
}
}
private Object createProxyFor(Class interf, Object object) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addInterface(interf);
proxyFactory.addAdvice(new RemoteInvocationTraceInterceptor(getExporterName()));
proxyFactory.setTarget(object);
// don't make opaque, on the client we need to know the interfaces implemented
return proxyFactory.getProxy(getBeanClassLoader());
}
}
and on the client side:
class RmiStubReturnValueProxyFactoryBean extends RmiProxyFactoryBean {
@Override
protected Object doInvoke(MethodInvocation methodInvocation,
RmiInvocationHandler invocationHandler) {
def superValue = super.doInvoke(methodInvocation, invocationHandler)
if (superValue instanceof java.lang.reflect.Proxy &&
java.lang.reflect.Proxy.getInvocationHandler(superValue)
instanceof RemoteObjectInvocationHandler) {
RmiInvocationHandler rih = superValue
def proxiedInterfaces = rih.invoke(
new RemoteInvocation('getProxiedInterfaces',
[] as Class[], [] as Object[]))
def clientProxy = new ProxyFactory(proxiedInterfaces)
clientProxy.addAdvice({ MethodInvocation invoc ->
doInvoke(invoc, rih)
} as MethodInterceptor)
clientProxy.getProxy beanClassLoader
} else {
superValue
}
}
}
This seems to work fine with two levels of wrapping; in this case the service returning a List<OntologyTerm>
. I suspect this has quite some problems, first of all the call UnicastRemoteObject.exportObject
without ever reclaiming anything. Another things that's maybe also missing here is handling the passing of these stubs the clients gets as arguments to the services. Because the arguments are Proxy
objects created on the client (wrapping the RMI stub), I guess that at the very least the server will have to load classes from the client, which is something I'd like to avoid.
My question is if there's something like this already implemented (not necessarily with RMI), where I can easily specify what types are returned as a value and serialized and which types of return values should be stubbed.
This is an ancient problem that originated technologies like CORBA, Jini and the first versions of EJB. This idea is mostly abandoned as it simply does not scale well.
A remote object cannot be treated transparently as a local object due to the network overhead. Due to the network latency, when calling a remote object you want to pack the most data in a remote call, while in a local object you prefer to keep the method calls the most fine-grained possible, so that they can be the most reusable from a OO point of view.
These two requirements of method reusability and low network overhead conflict and the end result is that the API of a remote object is always going to be very different than the API of a local object, even if the technology existed to make calls to remote objects completelly transparent.
The solution that is nowadays widelly adopted is to send DTOs (Data Transfer Objects) over the wire, which are data only objects. This can be done in RMI, HTTP (REST), SOAP or other protocols.
Then at each end some mapping can be made to the domain model, so that the service and persistence layer can be written in it. The DTO model on the other hand is not meant to cross the presentation layer.