4

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?

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
Nikola Veber
  • 73
  • 1
  • 4
  • I’m not sure what’s your actual question. Do you want to know whether you can store the result of `FieldAccessor.ofField(…)` into a variable and use it multiple times? Why don’t you try it? Or what kind of re-use do you mean? – Holger Oct 29 '14 at 14:57
  • Hi Holger, I would like to extend the FieldAccessor.ofField(...) with a method call (in the example above: setChanged("name");). I meant re-use of code, not objects. Basically (this may be misinformed approach), I would like to get the stack manipulation of the FieldAccessor and use it in another compound stack manipulation. – Nikola Veber Oct 30 '14 at 08:23
  • Well, there’s [`Instrumentation.Compound`](http://bytebuddy.net/javadoc/0.3.1/net/bytebuddy/instrumentation/Instrumentation.Compound.html#Compound-net.bytebuddy.instrumentation.Instrumentation...-), but strangely, I didn’t find any way to call another method on `this`… – Holger Oct 30 '14 at 10:01

1 Answers1

4

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:

  1. 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.
  2. A 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.
  3. A 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:

  1. Load the this reference onto the stack by MethodVariableAccess.REFERENCE.loadFromIndex(0).
  2. Load a string onto the stack that describes the field that is access. This can be derived from the method's name which is given to a ByteCodeAppender as an argument. Using a TextConstant, a name can then be placed on the stack.
  3. A method can then be invoked by using a 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.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • Great! That is a very helpful feature addition! It would be great if you could give some resulting bytecode examples in the documentation (for various Byte Buddy "patterns"), to make performance assessment easier. This is a spoiled suggestion, since I could look myself, but it could help with design decisions in performance-critical cases. – Nikola Veber Nov 03 '14 at 10:53
  • I wrote an extensive tutorial but I plan a series of blog postings after release of version 0.5 which allows for a DSL creation if Java agents. Currently, I present Byte Buddy on conferences what takes a lot of time but expect such a series early next year. So long, thanks for being an early adopter. – Rafael Winterhalter Nov 03 '14 at 11:04
  • I have read the tutorial, good work! There are of course some improvement ideas, but all in all, I find it a well written tutorial. Thanks for your efforts, keep it up! :) – Nikola Veber Nov 04 '14 at 09:39
  • Thanks. Feel free to find the tutorial at https://github.com/raphw/byte-buddy/tree/gh-pages and suggest improvements by a pull request, I would be very grateful for your input as I read it with the blind eyes of a library's author. – Rafael Winterhalter Nov 04 '14 at 09:41