问题
I'm trying to replace a reflective invocation with a MethodHandle, but varargs seem to be impossible to deal with.
My reflective invoker currently looks like this:
public class Invoker {
private final Method delegate;
public Invoker(Method delegate) {
this.delegate = delegate;
}
public Object execute(Object target, Object[] args) {
return delegate.invoke(target, args);
}
}
My current attempt at rewriting it looks like this (the interface the Invoker
exposes has to stay the same):
public class Invoker {
private final Method delegate;
private final MethodHandle handle;
public Invoker(Method delegate) {
this.delegate = delegate;
this.handle = MethodHandles.lookup().unreflect(delegate);
}
public Object execute(Object target, Object[] args) throws InvocationTargetException, IllegalAccessException {
Object[] allArgs = Stream.concat(Stream.of(target), Stream.of(args)).toArray(Object[]::new);
return handle.invokeWithArguments(allArgs);
}
}
And this works just fine in most cases. But varargs break everything. E.g. have a method like:
public String test(int i, String... args) {
return ...;
}
And the arguments like:
Object[] args = new Object[] {10, new String[] {"aaa", "bbb"}};
And execute
as implemented above will fail. I tried various combinations of asSpreader()
, MethodHandles.explicitCastArguments()
, invoke
instead of invokeWithArguments
etc with no success.
The only way I can invoke a varargs method is to provide the arguments inline and not as an array. E.g.
handle.invokeWithArguments(10, "aaa", "bbb")
but I can not do that and maintain the generic nature of the Invoker
that it currently has.
Is this really impossible to do the way I'm trying?
UPDATE:
After benchmarking various scenarios, I decided to stick to reflection as invokeWithArguments
performs significantly worse in all tested cases.
回答1:
Seems that all you need is one call to .asFixedArity
as by default java will create method handle with asVarargsCollector
public class Main {
public static String test(int i, String... args) { return "works!"; }
public static void main(String[] args) throws Throwable {
Method method = Main.class.getMethod("test", int.class, String[].class);
System.out.println(new Invoker(method).execute(null, new Object[]{1, new String[] {"foo", "bar"} }));
}
public static class Invoker {
private final MethodHandle handle;
public Invoker(final Method delegate) throws Exception {
MethodHandle handle = MethodHandles.lookup().unreflect(delegate);
if (Modifier.isStatic(delegate.getModifiers())) { // for easy static methods support
handle = MethodHandles.dropArguments(handle, 0, Object.class);
}
this.handle = handle.asFixedArity();
}
public Object execute(Object target, Object[] args) throws Throwable {
Object[] allArgs = new Object[args.length + 1];
allArgs[0] = target;
System.arraycopy(args, 0, allArgs, 1, args.length);
return handle.invokeWithArguments(allArgs);
}
}
}
There is also many other possible solutions, like you can add more logic to Invoker constructor (static factory might be a good idea) and use asType
method to prepare signature you want and then you should be able to call this using .invokeExact
to get small performance boost.
You can also just keep using Method
;)
回答2:
I completed your code to reproduce your issue, but it works using invokeWithArguments. Maybe I missed something ?
public class Main {
public String test(int i, String... args) {
return i + Stream.of(args).collect(Collectors.joining());
}
public static void main(String[] args) throws Throwable {
Main main = new Main();
Method method = Main.class.getMethod(
"test",
int.class,
String[].class)
Invoker invoker = new Invoker(method);
assertEquals("1foobar", invoker.execute(main, new Object[]{1, "foo", "bar"})); // Success
}
public static class Invoker {
private final MethodHandle handle;
public Invoker(final Method delegate) throws Exception {
this.handle = MethodHandles.lookup().unreflect(delegate);
}
public Object execute(Object target, Object[] args) throws Throwable {
// Add the target and all arguments in a new array
Object[] allArgs = Stream.concat(Stream.of(new Object[]{target}), Stream.of(args))
.toArray(Object[]::new);
return handle.invokeWithArguments(allArgs);
}
}
}
来源:https://stackoverflow.com/questions/52093694/how-to-invoke-a-methodhandle-with-varargs