I am aware of the MethodCall#withArgumentArrayElements(int)
method. Briefly, it allows you to accept an Object[]
and invoke some other method, supplying its parameters in order from the Object[]
identified by the int
parameter. Usually you also use a dynamic assigner.
I am finding an odd limitation where this does not appear to work. I'd like to understand what I've done wrong or if there is a (rare) problem in ByteBuddy.
I have generated a static
method using ByteBuddy whose Java code might look like the following:
private static final void setFrob(final T target, final Object... parameters) throws Exception {
target.setFrob((String)parameters[0],
(Integer)parameters[1]); // pseudocode; happens via "spreading" mentioned above
}
The pattern, as I hope you can see, is that in my instrumented class I define a private static
setter method named after the "real" setter method (for various reasons). I take in the target
and the parameters
, whatever they might be, and then invoke the "real" setter method on the target
with the supplied parameters
array supplying values for the "real" setter method's parameters. This is not complicated and works fine, but interestingly only in certain scenarios.
The method definition's ByteBuddy recipe is:
final MethodDescription staticSetterMethod =
new MethodDescription.Latent(builder.toTypeDescription(),
methodToken.getName(),
PRIVATE_STATIC_FINAL_SYNTHETIC_VARARGS_METHOD_MODIFIERS,
Collections.emptyList(),
TypeDescription.Generic.VOID,
List.of(new ParameterDescription.Token(targetType,
"target",
ParameterManifestation.FINAL.getMask()),
new ParameterDescription.Token(OBJECT_ARRAY_TYPE_DESCRIPTION_GENERIC,
"parameters",
ParameterManifestation.FINAL.getMask())),
Collections.singletonList(EXCEPTION_TYPE_GENERIC),
Collections.emptyList(),
null,
null);
builder = builder
.define(staticSetterMethod)
Because things work in certain scenarios, I've been able to javap
the resulting class and the method definition is as I would expect. I'm not worried about this part.
Next, the ByteBuddy implementation recipe looks like this (I've tried to keep it short and relevant):
MethodCall.invoke(new MethodDescription.Latent(targetType.asErasure(), // T's actual type
methodToken)) // setFrob(String, Integer)
.onArgument(0) // target (of type T)
.withArgumentArrayElements(1) // parameters
.withAssigner(Assigner.DEFAULT, Assigner.Typing.DYNAMIC));
…so: "invoke setFrob
, declared by the type of target
, described by methodToken
, on the first argument (target
) spreading the incoming Object[]
array values found in the second argument (parameters
) using dynamic assignment".
My understanding is that if the "real" setFrob
method needs a String
and an Integer
supplied to it, then if the Object[]
here is two elements long, and if the first element is a String
, and the second is an Integer
, then the withArgumentArrayElements(1)
invocation will ensure that these elements are "spread" "into" the proper method parameters.
Indeed this works fine when the setFrob
method being called accepts zero or one parameters (let's say it is defined to accept only a String
parameter). That tells me that at least my ByteBuddy recipes are correct.
However, I was extremely surprised to note that it fails when the setFrob
method being called is changed to accept two parameters. The partial stack says:
java.lang.IllegalStateException: public void com.foo.bar.TestExplorations$Foo.setFrob(java.lang.String,java.lang.Integer) does not accept 1 arguments
at net.bytebuddy.implementation.MethodCall$Appender.toStackManipulation(MethodCall.java:3539)
at net.bytebuddy.implementation.MethodCall$Appender.apply(MethodCall.java:3508)
at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyCode(TypeWriter.java:708)
at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyBody(TypeWriter.java:693)
at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod.apply(TypeWriter.java:600)
at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForCreation.create(TypeWriter.java:5660)
at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:2166)
at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:232)
at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:204)
The error is resulting from this conditional:
ParameterList<?> parameters = invokedMethod.getParameters();
if (parameters.size() != argumentLoaders.size()) {
throw new IllegalStateException(invokedMethod + " does not accept " + argumentLoaders.size() + " arguments");
}
The argumentLoaders
in this case has a size of 1
. parameters
has a size of 2
.
What am I doing wrong?
Maintainer here: It's indeed a bug, the increment was repeated inside the loop. This will be fixed in version 1.10.15.