Reputation: 616
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
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
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