Skip to content

Advanced RMI

Thorben Kuck edited this page Dec 17, 2018 · 12 revisions

This Framework enables you to use a generic Remote Method Invocation (RMI).

The advantages over the Java RMI is, that NetCom2 utilizes Proxys (therefor is generic) and its own communication mechanism (therefor is easily changeable back to a reactive style). Changing the serialization, changing from RMI to reactive style and even cutting the RMI out completely is way more easy than if you where using the Java RMI. Even the Encryption is applied to the NetCom2-RMI. Even further, those RemoteObjects do allow you, to define fallback objects, if the Object is not registered at the Server or if the Connection is terminated.

Most important however is, that the design of this RMI allows for other languages to hook up to this RMI. An Example with PHP and Javascript had been done, but quick and dirty. In the (hopefully near) future, i will add examples for that and eventually create NetCom2 for other languages.

This RMI is always using the DefaultConnection. It is planed to make it interchangeable in the near future.

Lets say, we want the Client to execute an interface-method, that he knows the signature of, but not the implementation. Let's call it TestInterface:

public interface TestInterface {
    String getHelloWorld();
}

Before we can do anything, we must add the rmi-module to our project. In this example, we use maven and add the following to the dependencies tag:

<dependency>
  <groupId>com.github.thorbenkuck</groupId>
  <artifactId>NetCom2-RMI</artifactId>
  <version>1.0</version>
</dependency>

Now, we want to define what the happens at the execution of TestInterface#getHelloWorld without specifically telling the Client what it does. For that, we request an RemoteObject at the Client-side like this:

class ExampleClient {
    public static void main(String[] args) throws Exception {
        ClientStart clientStart = ClientStart.at(4444);
        clientStart.launch();

        RemoteObjectFactory factory = RemoteObjectFactory.open(clientStart);
        TestInterface test = factory.getRemoteObject(Test.class);
    }
}

This would allow you, to now execute any method that the Interface TestInterface has. But the twist is, that the Result will be computed at the Server-side. In the next Stepp we now have to say what should happen, if the getHelloWorld() method is called. For that, we have to register this Object at the Server-side like this:

class ExampleServer {

    private static TestInterface testObject = new TestImpl();

    public static void main(String[] args) throws Exception {
        ServerStart serverStart = ServerStart.at("localhost", 4444);
        RemoteObjectRegistration registration = RemoteObjectRegistration.open(serverStart);
        registration.register(testObject, TestInterface.class);
        serverStart.launch();
        serverStart.acceptAllNextClients();
    }

    private class TestImpl implements TestInterface {
        @Override
        public String getHelloWorld() {
            return "Hello World!";
        }
    }
}

We now have defined what should happen if TestInterface#getHelloWorld method is called, but note, that the TestImpl is a private, inner class of the ExampleServer. The Client has no idea, what the execution of this Method will do. All it knows is, that there is a method, that may be called. So if we expand the ExampleClient like this:

class ExampleClient {
    public static void main(String[] args) throws Exception {
        ClientStart clientStart = ClientStart.at(4444);
        clientStart.launch();

        RemoteObjectFactory factory = RemoteObjectFactory.open(clientStart);
        TestInterface test = factory.getRemoteObject(Test.class);
        System.out.println(test.getHelloWorld());
    }
}

We will see the output

Hello World

This means, if we change the behavior of the private inner class TestImpl at the ServerExample, we change the result that the Client gets, without the need to change the signature of the Interface, or of the Implementation and therefor recompiling the Client/Server.

By Default, any thrown Exception is passed to the Client. This means, the Stacktrace of those Exceptions is the Stacktrace of the Server! To prohibit this, take a look at the provided Annotations.


Registering

For registering Objects at the Server-Side, there are multiple possibilities. Following is an example, utilizing all of those possibilities and an explanation afterwards.

ServerStart serverStart = ...
RemoteObjectRegistration registration = serverStart.remoteObjects();

ServerFooImpl impl = new ServerFooImpl();

registration.register(impl); // 1
registration.register(impl, Foo.class); // 2
registration.hook(impl); // 3

1: registers an Object by simply stating register(impl). The result is, that the ServerFooImpl is registered as the ServerFooImpl and only reachable if the Client requests the ServerFooImpl. In most cases, this is not what you want.

2: registers an Object by stating register(Object, Class...). This will result in the Object beeing registered as all of the Classes provided and it will be executed, if any of those classes are requested by the Client.

Note: This allows you to state any Object followed by any class. This is done, because of possible heap pollution problems with generic types. The procedure does a assignable check at Runtime, so do not state just any class as the registered type.

3: registers an Object by stating hook(Object). This means, the provided Object will be registered by its class, its super class and all of its implementing Interfaces.


Fallbacks

The basic idea of an RMI is, that whatever is using the RemoteObject does not know about the Network and just wants to do some work. If you use the Java-RMI, an Exception is thrown, if the RemoteObject is not registered. NetCom2 does not. NetCom2 allows you to define, what a RemotObject should do, if the Remote-Implementation of that Object is not reachable for any reason.

By default, the RemoteObject throws an RemoteObjectNotRegisteredException, if the RemoteImplementation is not reachable.

You may define one of 2 Fallback-Methods:

Fallback-Instance.

A Fallback-Instance is a local instance, that should be asked, if the Remote-Instance is not reachable.

Fallback-Runnable

A Fallback-Runnable is a simple Runnable, that is execute, if the Remote-Instance is not reachable. If however, a Fallback-Instance is set, this Runnable will not be executed.

Note: Since a Runnable has no return value, the RemoteObject will always return null if asked to compute anything an only provided a fallback runnable.

Setting a Fallback

For setting a Fallback, you have to tell the RemoteObjectFactory what fallback to use.

So, first of, you may set a Default fallback (which is a runnable) like this:

ClientStart clientStart = ...
RemoteObjectFactory fatory = RemoteObjectFactory.open(clientStart);
fatory.setDefaultFallback(() -> {
    // what to do, if the RemoteImplementation is not reachable
});

This Runnable is now the default Fallback for any RemoteObject. If you want to handle every type of RemoteObject differently, you should State:

ClientStart clientStart = ...
RemoteObjectFactory fatory = RemoteObjectFactory.open(clientStart);
factory.setFallback(Foo.class, () -> {
    // what to do, if the RemoteImplementation is not reachable
});

If such a fallback is set, the default Fallback will be ignored. Now, that you set a Fallback for a specific type, you may provide a "local" instance. This local instance is like the runnable asked to do something, if the RemoteImplementation is not reachable. You may define local instances like this:

ClientStart clientStart = ...
RemoteObjectFactory fatory = RemoteObjectFactory.open(clientStart);
factory.setFallbackInstance(Foo.class, new LocalFooInstance());

Once provided, the RemoteObjectFactory will inject this instance into the FooRemoteObject and ignore both the defaultFallback, as well as our Fallback runnable. Using local instances has a hughe advantage, because you may cache last computed results and use them once the RemoteImplementation is not reachable. This means your code will work just like before, without breaking because null is returned.

If you have a RemoteObject, that is speciall and should not use any previously provided Fallbacks, you may inject either a Runnable fallback or an Instance fallback at the Creation of this RemoteObject like this:

ClientStart clientStart = ...
RemoteObjectFactory fatory = RemoteObjectFactory.open(clientStart);
factory.create(Foo.class, new LocalFooInstance());
factory.create(RemoteTestInterface.class, (Runnable) () -> {
    // what to do, if the RemoteImplementation is not reachable
});

This now will override whatever has been set beforehand.


Annotations

Annotations, that are processed to help you with small things, are:

IgnoreRemoteExceptions

This Annotation is used at the RemoteObjectDefinition (i.e. The Shared instance that the Client requests). It may be placed either at a Method or a Class definition, so that if the Server encounters any Exception, nothing is passed to the Client. If it is placed at the Class declartion, any Exception thrown at any Method is ignored. The only exception to this is the RemoteObjectNotRegisteredException, which cannot be ignored. Example:

public interface Foo {
    @IgnoreRemoteExceptions
    public void work();
}

This will ignore any Exception, that is encountered on the Server. If you want to allow certain Exceptions to be passed from the Server to the Client, you may provide exceptTypes in the Annotation like this:

public interface Foo {
    @IgnoreRemoteExceptions(exceptTypes = {FirstException.class, SecondException.class, ...})
    public void work();
}

SingletonRemoteObject

This Annotation is used at the RemoteObjectDefinition (i.e. The Shared instance that the Client requests). This Annotations signals the RemoteObjectFactory that a maximum of one instance of the RemoteObject-Type should exist at all times. Example:

@SingletonRemoteObject
public interface Foo {
    public void work();
}

With this annotation, the following would be true:

ClientStart clientStart = ...
RemoteObjectFactory fatory = RemoteObjectFactory.open(clientStart);
Foo foo = factory.getRemoteObject(Foo.class);
Foo foo2 = factory.getRemoteObject(Foo.class);

System.out.println(foo.equals(foo2));

RegistrationOverrideProhibited

This Annotation is used at the RemoteImplementation (i.e. the Object, that is registered at the Server-side).

If this annotation is set the the Server-Instance of the requested RemoteObject type, the RemoteObjectRegistration will ignore other instances for the same type. Example:

@RegistrationOverrideProhibited
public class ServerFooImpl implements Foo {
    @Override
    public void work() {
        // Compute something
    }
}

So any Object, that would override this instance at ServerStart#remoteObjects will be ignored.

ServerStart serverStart = ...
RemoteObjectRegistration registration = RemoteObjectRegistration.open(serverStart);
registration.register(new ServerFooImpl(), Foo.class);

Note: If you are using hook, this might be an unwanted behavior. Be careful where to use hook.


Communication Protocol

Other than the Java-RMI, this RMI might be used with any Programming language that supports an OOP-design.

The Method, that is executed is provided as a String. The Arguments are provided as Objects and the type is provided as an Class. Therefor, if you override the SerializationAdapter to serialize the RemoteAccessCommunicationRequest, you may create a Hook, that allows NetCom2 to communicate with other languages.

Example follows in the far future. I have to study in the mean time, sorry.


Current State and possible changes

At the current point in time, ClientStart#getRemoteObject will always create this proxy, whether or not the implementation is registered at the Server-side or not. An RemoteException will be thrown, if any method of the Proxy is called and the real Object is not registered at the ServerSide.

This behavior is implemented, so that in the future, an error-handling my be implemented and the real instances at the Server-side may be interchanged or completely removed at Runtime, without fiddling with the ClientStart.

Planned is also, that those Proxies may be created with a Fallback. Like "If the Client has no registration for that Object, do this". This might look like the following:

ClientStart clientStart = ...
// Same behaviour as today
clientStart.getRemoteObject(Foo.class).now();
// Define what to do, if the Server does not have the Object
clientStart.getRemoteObject(Foo.class).withFallback(() -> someOtherMethod());

or it might change to a factory like behaviour:

ClientStart clientStart = ...
// Define what to do, if the Server does not have the Object
clientStart.remoteObjectFactory().addFallback(Foo.class, () -> someOtherMethod());
// Same behaviour as today
clientStart.remoteObjectFactory().create(Foo.class);

This is up to discuss. Feel free to submit us your suggestion, what is better and why.

Also, if you have a completely new Idea, feel free to suggest it. Make sure, that you provide an code-example how it should look, explain what it does and say, why it is better the way you imagine it.

A mix between the two has been implemented with v.0.4.1. Hence the RemoteObjectFactory allows for multiple fallback types. The reason is, that those remote-objects should work even if the RemoteInstance is not reachable. If the connection to the Server is lost for example, you may use a fallback instance, until the connection is re established.

As of Version 2.0, the RemoteObjectRegistration and the RemoteObjectFactory have been extracted into custom Modules

Clone this wiki locally