Anton Kudruk
Anton Kudruk

Reputation: 45

Initiating fields in constructors with Byte Buddy interceptors

How can I initiate object fields in the constructor interceptor?

I created a constructor with Byte Buddy like in the following code.

Class<?> klass = new ByteBuddy()
            .subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)

            .defineProperty("origin", CoreObject.class, true)
            .defineProperty("variableNamedField", Map.class, true)

            .defineConstructor(Visibility.PUBLIC)
            .withParameters(CoreObject.class)
            .intercept(
                    // Invoke Objects default constructor explicitly
                    MethodCall.invoke(Object.class.getConstructor())
                            .andThen(FieldAccessor.ofField("origin").setsArgumentAt(0))
                            .andThen(FieldAccessor.ofField("variableNamedField").setsValue(new HashMap<>()))
                            .andThen(MethodDelegation.to(new FillMapInterceptor("variableNamedField")))

                    //
                    // I have to fill the map.
                    // Something like this:
                    //
                    // variableNamedField.put("first", new FirstHandler(origin));
                    // variableNamedField.put("second", new SecondHandler(origin));
                    //

            )
            .make()
            .load(CoreObject.class.getClassLoader())
            .getLoaded();

First the constructor saves the parameter to the private field. Then it creates the collection. Then it calls the following interceptor to fill that collection.

class FillMapInterceptor {

    private final String mapField;

    public FillMapInterceptor(String mapField) {
        this.mapField = mapField;
    }

    public void construct(@FieldValue("variableNamedField") Map<String, Handler> map, @FieldValue("origin") CoreObject coreObject){
        map.put("first", new FirstHandler(coreObject));
        map.put("second", new SecondHandler(coreObject));
    }
}

It's better to instantiate the field variableNamedField in the interceptor because it turns out that variableNamedField fields are instantiated with the same HashMap object each time a new class instance in created. However, I can only pass the existing field to the interceptor via @FieldValue annotation. But it seems that I can't assign the field with new variable in the interceptor.

  1. How to initiate class fields in the constructor interceptor? I can only pess an existing field value with @FieldValue in the interceptor.
  2. How to make the field name arbitary? Annotations like @FieldValue require constant parameters (e. g. field names). Maybe, I can somehow take the field from defineProperty method result and pass it to the interceptor or use as an interceptor's field?
  3. How can I pass constructor parameters directly into the interceptor? I don't like the way to initiate the field first and then pass the field to the interceptor via @FieldValue annotation.

Upvotes: 1

Views: 1098

Answers (1)

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44032

The easiest way to do this would be to define the constructor using an Advice class. An advice class allows you to define code using a template that is later inlined. You would use @Advice.OnMethodExit in this case to add the code after the above Implementation and then wrap around the code you already created above:

Advice.to(YourAdvice.class).wrap(...)

You find all information about defining advice in the javadoc by basically, you can copy paste your code above into a static method to inline it:

class YourAdvice {
  @Advice.OnMethodExit
  static void exit(@Advice.FieldValue("variableNamedField") 
        Map<Object, Object> variableNamedField) {
    variableNamedField.put("first", new FirstHandler(origin));
    variableNamedField.put("second", new SecondHandler(origin));
  }
}

Upvotes: 1

Related Questions