Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reflection-based roles #356

Merged
merged 2 commits into from Dec 23, 2017

Conversation

@darkl
Copy link
Contributor

darkl commented Dec 20, 2017

Hi @oberstet,

This pull request contains a sketch for reflection based caller/callee roles. This is pretty cool.

Usage:

Callee proxy:

Declare an interface:

import io.crossbar.autobahn.wamp.reflectionRoles.WampProcedure;

import java.util.concurrent.CompletableFuture;

public interface IMulCalculator {
    @WampProcedure("com.example.mul2")
    CompletableFuture<Integer> mulAsync(int x, int y);

    @WampProcedure("com.example.mul2")
    int mulSync(int x, int y);
}

Use ReflectionServices property of session in order to obtain a proxy, and then call the desired method:

import io.crossbar.autobahn.wamp.Client;
import io.crossbar.autobahn.wamp.Session;
import io.crossbar.autobahn.wamp.interfaces.IInvocationHandler;
import io.crossbar.autobahn.wamp.reflectionRoles.ReflectionServices;
import io.crossbar.autobahn.wamp.reflectionRoles.WampProcedure;
import io.crossbar.autobahn.wamp.serializers.JSONSerializer;
import io.crossbar.autobahn.wamp.types.ExitInfo;
import io.crossbar.autobahn.wamp.types.InvocationDetails;
import io.crossbar.autobahn.wamp.types.InvocationResult;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class Main {

    public static void main(String[] args) {
        Executor executor = Executors.newSingleThreadExecutor();

        Session mySession = new Session();

        mySession.addOnJoinListener((session, sessionDetails) -> {
            System.out.println("Connected!");

            ReflectionServices reflectionServices = mySession.getReflectionServices();

            // Call obtain a callee-proxy.
            IMulCalculator calleeProxy = reflectionServices.getCalleeProxy(IMulCalculator.class);

            // Call "com.example.mul2".
            int sixtyThree = calleeProxy.mulSync(3, 21);
        });

        // finally, provide everything to a Client and connect
        Client client = new Client(mySession, "ws://127.0.0.1:8080/ws", "realm1", executor);

        CompletableFuture<ExitInfo> exitInfoCompletableFuture = client.connect();
    }
}

Callee

Declare a class

import io.crossbar.autobahn.wamp.reflectionRoles.WampProcedure;

public class AddCalculator {
    @WampProcedure("com.example.add2")
    public int add(int x, int y){
        return (x+y);
    }
}

Use ReflectionServices property of service to register all procedures of an instance of this class:

import io.crossbar.autobahn.wamp.Client;
import io.crossbar.autobahn.wamp.Session;
import io.crossbar.autobahn.wamp.interfaces.IInvocationHandler;
import io.crossbar.autobahn.wamp.reflectionRoles.ReflectionServices;
import io.crossbar.autobahn.wamp.reflectionRoles.WampProcedure;
import io.crossbar.autobahn.wamp.serializers.JSONSerializer;
import io.crossbar.autobahn.wamp.types.ExitInfo;
import io.crossbar.autobahn.wamp.types.InvocationDetails;
import io.crossbar.autobahn.wamp.types.InvocationResult;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class Main {

    public static void main(String[] args) {
        Executor executor = Executors.newSingleThreadExecutor();

        Session mySession = new Session();

        mySession.addOnJoinListener((session, sessionDetails) -> {
            System.out.println("Connected!");

            ReflectionServices reflectionServices = mySession.getReflectionServices();

            // Register "com.example.add2"
            reflectionServices.registerCallee(new AddCalculator());
        });


        // finally, provide everything to a Client and connect
        Client client = new Client(mySession, "ws://127.0.0.1:8080/ws", "realm1", executor);

        CompletableFuture<ExitInfo> exitInfoCompletableFuture = client.connect();
    }
}

TODOs

  • I also added the WampException class, which should allow users to throw their own WAMP ERRORs. Unfortunately, the IInvocationHandler interface does not allow methods to throw exceptions, so this is currently unusable.
  • You would probably want to add support for async methods (methods that return a CompletableFuture) for the callee role - I didn't support this, since IInvocationHandler currently only returns InvocationResult.
  • When deserialization fails, or a parameter is missing, you should throw an appropriate WampException indicating which parameter is missing/isn't of the requested type.
  • Deserialization of parameter by names doesn't work by default. This is because Java omits parameter names from compilation by default. This does work when adding the -parameters flag to the Java compiler (this should be done by the application code). See here.

I really hope that this kind of feature will be integrated in AutobahnJava.
Feel free to ask me any question about this.

Elad

@darkl

This comment has been minimized.

Copy link
Contributor Author

darkl commented Dec 20, 2017

Hi @oberstet,
I also added a Reflection based subscriber implementation for completeness.

Usage sample: declare the following classes

import java.util.Arrays;

public class MyClass {
    public int counter;
    public int[] foo;

    @Override
    public String toString() {
        return String.format("{counter: %d, foo:%s}", counter, Arrays.toString(foo));
    }
}
import io.crossbar.autobahn.wamp.reflectionRoles.WampTopic;

public class ComplexSubscriber {
    @WampTopic("com.myapp.topic2")
    public void OnTopic2(int number1, int number2, String c, MyClass d){
        System.out.printf("Got event: number1:%d, number2:%d, c:%s, d: %s%n", number1, number2, c, d);
    }
}

Then call session.registerSubscriber:

import io.crossbar.autobahn.wamp.Client;
import io.crossbar.autobahn.wamp.Session;
import io.crossbar.autobahn.wamp.interfaces.IInvocationHandler;
import io.crossbar.autobahn.wamp.reflectionRoles.ReflectionServices;
import io.crossbar.autobahn.wamp.reflectionRoles.WampProcedure;
import io.crossbar.autobahn.wamp.serializers.JSONSerializer;
import io.crossbar.autobahn.wamp.types.ExitInfo;
import io.crossbar.autobahn.wamp.types.InvocationDetails;
import io.crossbar.autobahn.wamp.types.InvocationResult;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class Main {

    public static void main(String[] args) {
        Executor executor = Executors.newSingleThreadExecutor();

        Session mySession = new Session();

        mySession.addOnJoinListener((session, sessionDetails) -> {
            System.out.println("Connected!");

            ReflectionServices reflectionServices = mySession.getReflectionServices();

            // Subscribe to "com.myapp.topic2"
            reflectionServices.registerSubscriber(new ComplexSubscriber());
        });

        Client client = new Client(mySession, "ws://127.0.0.1:8080/ws", "realm1", executor);

        CompletableFuture<ExitInfo> exitInfoCompletableFuture = client.connect();
    }
}

This sample works with this code. In order to have the parameters c and d filled (and not null), make sure to compile with the "-parameters" flag, otherwise Java won't emit the names of the parameters, and the code will fail to attach them to the corresponding parameters.

Elad.

@oberstet

This comment has been minimized.

Copy link
Member

oberstet commented Dec 20, 2017

@darkl Hi Elad! Awesome. Really cool. This allows for a style that is definitely even more expressive for app code, which is good. Also, thanks a lot for outlining rationale, usage examples, todos and all. @om26er maybe you also have a look over the code as it is quite extensive and see if you have comments from you side (you are much more familiar with the details I guess by now). also you could also check over all what Elad wrote (in particular the todos) and file issues as you see fit. I think this is really worth following up on, and of course then also putting it on the frontpage etc

@oberstet oberstet requested a review from om26er Dec 20, 2017
@om26er

This comment has been minimized.

Copy link
Member

om26er commented Dec 22, 2017

I have been looking into this, the change is mostly isolated, so doesn't really change the existing code (which is a good thing).

One big problem however is that the new code won't work on Android, except for Android 8.0+ aka API level 26[1]. We currently support API level 24 as minimum.

I have spent quite some time trying to figure out if there is a way to get that working on older versions through a third party library, so far no luck. I came across these libraries that could help though.

https://github.com/maxxedev/javapan
https://github.com/paul-hammant/paranamer

@oberstet For now what we can do is to only land that feature for Netty (need to make some changes in gradle config for that). Your call.

[1] https://developer.android.com/reference/java/lang/reflect/Parameter.html


for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String parameterName = parameter.getName();

This comment has been minimized.

Copy link
@om26er

om26er Dec 22, 2017

Member

For Android, the main issue is this API(level 26 only) and of course its use is central to what we are trying to achieve here.

@darkl

This comment has been minimized.

Copy link
Contributor Author

darkl commented Dec 22, 2017

@oberstet

This comment has been minimized.

Copy link
Member

oberstet commented Dec 23, 2017

For now what we can do is to only land that feature for Netty (need to make some changes in gradle config for that). Your call.

@om26er Ok, making it work on Netty only first is cool! We can figure out Android later. So I am going to merge this now - could you pls create a follow up PR with the gradle changes you mentioned? ah, and pls file follow up issues for the remaining todos mentioned by Elad

@oberstet oberstet merged commit 5f528ed into crossbario:master Dec 23, 2017
1 check passed
1 check passed
continuous-integration/travis-ci/pr The Travis CI build passed
Details
@oberstet

This comment has been minimized.

Copy link
Member

oberstet commented Dec 23, 2017

@darkl awesome! thanks for coming around here and contributing=)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.