Multiple RPC Handlers in Module

How would I go about adding multiple RPC handlers to the gateway? The current module I’m working on has a fairly large API and I would like to split it over organize it over multiple classes. Do I have to do some sort of switch statement in the getRPCHandler class of the GatewayHook? I see that the ModuleRPCFactory allows you to request separate interfaces.

You can put your implementations anywhere you want them. The actual RPC implementation returned from the gateway hook can be full of one-liners that delegate to the actual class and method that is appropriate.

The RPC engine does the first “switch” for you with the method name. Nothing prevents you from defining a second level of naming for some of the first level calls.

But I’d have to setup a ton of 1 line methods to do this?

So just to clarify what I’m trying to do - I have a bunch of data classes and service classes doing CRUD operations on the gateway representing some objects in my module and I was hoping I could make a abstract RPC handler like this:

public abstract class RpcHandler<T> {}...

Then I’d like to find a way to have a top level RPC handler class return a reference to that so I can save myself from having to write 1000’s of one liners delegating everything to the correct member service method.

I think the best you’re going to get is the “delegate” pattern.

Imagine you have interfaces IFoo and IBar, and class ModuleRPC.

Implement ModuleRPC like this:

public class ModuleRPC implements IFoo, IBar {

    IFoo fooImpl;
    IBar barImpl;

    @Override
    public void bar() {
        barImpl.bar();
    }

    @Override
    public void foo() {
        fooImpl.foo();
    }

}

but let IntelliJ generate all the delegated methods for you once you’ve added the impl references via Code > Generate... > Delegate Methods....

1 Like

And if you’re allergic to boilerplate, “just” switch to Kotlin :wink:

class ModuleRPC(context: GatewayContext) : IFoo by FooImpl(context), IBar by BarImpl(context)

Strictly speaking, the RPC system just does switching on string identifiers. First is the module ID, where Ignition looks up the module to get your handler. It then looks up the method name (second string identifier) via reflection and hands off the rest of the arguments to that method.

Nothing prevents you from setting up another layer of indirection, into classes of your choice. Or some mixture, where some methods do further indirection and some perform a direct action.

You don’t even have to make interfaces for everything if you simply define methods that accept Object... variants.

This is what I ended up doing actually. I thought I’d post the solution here if anybody is looking for the same thing here in the future.

I made a hash-map of the various RPC service I would like to be exposed then defined a single method in my gateway RPC handler:

    @Override
    public <T> T invoke(Class<?> apiClass, String method, Object... args) {
        var service = serviceMap.get(apiClass);
        Objects.requireNonNull(service, "No service found for class " + apiClass);
        try {
            return ReflectionUtils.invokeMethod(service, method, args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

Then added a method to a reflections utility class to call the method I want:

    @SuppressWarnings("unchecked")
    public static <T> T invokeMethod(Object bean, String methodName, Object... args) throws IllegalAccessException, 
            InvocationTargetException, NoSuchMethodException {
        
        Class<?> beanClass = bean.getClass();
        Class<?>[] argTypes = new Class<?>[args.length];
        for (int i = 0; i < args.length; i++) {
            argTypes[i] = args[i].getClass();
        }

        Method method = beanClass.getMethod(methodName, argTypes);
        
        return (T) method.invoke(bean, args);
    }

I quite liked having a common interface so that I have to fulfill a contract on both ends of the RPC call because I think that will prevent a lot of bugs…

3 Likes