Minato Namikaze
Minato Namikaze

Reputation: 616

Java pass a string variable as a function in a callback

How can I pass a method class variable in a callback?

I have following code:

PostsController postController = new PostsController();
router.get("/").handler(postController::index);

But my controller will be dynamic, how can I pass dynamic class and its function. I tried following code

Class controller = Class.forName("some.package"+controllerName); //PostsController
Method[] methods = controller.getMethods();
//some loop
Method m = methods[i]; // This is the index method of my controller
router.get(somePath).handler(m); // How can I pass my method m as callback?

PostsController

public class PostsController {

    @Route(method="get", path = "/")
    public void index(RoutingContext routingContext){
        routingContext.response()
                .putHeader("content-type", "application/json; charset=utf-8")
                .end(Json.encodePrettily("{}"));
    }

}

Upvotes: 1

Views: 1306

Answers (2)

Holger
Holger

Reputation: 298203

The cleanest solution would be to use an interface defining the common functionality, e.g.

public interface Controller {
    void index(RoutingContext routingContext);
}
public class PostsController implements Controller {
    @Route(method="get", path = "/")
    public void index(RoutingContext routingContext){
        routingContext.response()
                .putHeader("content-type", "application/json; charset=utf-8")
                .end(Json.encodePrettily("{}"));
    }
}

 

Class<? extends Controller> controllerType =
    Class.forName("some.package"+controllers.getString(i))
         .asSubclass(Controller.class);
Controller controller=controllerType.newInstance();
Handler<RoutingContext> h=controller::index;

Of course, this doesn’t interact with the annotation based processing. A dynamic solution would look like

Class<?> controllerType = Class.forName("some.package"+controllers.getString(i));
MethodHandles.Lookup lookup=MethodHandles.lookup();
Method[] methods = controllerType.getMethods();
Object instance = null;
for (Method m : methods) {
    Route annotation = m.getAnnotation(Route.class);
    if (annotation != null) {
        if(instance == null) instance = controllerType.newInstance();
        Handler<RoutingContext> handler
            =createHandler(lookup, instance, m, RoutingContext.class);
        router.get(annotation.path()).handler(handler);
    }
}

 

static <T> Handler<T> createHandler(MethodHandles.Lookup lookup,
        Object instance, Method m, Class<T> arg) throws Throwable {

    MethodHandle mh=lookup.unreflect(m);
    MethodType t=mh.type();
    Class<?> receiver=t.parameterType(0);
    if(t.parameterCount()!=2
    || !receiver.isAssignableFrom(instance.getClass())
    || !t.parameterType(1).isAssignableFrom(arg))
      throw new IllegalArgumentException(m+" not suitable for Handler<"+arg.getName()+'>');
    t=t.dropParameterTypes(0, 1);
    return (Handler)LambdaMetafactory.metafactory(lookup, "accept",
        MethodType.methodType(Handler.class, receiver),
        t.changeParameterType(0, Object.class), mh, t)
        .getTarget().invoke(instance);
}

This works under certain assumption about your Handler interface which you didn’t specify. If it was defined like, interface Handler<T> extends Consumer<T> {}, it will work out of the box. Otherwise, you have to adapt "accept" to the actual name of your functional interface’s function method. And if the type parameter has a bound, you’ll have to change the t.changeParameterType(0, Object.class) line to use the actual bound (the Handler’s parameter type after type erasure).

Generally, this requires great care as you don’t have immediate error feedback. In the worst case, errors will only be detected when the call back actually is invoked (with certain arguments).

In principle, you could also just create a lambda expression encapsulating a Reflective invocation of the method, i.e.

Handler<RoutingContext> handler=rc -> { try {
    m.invoke(capturedInstance, rc);
} catch(ReflectiveOperationException ex) { ex.printStackTrace(); }};

but this doesn’t change the fact that you will be detecting errors only very late…

Upvotes: 2

J.Mengelle
J.Mengelle

Reputation: 341

First thing i can see, is that you need to create an instance of your class.

Class controller = Class.forName("some.package"+controllers.getString(i));
Object myInstance = controller.newInstance();

Then you get your Method from it, and call it :

Method myMethod = ...
myMethod.invoke(myInstance, null);  // or with some params

If you only need to call "index", you could also make your controller implement a common interface, and then :

MyInterface myInterface = (MyInterface) myInstance;
myInterface.index();

Hope that helped.

Upvotes: 0

Related Questions