3

On the server-side I have a ListenerManager which fires callbacks to its Listeners. The manager is exported using a Spring RmiServiceExporter

On the client-side I have a proxy to the manager created by an RmiProxyFactoryBean, and a Listener implementation registered through this proxy with the manager on the server side.

So far so good: the ListenerManager is given a Listener and it invokes its callbacks, however since the listener is just a deserialized copy of the client-side object, the callback runs on the server side, not the client side.

How can I get Spring to generate a proxy on the server-side to the client-side listener so that the callback invoked by the server is executed remotely on the client-side? Surely I don't need another (exporter, proxy factory) pair in the opposite direction?

Joe Kearney
  • 7,397
  • 6
  • 34
  • 45

3 Answers3

4

A pure RMI solution: the client-side listener object needs to implement java.rmi.server.UnicastRemoteObject. If it does, and each of its methods throw RemoteException then when it is passed to the server through the manager proxy everything is wired up automatically, and method invocations on the server-side proxy to this listener are remote invocations of methods on the real client-side object.

This will do, but it's even better to be able to wrap the object for export without requiring a particular superclass. We can use a CGLIB Enhancer to "proxy" the listener as a subclass of UnicastRemoteObject that also implements the service interfaces. This still requires that the target object implement java.rmi.Remote and declare throws RemoteException.

Next step is a solution that can export arbitrary objects for remote invocation of their methods, without requiring that they implement Remote or declare throws RemoteException. We must integrate this proxying with the existing Spring infrastructure, which we can do with a new implementation of RmiBasedExporter modelled on the non-registry bits of RmiServiceExporter#prepare() to export the RMI stub of our proxy and on the invocation part of RmiClientInterceptor.doInvoke(MethodInvocation, RmiInvocationHandler). We need to be able to get hold of an exported proxy instance of our service interfaces. We can model this on the means used by Spring to apparently "export" non-RMI interfaces. Spring proxies the interface to generate a RmiInvocationWrapper for invocation of a non-RMI method, serialises the method details and arguments, then invokes this on the far side of the RMI connection.

  • Use a ProxyFactory and an RmiInvocationHandler implementation to proxy the target object.
  • Use a new implementation of RmiBasedExporter to getObjectToExport(), and export it using UnicastRemoteObject#export(obj, 0).
  • For the invocation handler, rmiInvocationHandler.invoke(invocationFactory.createRemoteInvocation(invocation)), with a DefaultRemoteInvocationFactory.
  • Handle exceptions and wrap appropriately to avoid seeing UndeclaredThrowableExceptions.

So, we can use RMI to export arbitrary objects. This means we can use one of these objects on the client-side as a parameter to an RMI method call on an RMI server-side object, and when the deserialised stub on the server-side has methods invoked, those methods will execute on the client-side. Magic.

Joe Kearney
  • 7,397
  • 6
  • 34
  • 45
2

Following Joe Kearney's explaination, I have created my RMIUtil.java. Hope there is nothing left.

BTW, please ref this for "java.rmi.NoSuchObjectException: no such object in table"

btpka3
  • 3,720
  • 2
  • 23
  • 26
  • You may want to update your link to RMIUtil.java to [this location](http://www.programcreek.com/java-api-examples/index.php?source_dir=btpka3.github.com-master/java/spring/rmi/first-spring-rmi/src/main/java/me/test/spring/rmi/pojo/RMIUtil.java)? – 2Aguy Aug 17 '17 at 17:20
0

Just add some code to Joe's answer.

Extends RmiServiceExporter and get access to exported object:

public class RmiServiceExporter extends org.springframework.remoting.rmi.RmiServiceExporter {
    private Object remoteService;
    private String remoteServiceName;

    @Override
    public Remote getObjectToExport() {
        Remote exportedObject = super.getObjectToExport();

        if (getService() instanceof Remote && (
                getServiceInterface() == null || exportedObject.getClass().isAssignableFrom(getServiceInterface()))) {
            this.remoteService = exportedObject;
        }
        else {
            // RMI Invokers. 
            ProxyFactory factory = new ProxyFactory(getServiceInterface(), 
                    new RmiServiceInterceptor((RmiInvocationHandler) exportedObject, remoteServiceName));

            this.remoteService = factory.getProxy();
        }

        return exportedObject;
    }

    public Object getRemoteService()  {
        return remoteService;
    }

    /** 
     * Override to get access to the serviceName
     */
    @Override
    public void setServiceName(String serviceName) {
        this.remoteServiceName = serviceName;
        super.setServiceName(serviceName);
    }
}

The interceptor used in the proxy (the remote service callback):

public class RmiServiceInterceptor extends RemoteInvocationBasedAccessor 
    implements MethodInterceptor, Serializable  {

    private RmiInvocationHandler invocationHandler; 
    private String serviceName;

    public RmiServiceInterceptor(RmiInvocationHandler invocationHandler) {
        this(invocationHandler, null);
    }

    public RmiServiceInterceptor(RmiInvocationHandler invocationHandler, String serviceName) {
        this.invocationHandler = invocationHandler;
        this.serviceName = serviceName;
    }

    /**
     * {@inheritDoc}
     */
    public Object invoke(MethodInvocation invocation) throws Throwable {
        try {
            return invocationHandler.invoke(createRemoteInvocation(invocation));
        }
        catch (RemoteException ex) {
                throw RmiClientInterceptorUtils.convertRmiAccessException(
                    invocation.getMethod(), ex, RmiClientInterceptorUtils.isConnectFailure(ex), 
                    extractServiceUrl());
            }
    }

    /**
     * Try to extract service Url from invationHandler.toString() for exception info
     * @return Service Url
     */
    private String extractServiceUrl() {
        String toParse = invocationHandler.toString();
        String url = "rmi://" + StringUtils.substringBefore(
                StringUtils.substringAfter(toParse, "endpoint:["), "]");

        if (serviceName != null)
            url = StringUtils.substringBefore(url, ":") + "/" + serviceName;

        return url;
    }
}

When exporting the service with this RmiServiceExporter, we cand send a rmi callback with:

someRemoteService.someRemoteMethod(rmiServiceExporter.getRemoteService());
Jose Luis Martin
  • 10,459
  • 1
  • 37
  • 38