ByteBuddy fails when trying to redefine sun.reflec

2019-06-02 04:52发布

问题:

Driven by curiosity, I tried to export the bytecode of GeneratedMethodAccessor1 (generated by the JVM when using reflection).

I try to get the bytecode of the class the following way:

public class MethodExtractor {

    public static void main(String[] args) throws Exception {

        ExampleClass example = new ExampleClass();

        Method exampleMethod = ExampleClass.class
                .getDeclaredMethod("exampleMethod");
        exampleMethod.setAccessible(true);

        int rndSum = 0;
        for (int i = 0; i < 20; i++) {
            rndSum += (Integer) exampleMethod.invoke(example);
        }

        Field field = Method.class.getDeclaredField("methodAccessor");
        field.setAccessible(true);
        Object methodAccessor = field.get(exampleMethod);
        Field delegate = methodAccessor.getClass().getDeclaredField("delegate");
        delegate.setAccessible(true);
        Object gma = delegate.get(methodAccessor);

        ByteBuddyAgent.installOnOpenJDK();
        try {
            ClassFileLocator classFileLocator = ClassFileLocator.AgentBased
                    .fromInstalledAgent(gma.getClass().getClassLoader());
            Unloaded<? extends Object> unloaded = new ByteBuddy().redefine(
                    gma.getClass(), classFileLocator).make();
            Map<TypeDescription, File> saved = unloaded.saveIn(Files
                    .createTempDirectory("javaproxy").toFile());
            saved.forEach((t, u) -> System.out.println(u.getAbsolutePath()));
        } catch (IOException e) {
            throw new RuntimeException("Failed to save class to file");
        }
    }
}

I however get the following error when executing this class:

Exception in thread "main" java.lang.NullPointerException
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Engine$ForRedefinition.create(TypeWriter.java:172)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:1182)
    at net.bytebuddy.dynamic.scaffold.inline.InlineDynamicTypeBuilder.make(InlineDynamicTypeBuilder.java:244)
    at reegnz.dyna.proxy.extractor.MethodExtractor.main(MethodExtractor.java:48)

Basically I first iterate on the method call enough times for the JVM to inflate the method (generate the GeneratedMethodAccessor) and then try to redefine the class to get the bytecode.

I tried the same method to export a generated Proxy class, and it worked flawlessly. That's what drove me to try this.

It seems that the DelegatingClassLoader of the GeneratedMethodAccessor1 class can't even reload the class when I try to load the class with the loadClass method.

Any ideas how I could retrieve the bytecode for GeneratedMethodAccessor classes?

回答1:

First of all, the NullPointerException is a bug, I just fixed that. The loader should have thrown an IllegalArgumentException instead but it never got that far. Thanks for bringing this to my attention.

Boiled down, the problem Byte Buddy is facing is that

gma.getClass().getClassLoader().findClass(gma.getClass().getName());

throws a ClassNotFoundException. This is a consequence of using a DelegatingClassLoader for the accessor classes. As an educated guess, I think that this class loader intends to shield its classes from the outside in order to make them easily garbage collectable. However, not allowing the lookup of a class what somewhat breaks the contract for a ClassLoader. Apart from that, I assume that this loading routine will be refactored to use the JDK's anonymous class loaders at some point in the future (similar to classes representing lambda expressions). Strangely enough, it seems like the source code for the DelegatingClassLoader is not available in the JDK even though I can find it in the distribution. Probably, the VM treats these loader specially at some place.

For now, you can use the following ClassFileTransformer which uses some reflection magic on the class loader to locate the loaded class and to then extract the byte array. (The ClassFileLocator interface only takes a name instead of a loaded class in order to allow working with unloaded types which is normally always the case. No idea why this does not work in this case.)

class DelegateExtractor extends ClassFileLocator.AgentBased {

  private final ClassLoader classLoader;
  private final Instrumentation instrumentation;

  public DelegateExtractor(ClassLoader classLoader, Instrumentation instrumentation) {
    super(classLoader, instrumentation);
    this.classLoader = classLoader;
    this.instrumentation = instrumentation;
  }

  @Override
  public Resolution locate(String typeName) {
    try {
      ExtractionClassFileTransformer classFileTransformer = 
          new ExtractionClassFileTransformer(classLoader, typeName);
      try {
        instrumentation.addTransformer(classFileTransformer, true);
        // Start nasty hack
        Field field = ClassLoader.class.getDeclaredField("classes");
        field.setAccessible(true);
        instrumentation.retransformClasses(
            (Class<?>) ((Vector<?>) field.get(classLoader)).get(0));
        // End nasty hack
        byte[] binaryRepresentation = classFileTransformer.getBinaryRepresentation();
        return binaryRepresentation == null
          ? Resolution.Illegal.INSTANCE
          : new Resolution.Explicit(binaryRepresentation);
      } finally {
        instrumentation.removeTransformer(classFileTransformer);
      }
    } catch (Exception ignored) {
      return Resolution.Illegal.INSTANCE;
    }
  }
}

To further simplify your code, you can use the ClassFileLocators directly instead of applying a rewrite which as a matter of fact might slightly modify the class file even if you do not apply any changes to a class.