User1291
User1291

Reputation: 8182

Avoiding Injection when loading instrumented classes

Let's say I want to create a custom class at runtime that another class can make use of.

package redefineconcept;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;

import java.lang.reflect.InvocationTargetException;

public class LoadingTest {
    public static final String HelloWorldTag = "$HelloWorld";

    public static void main(String[] args){
        new LoadingTest().run();
    }

    private void run(){

        InstanceUser u = new InstanceUser();
        u.start();

        Class <?> createdClass = createAndLoadFor(InstanceUser.class);
        System.out.println(String.format("created new class %s", createdClass.getName()));
        InstanceUser.canAccess = true;

        try {
            u.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private Class<?> createAndLoadFor(Class<?> clazz){
        ByteBuddy byteBuddy = new ByteBuddy();

        String newClassName = clazz.getName() + LoadingTest.HelloWorldTag;

        DynamicType.Builder builder = byteBuddy
                .subclass(Object.class)
                .name(newClassName)
                ;

        DynamicType.Unloaded<?> newType = builder.make();

        return newType
                .load(clazz.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
                .getLoaded();
    }
}

class InstanceUser extends Thread{
    public static volatile boolean canAccess = false;
    Object instance;

    @Override
    public void run() {
        while(!canAccess){}
        String cn = this.getClass().getName() + LoadingTest.HelloWorldTag;
        Class clazz;
        try{
            clazz = Class.forName(cn);
        }catch(ClassNotFoundException e){
            e.printStackTrace();
            throw new RuntimeException();
        }
        try{
            instance = clazz.getConstructor().newInstance();
        }catch(NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e){
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
}

This works.

However, the ByteBuddy tutorial, suggests

You might consider the chance of encountering circular dependencies to be of minor relevance since you are creating one dynamic type at a time. However, the dynamic creation of a type might trigger the creation of so-called auxiliary types.

These types are created by Byte Buddy automatically to provide access to the dynamic type you are creating.

because of this, we recommend you to load dynamically created classes by creating a specific ClassLoader instead of injecting them into an existing one, whenever possible.

I don't know terribly much about classloaders -- or ByteBuddy, for that matter -- but the tutorial seems to suggest that classloaders are hierachically ordered.

If so, it should be possbible to chain the new class loader to clazz.getClassLoader(), right?

Well, I've had no such luck with neither ClassLoadingStrategy.Default.WRAPPER nor ClassLoadingStrategy.Default.CHILD_FIRST.

Both result in

created new class redefineconcept.InstanceUser$HelloWorld
java.lang.ClassNotFoundException: redefineconcept.InstanceUser$HelloWorld

which led me to believe that

Normally, Java class loaders query their parent ClassLoader before attempting to directly load a type of a given name.

means they only query the parent ClassLoaders but not the children.

Is that so?

And is it at all possible to avoid using ClassLoadingStrategy.Default.INJECTION, here?

Upvotes: 2

Views: 790

Answers (1)

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44032

Class loaders are (normally) hierarchical. If you are using the INJECTION strategy, Byte Buddy manually defines type by type by explicitly defining the classes. Depending on the JVM and class loader, this might trigger a class loading.

Consider a situation where A references B and B references A. If Byte Buddy injects A before B, the injection of A might cause a loading of B which is not yet injected at that point. At this point, the class loader that is the target of the injection will prematurly and unsuccessfully try to look up B and fail with a NoClassDefFoundError.

When using the WRAPPER strategy, Byte Buddy creates a new class loader that is aware of both types and can look up B when A is loaded as no injection is required.

The problem you encounter is caused by your use of Class.forName(name). This method is caller sensitive meaning that the calling class's class loader is used. From your thread, this will most likely be the system class loader which is the same class loader you injected before.

That said, typically a JVM is loading types lazily and injection should not render a big problem for 99% of all use cases.

Upvotes: 2

Related Questions