2

I'm trying to create a proxy with ByteBuddy that can delegate calls of a protected method getRawId of a MyEntityA class to the same method of an object of the same class referenced in a target field.

package it.mict.lab.bytebuddy.entity;

public class MyEntityA {

    private int id;
    
    public MyEntityA() {
    }
    
    public MyEntityA(int id) {
        this.id = id;
    }
    
    protected int getRawId() {
        return id;
    }
}

The proxy should do something similar to this MyEntityB class:

package it.mict.lab.bytebuddy.entity;

public class MyEntityB extends MyEntityA {

    private MyEntityA _target;
    
    public MyEntityB(MyEntityA _target) {
        this._target = _target;
    }
    
    public void hello() {
        System.out.println(_target.getRawId());
    }
}

And this is an example of what I would achieve:

package it.mict.lab.bytebuddy;


import java.lang.reflect.Constructor;

import it.mict.lab.bytebuddy.entity.MyEntityA;
import it.mict.lab.bytebuddy.entity.MyEntityB;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType.Unloaded;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodCall;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.auxiliary.AuxiliaryType.NamingStrategy;
import net.bytebuddy.matcher.ElementMatchers;

public class App 
{
    private static Class<? extends MyEntityA> creteProxyClass() {

        Unloaded<MyEntityA> unloadedClass = new ByteBuddy()
                .with(new NamingStrategy.SuffixingRandom("MyProxy"))
                .subclass(MyEntityA.class)
                .defineField("_target", MyEntityA.class, Visibility.PRIVATE)
                .defineConstructor(Visibility.PUBLIC)
                .withParameter(MyEntityA.class)
                .intercept(
                    MethodCall.invoke(getDefaultConstructor(MyEntityA.class))
                        .andThen(FieldAccessor.ofField("_target").setsArgumentAt(0))
                )
                .method(ElementMatchers.nameStartsWith("getRaw")
                    .or(ElementMatchers.nameStartsWith("setRaw")))
                    .intercept(MethodDelegation.toField("_target"))
                .make();
        
        Class<? extends MyEntityA> proxyClass = unloadedClass
            .load(MyEntityA.class.getClassLoader())
            .getLoaded();
        
        return proxyClass;
    }

    private static Constructor<?> getDefaultConstructor(Class<MyEntityA> entityClass) {
        
        for (Constructor<?> constructor : entityClass.getDeclaredConstructors()) {
            if (constructor.getParameterCount() == 0) {
                return constructor;
            }
        }
        
        throw new IllegalStateException();
    }
    
    public static void main( String[] args ) throws Exception
    {
        MyEntityB entityB = new MyEntityB(new MyEntityA(123));
        entityB.hello();
        
        Class<? extends MyEntityA> proxyClass = creteProxyClass();
        System.out.println("MyEntityA package : " + MyEntityA.class.getPackage().getName());
        System.out.println("Proxy package     : " + proxyClass.getPackage().getName());

        Constructor<? extends MyEntityA> proxyConstructor = proxyClass.getConstructor(new Class<?>[] { MyEntityA.class });
        MyEntityA proxy = proxyConstructor.newInstance(new MyEntityA());
    }
}

I'm using:

OpenJDK Runtime Environment (Temurin)(build 1.8.0_332-b09) OpenJDK 64-Bit Server VM (Temurin)(build 25.332-b09, mixed mode)

and ByteBuddy version 1.14.4

When I execute this App class, I expect no errors, while I get:

123
MyEntityA package : it.mict.lab.bytebuddy.entity
Proxy package     : it.mict.lab.bytebuddy.entity
Exception in thread "main" java.lang.VerifyError: Bad access to protected data in invokevirtual
Exception Details:
  Location:
    it/mict/lab/bytebuddy/entity/MyEntityA$ByteBuddy$GZzebPWq.getRawId()I @4: invokevirtual
  Reason:
    Type 'it/mict/lab/bytebuddy/entity/MyEntityA' (current frame, stack[0]) is not assignable to 'it/mict/lab/bytebuddy/entity/MyEntityA$ByteBuddy$GZzebPWq'
  Current Frame:
    bci: @4
    flags: { }
    locals: { 'it/mict/lab/bytebuddy/entity/MyEntityA$ByteBuddy$GZzebPWq' }
    stack: { 'it/mict/lab/bytebuddy/entity/MyEntityA' }
  Bytecode:
    0x0000000: 2ab4 000a b600 0cac                    

    at java.lang.Class.getDeclaredConstructors0(Native Method)
    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
    at java.lang.Class.getConstructor0(Class.java:3075)
    at java.lang.Class.getConstructor(Class.java:1825)
    at it.mict.lab.bytebuddy.App.main(App.java:63)

123 is what is printed using the MyEntityB class, then the next two lines check that that MyEntityA class and the one created by ByteBuddy are in the same package, and then there is the exception encountered while creating the instance of the proxy.

If I change MyEntityA.getRawId() visibility from protected to public, everything works fine (but of course, I need it to be protected).

2 Answers2

1

According to this (old) issue it's a bytebuddy bug, the workaround explained there is to use the same package for the Subclass, So:

Unloaded<MyEntityA> unloadedClass = new ByteBuddy()
    .with(new NamingStrategy.SuffixingRandom("MyProxy"))

becomes

Unloaded<MyEntityA> unloadedClass = new ByteBuddy()
    .with(new NamingStrategy.SuffixingRandom("it.mict.lab.bytebuddy.entity.MyProxy"))

Also make sure to load the class with the same Classloader as MyEntityA.

uglibubla
  • 83
  • 11
  • 1
    Changing the NamingStrategy did not change the result, but changing `.load(MyEntityA.class.getClassLoader())` with `.load(MyEntityA.class.getClassLoader(), ClassLoadingStrategy.UsingLookup.withFallback(() -> MethodHandles.lookup()))` it works. But in this case, if I change JDK with version 17, I get this error: `Exception in thread "main" java.lang.IllegalArgumentException: it.mict.lab.bytebuddy.entity.MyEntityA$ByteBuddy$kpHeegtl must be defined in the same package as it.mict.lab.bytebuddy.App` – Marco Dabbene May 10 '23 at 16:35
0

Being sure the classloader is the same for both the original class and the proxy is not easy at is seems. In fact I found a way to force the same classloader in Java 8, but it did not work in Java 17. Then I found a wonderful 2018 article from the author of ByteBuddy JDK 11 and proxies in a world past sun.misc.Unsafe where he shows the correct way to specify class loading strategy and now my example works for both Java 8 and Java 17. I copy here the new version of the App class example that shows that solution:

package it.mict.lab.bytebuddy;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import it.mict.lab.bytebuddy.entity.MyEntityA;
import it.mict.lab.bytebuddy.entity.MyEntityB;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType.Unloaded;
import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.MethodCall;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.auxiliary.AuxiliaryType.NamingStrategy;
import net.bytebuddy.matcher.ElementMatchers;

public class App 
{
    private static Class<? extends MyEntityA> creteProxyClass() throws Exception {

        Unloaded<MyEntityA> unloadedClass = new ByteBuddy()
                .with(new NamingStrategy.SuffixingRandom("MyProxy"))
                .subclass(MyEntityA.class)
                .defineField("_target", MyEntityA.class, Visibility.PRIVATE)
                .defineConstructor(Visibility.PUBLIC)
                .withParameter(MyEntityA.class)
                .intercept(
                    MethodCall.invoke(getDefaultConstructor(MyEntityA.class))
                        .andThen(FieldAccessor.ofField("_target").setsArgumentAt(0))
                )
                .method(ElementMatchers.nameStartsWith("getRaw")
                    .or(ElementMatchers.nameStartsWith("setRaw")))
                    .intercept(MethodDelegation.toField("_target"))
                .make();
        
        // This is the strategy part shown in the Rafael blog

        ClassLoadingStrategy<ClassLoader> strategy;
        if (ClassInjector.UsingLookup.isAvailable()) {
            Class<?> methodHandles = Class
                    .forName("java.lang.invoke.MethodHandles");
            Object lookup = methodHandles.getMethod("lookup").invoke(null);
            Method privateLookupIn = methodHandles.getMethod("privateLookupIn",
                    Class.class,
                    Class.forName("java.lang.invoke.MethodHandles$Lookup"));
            Object privateLookup = privateLookupIn.invoke(null, MyEntityA.class,
                    lookup);
            strategy = ClassLoadingStrategy.UsingLookup.of(privateLookup);
        } else if (ClassInjector.UsingReflection.isAvailable()) {
            strategy = ClassLoadingStrategy.Default.INJECTION;
        } else {
            throw new IllegalStateException(
                    "No code generation strategy available");
        }
        
        Class<? extends MyEntityA> proxyClass = unloadedClass
            .load(MyEntityA.class.getClassLoader(), strategy)
            .getLoaded();
        
        return proxyClass;
    }

    private static Constructor<?> getDefaultConstructor(Class<MyEntityA> entityClass) {
        
        for (Constructor<?> constructor : entityClass.getDeclaredConstructors()) {
            if (constructor.getParameterCount() == 0) {
                return constructor;
            }
        }
        
        throw new IllegalStateException();
    }
    
    public static void main( String[] args ) throws Exception
    {
        System.out.println("Java version: " + System.getProperty("java.version"));
        
        MyEntityB entityB = new MyEntityB(new MyEntityA(123));
        entityB.hello();
        
        Class<? extends MyEntityA> proxyClass = creteProxyClass();
        System.out.println("MyEntityA package : " + MyEntityA.class.getPackage().getName());
        System.out.println("Proxy package     : " + proxyClass.getPackage().getName());

        System.out.println("MyEntityA ClassLoader : " + MyEntityA.class.getClassLoader());
        System.out.println("Proxy ClassLoader     : " + proxyClass.getClassLoader());

        Constructor<? extends MyEntityA> proxyConstructor = proxyClass.getConstructor(new Class<?>[] { MyEntityA.class });
        MyEntityA proxy = proxyConstructor.newInstance(new MyEntityA());
        System.out.println("Proxy: " + proxy);
    }
}