Search code examples
javabyte-buddy

Setting up @FieldProxy field dynamically in Byte Buddy


I have to implement an interceptor that can be used for dynamically specified fields regardless of the field name.

On the comment for the answer here https://stackoverflow.com/a/35113359/11390192 I've read

you can really just use reflection on a @This object. As long as you cache the Field instances, this has no relevance to performance.

However I doubt the following interceptor implementation is an effecient one (if I understood the comment right).

public static class DynamicFieldInterceptor {
    private final String fieldName;

    public DynamicFieldInterceptor(String fieldName) {
        this.fieldName = fieldName;
    }

    public void intercept(@This Object thiz) throws NoSuchFieldException, IllegalAccessException {
        Field field = thiz.getClass().getDeclaredField(fieldName);
        boolean oldAccessible = field.isAccessible();
        field.setAccessible(true);
        Long fieldValue = (Long)field.get(thiz);
        field.set(thiz, fieldValue + 1L);       // !< Instead of my business logic
        field.setAccessible(oldAccessible);
    }
}

I've also tried the following idea: to generate interceptor classes for each field with the different annotations on the @FieldProxy argument. Than use the generated class as an interceptor to the target class.

public interface Metaclass {
    void intercept(GetterAndSetter field);
}

public static class MetaclassInterceptor implements Metaclass{
    @Override
    public void intercept(GetterAndSetter field) {
        field.set((Long)field.get() + 1L);
    }
}

public static Class<?> annotateInterceptorClass(final String annotation)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    return new ByteBuddy()
            .subclass(MetaclassInterceptor.class)
            .topLevelType()
            .name("ClassForIntercepting_" + annotation + "_Field")
            .modifiers(Visibility.PUBLIC, Ownership.STATIC)

            .defineMethod("intercept", void.class, Visibility.PUBLIC)
            .withParameter(GetterAndSetter.class, "intercept")
            .annotateParameter(AnnotationDescription.Builder.ofType(FieldProxy.class)
                    .define("value", annotation).build())

            .intercept(SuperMethodCall.INSTANCE)
            .make()
            .load(MetaclassInterceptor.class.getClassLoader())
            .getLoaded();
}

The class seems to be generated well. The method in the generated class exists and the parameter is annotated with the expected annotation.

However when I tried to use the generated class as an interceptor, I've got an exception.

Class<?> klass = new ByteBuddy()
            .subclass(Object.class)

            .defineProperty("index0", Long.class, false)
            .defineProperty("index1", Long.class, false)

            .defineMethod("doSomeActions", void.class, Visibility.PUBLIC)
            .intercept(
                    MethodDelegation
                            .withDefaultConfiguration()
                            .withBinders(FieldProxy.Binder.install(GetterAndSetter.class))

                            // Use dynamically generated interceptor, see abode
                            .to(annotateInterceptor("index0"))
                    .andThen(
                            MethodDelegation
                                    .withDefaultConfiguration()
                                    .withBinders(FieldProxy.Binder.install(GetterAndSetter.class))

                                    // Use dynamically generated interceptor, see abode
                                    .to(annotateInterceptor("index1"))
                    )

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

Exception in thread "main" java.lang.NoClassDefFoundError: LClassForIntercepting_index0_Field;
    at java.base/java.lang.Class.getDeclaredFields0(Native Method)
    at java.base/java.lang.Class.privateGetDeclaredFields(Class.java:3062)
    at java.base/java.lang.Class.getDeclaredField(Class.java:2410)
    at net.bytebuddy.implementation.LoadedTypeInitializer$ForStaticField.onLoad(LoadedTypeInitializer.java:120)
    at net.bytebuddy.implementation.LoadedTypeInitializer$Compound.onLoad(LoadedTypeInitializer.java:187)
    at net.bytebuddy.dynamic.TypeResolutionStrategy$Passive.initialize(TypeResolutionStrategy.java:102)
    at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:5662)
    at net.bytebuddy.dynamic.DynamicType$Default$Unloaded.load(DynamicType.java:5651)
    at MainClass4.main(MainClass4.java:107)

Even if I succeeded with dynamic implementation of interceptors, I'd be sure that it's not the perfect way. I think it has to be the possibility to make it in the easier way. Really, @FieldProxy annotation can get the field from both explicitly specified name and bean property if the field name in the annotation is not specified, so I think it is the technical opportunity to map it to any other field.


Solution

  • When you load a class using load(MetaclassInterceptor.class.getClassLoader()), you are creating a new class loader that does not become visible to any other classes on other loaders unless you reuse it.

    You can:

    a) Combine the two DynamicTypes that are created by the make step and load them together. This way, they will end up in the same class loader.

    b) Take the class loader of the first generated class and cast it to an InjectionClassLoader. You will also need to specify the ClassLoadingStrategy.WRAPPER.opened() and use it together with InjectionClassLoader.Strategy.INSTANCE. Note that this will allow anybody with a reference to an instance of your generated class to define classes in the same package.

    c) Use ClassLoadingStrategy.Default.INJECTION what defines classes in the original class loader without creating a wrapper. Not that this strategy relies on Unsafe API.