Closed LarsBodewig closed 3 months ago
A lambda expression simply binds a value that is returned from a bootstrap method. If you want to bind a value, you should either intercept the constructor (members) or the initializer (static). From there, construct an expression and bind the result to the field. You could also use MethodDelegation
or Advice
to implement this code.
Thanks for taking the time!
As far as I understand I do intercept the initializer and create an expression by using FieldAccessor.ofField("_writeObject").setsValue()
. If I change my added field to type String
and use setsValue("myString")
the field will be correctly initialized.
I only struggle to represent the method reference as the value.
I am not sure if MethodDelegation
could help, as I don't try to invoke ObjectOutputStream#defaultWriteObject
during initialization but assign the method reference instead. The docs also state
For invoking a method on another instance, use the MethodCall implementation
so I tried to assign a MethodCall
as value
// in MyPlugin.apply
builder = builder
.invokable(net.bytebuddy.matcher.ElementMatchers.isTypeInitializer())
.intercept(
net.bytebuddy.implementation.FieldAccessor.ofField("_writeObject").setsValue(
net.bytebuddy.implementation.MethodCall.of(
new net.bytebuddy.description.method.MethodDescription.ForLoadedMethod(
java.io.ObjectOutputStream.class.getMethod("defaultWriteObject"))
.withArgument(0)));
but it fails with the same exception as before.
You will have to use InvokeDynamic#lambda
to bind a lambda expression (or method reference). Unfortunately, there is not yet a way to set the lambda expression as a field.
Instead, you can construct the lambda expression manually using MethodInvocation
by defining a manual call to LambdaMetaFactory
.
When using MethodCall
, you would also need to use the setsField
method from there.
I tried to manually call the LambdaMetaFactory
which should in theory return a MethodHandle
that can be written to the byte code as a constant but ultimately got stuck on constructing the lambda correctly.
builder = builder.invokable(isTypeInitializer()).intercept(
FieldAccessor.ofField("_writeObject")
.setsValue(
// does not work :(
LambdaMetafactory.metafactory(
MethodHandles.lookup(),
"apply",
MethodType.methodType(WriteInterface.class, ObjectOutputStream.class),
MethodType.methodType(void.class, ObjectOutputStream.class),
MethodHandles.lookup().findVirtual(ObjectOutputStream.class, "defaultWriteObject", MethodType.methodType(void.class)),
MethodType.methodType(void.class)
).getTarget()));
I ended up creating another dynamic type to implement the functional interface and delegate the call to the target method (the "pre-java-8-way"). Then I call its default constructor to set my static field. This does not solve the original question/error but is sufficient in my use case.
// in MyPlugin.apply
DynamicType writeInterface = new net.bytebuddy.ByteBuddy()
.makeInterface()
.name("WriteInterface")
.defineMethod("apply", void.class, Visibility.PUBLIC)
.withParameter(ObjectOutputStream.class, "out")
.throwing(IOException.class)
.withoutCode()
.make();
DynamicType writeDefaultImpl = new ByteBuddy()
.subclass(Object.class)
.name("DefaultImpl")
.implement(writeInterface.getTypeDescription())
.defineMethod("apply", void.class, Visibility.PUBLIC)
.withParameter(ObjectOutputStream.class, "out")
.throwing(IOException.class)
.intercept(MethodCall.invoke(new MethodDescription.ForLoadedMethod(
ObjectOutputStream.class.getMethod("defaultWriteObject"))).onArgument(0))
.make();
builder = builder.require(writeInterface);
builder = builder.require(writeDefaultImpl);
builder = builder.defineField(
"_writeObject",
writeInterface.getTypeDescription(),
Visiblity.PUBLIC,
Ownership.STATIC);
builder = builder.invokable(isTypeInitializer()).intercept(
MethodCall.construct(writeDefaultImpl.getTypeDescription().getDeclaredMethods().filter(isDefaultConstructor()).getOnly())
.setsField(named("_writeObject")));
I try to implement a byte-buddy plugin to introduce a static field with a functional interface type and an initial value into some library classes like this:
should be transformed to
I try to transform the classes at build-time, not using the Agent at runtime. I already found out, that I cannot use a
LoadedTypeInitializer
as they are not persisted in the bytecode and thatImplementation.Composable.setsValue()
can only work with constant values for the same reason. I foundInvokeDynamic.lambda()
that sounds like what I need.So I tried implementing my plugin like this:
But when I try to use my plugin the transformation fails with the following exception:
As far as I can tell, byte-buddy does create another field
public static net.bytebuddy.implementation.InvokeDynamic$WithImplicitType$OfArgument MyClass.fixedFieldValue$...
(I read somewhere that this is by design, so far so good) which resolves to aStackManipulation$Illegal
inReferenceTypeAwareAssigner.assign()
because thesource
-type is not assignable toWriteInterface
.This feels like a bug, but maybe I am just using it the wrong way. So my question is:
How can I set an initial value for a functional interface type on a static field at build-time?
I also tried including the functional interface as
DynamicType
, but got the same result.Any hints are greatly appreciated!