I am trying to create a "custom" setter method for a field with byte buddy. Buddy's own mechanism allows for standard setter/getter methods to be implemented very easily, however, I am looking for an elegant way to extend the setter with some additional logic.
To simplify the example, let's assume we have a class A, which has a method setChanged(String). Goal is to make a sub-class of A, add a field with corresponding access methods. The catch is, that I want to call setChanged("fieldName") from each added setter method.
public void setName(String name)
{
setChanged("name");
this.name = name;
}
For a "normal" setter method, byte byddy implementation would be:
new ByteBuddy()
.subclass(A.class)
.name("B")
.defineField("name", Integer.TYPE, Visibility.PUBLIC)
// args is a ArrayList<Class<?>>
.defineMethod(getSetterName(name), Void.TYPE, args, Visibility.PUBLIC)
.intercept( FieldAccessor.ofField(name) )
Bytecode I am after looks like this:
L0
ALOAD 0 // Loads the this reference onto the operand stack
ILOAD 1 // Loads the integer value of the local variable 1 (first method arg)
PUTFIELD package/B.name : I // stores the value to the field
L1
ALOAD 0
LDC "name"
INVOKEVIRTUAL package/A.setChanged (Ljava/lang/String;)V
RETURN
My question is: is there a way to re-use FieldAccessor in this context?
As of today, you would need to define a custom Instrumentation
to do such a custom job. As pointed out in the comments, you can then use an Instrumentation.Compound
to prepend the new behavior to for example FieldAccessor.ofBeanProperty()
and thus reuse the field accessor code.
In order to add custom code, Byte Buddy knows different abstraction levels:
Instrumentation
: Defines how a method is implemented. An instrumentation is able to define additional fields, methods or static initializer blocks that are required to implement a method. Furthermore, it determines if a method is to be defined for a type at all.ByteCodeAppender
is emitted by an Instrumentation
and determines if a defined method is abstract and a method's byte code if a method is implemented.StackManipulation
is a byte code instruction with a given impact on an operand stack's size. Stack manipulations are composed for implementing a non-abstract method.In order to call a method in byte code, you need to load all arguments (including this
) onto the stack and call the method after placing all these arguments. This can be done as follows:
this
reference onto the stack by MethodVariableAccess.REFERENCE.loadFromIndex(0)
.ByteCodeAppender
as an argument. Using a TextConstant
, a name can then be placed on the stack.MethodInvocation
where the setChanged
method can be extracted from the created instrumented type's TypeDescription
which is given to the Instrumentation
as an argument.Of course, this is not very pretty and it is Byte Buddy's aspiration to hide this byte code level API from a user and to express anything in a DSL or in plain Java. You might therefore be happy to hear that I am currently working with Byte Buddy version 0.4 which comes with some features that you can use for this. For your example, you can implement the custom setter using an extended form of Byte Buddy's Swiss army knife, the MethodDelegation
. A method delegation allows you to implement a method by delegating a call to any Java method using annotations.
Assuming that your beans implement a type:
interface Changeable {
void setChanged(String field);
}
you can intercept a method call using:
class Interceptor {
static void intercept(@This Changeable thiz, @Origin Method method) {
thiz.setChanged(method.getName());
}
}
Using a method delegation, Byte Buddy will always call the interceptor when a method is invoked. The interceptor method is passed arguments that describe the context of a specific interception. Above, the this
reference and the method that is intercepted are passed.
Of course we are still missing the actual setting of the field. However, with Byte Buddy 0.4, you can now create a new Instrumentation
as easy as follows:
MethodDelegation.to(Interceptor.class).andThen(FieldAccessor.ofBeanProperty())
With this delegation, Byte Buddy first calls the intercepor (then drops any potential return value) and finally applies the Instrumentation
that is passed as an argument to the andThen
method.