Search code examples
javaclassloaderinstrumentationjavassistjavaagents

How to instrument classes loaded by a custom class loader?


I was trying to modify the byte code of several classes whose packaging jar files are not in class path - they are loaded by a custom ClassLoader during runtime given an URL. I tried to use a java agent with ClassFileTransformer hoping to intercept those classes but failed. The classloader is part of a legacy project so I cannot make changes to it directly.

The agent works fine on classes loaded by AppClassLoader 'locally' but just ignores those loaded by the custom classloader.

the CustomClassLoader:

    public class CustomClassLoader extends URLClassLoader {

    public CustomClassLoader(URL[] urls) {
        super(urls, CustomClassLoader.class.getClassLoader());
    }

    // violates parent-delegation pattern
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> clazz = findLoadedClass(name);
            if (clazz == null) {
                try {
                    clazz = findClass(name);
                } catch (ClassNotFoundException e) {
                }

                if (clazz == null) {
                    clazz = getParent().loadClass(name);
                }
            }
            if (resolve) {
                resolveClass(clazz);
            }
            return clazz;
        }
    }
}

the ClassFileTransformer used in my agent (with javassist):

public class MyTransformer implements ClassFileTransformer
{
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException
    {
        byte[] byteCode = null;

        if (className.replace("/", ".").equals("com.example.services.TargetService"))
        {
            ClassPool cp = ClassPool.getDefault();
            CtClass cc;
            try
            {
                cc = cp.get("com.example.services.TargetService");
                CtMethod verifyMethod = cc.getDeclaredMethod("verify");
                //invalidate the verification process of method : verify
                verifyMethod.insertBefore("{return true;}");
                byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }

        return byteCode;
    }
}

the Agent:

public class Agent
{
    public static void premain(String agentArgs, Instrumentation inst)
    {
        inst.addTransformer(new MyTransformer());
    }

}

I came up with a workaround by instrumenting the CustomClassLoader itself, invoking instrumentation.redifineClasses() but don't know how to pass the instrumentation instance into the CustomClassLoader instance; I'm new to instrumentation/class loading and still not quite clear with their mechanism. Any help? Thanks.

To make it simple:

  1. Inside an app create a custom URLClassLoader which loads some jar files elsewhere in you file system during runtime.
  2. Implement an java agent transforming a class loaded by your classloader, replacing one of it's method's body or something.
  3. Inside the app assign a class's instance to its interface and call its instrumented methods.
  4. Run the app with -javaagent to check.

Solution

  • I assume that your class is not properly instrumented because you are calling ClassPool.getDefault() which does not include class files visible to your custom class loader but only to the system class loader. You never register the classfileBuffer class file.

    As an alternative, you can try out Byte Buddy which offers easier access to the instrumentation API:

    new AgentBuilder.Default()
      .type(named("com.example.services.TargetService"))
      .transform((builder, type, loader) -> {
        builder.method(named("verify")).intercept(FixedValue.of(true));
      }).installOn(instrumentation);
    

    The above agent can be invoked from the agentmain or premain method. You can also disable class file format changes and redefine existing classes in case that the class is already (potentially) loaded during attachment.