Search code examples
javajvmclassloaderjacocoinstrumentation

LinkageError from loading in classes that have a parent-child relation


I followed the CoreTutorial example class at http://www.jacoco.org/jacoco/trunk/doc/examples/java/CoreTutorial.java to understand how to incorporate Jacoco into a project.

However, I am facing java.lang.LinkageError problem that is related to the MemoryClassLoader class that is used.

/**
 * A class loader that loads classes from in-memory data.
 */
public static class MemoryClassLoader extends ClassLoader {

    private final Map<String, byte[]> definitions = new HashMap<String, byte[]>();

    /**
     * Add a in-memory representation of a class.
     * 
     * @param name
     *            name of the class
     * @param bytes
     *            class definition
     */
    public void addDefinition(final String name, final byte[] bytes) {
        definitions.put(name, bytes);
    }

    @Override
    protected Class<?> loadClass(final String name, final boolean resolve)
            throws ClassNotFoundException {
        final byte[] bytes = definitions.get(name);
        if (bytes != null) {
            return defineClass(name, bytes, 0, bytes.length);
        }
        return super.loadClass(name, resolve);
    }

}

Specifically, I have two classes, MyClass and MySubClass where MySubClass extends MyClass. I instrument both classes so that I can get coverage information related to each. I've found that if I instrument MySubClass first, MemoryClassLoader will call defineClass with the instrumented bytes for MySubClass. However, in doing so, MyClass, which is the parent class, also gets loaded in by the parent class loader. Thus, when I next instrument and load MyClass, I receive the java.lang.LinkageError which claims that there is already a definition of MyClass that has been loaded by a ClassLoader. The problem here is that the initial version of MyClass that was loaded in was not instrumented.

If I load in classes in the reverse order, this issue does not arise.

I've tried something a bit different here: https://github.com/huangwaylon/randoop/blob/bloodhound/src/main/java/randoop/main/MemoryClassLoader.java by immediately attempting to instrument the parent class upon a recursive call to loadClass. However, this isn't entirely working due to reasons I haven't figured out yet. I am observing that the coverage information for both classes are not changing if I use this faulty approach.

My question overall is, how can I instrument and load in classes that are related to each other in a parent, child relationship such that the behavior isn't affected by the order? Am I able to replace the definition of a class? Another problem seems to be super.load which would be the parent ClassLoader to MemoryClassLoader which I don't have control over.


Solution

  • First of all, you should override findClass instead of loadClass. The restriction that a class with a specific name can only be defined once in a loader does always apply and you may always encounter situations where the same class is requested multiple times (in any nontrivial scenario).

    The loadClass implementation inherited from ClassLoader will already take care of this and return the existing definition, if there is one. Since it also queries the parent loader first, you should take care to specify the right parent loader, if the loader is supposed to define classes with names that are also in use by other class loaders. Using the default implementation of loadClass, your findClass method can stay that simple:

    public static class MemoryClassLoader extends ClassLoader {
        private final Map<String, byte[]> definitions = new HashMap<>();
    
        public void addDefinition(final String name, final byte[] bytes) {
            definitions.put(name, bytes);
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            final byte[] bytes = definitions.get(name);
            if (bytes != null) {
                return defineClass(name, bytes, 0, bytes.length);
            }
            return super.findClass(name);
        }
    }
    

    Ensuring that all of your intended classes are instrumented, can be done in two ways.

    1. Instrument them on demand, i.e. let the findClass method trigger the instrumentation for the requested class.

    2. Instrument the byte code of all intended classes and put the result into the loader, before making the first call to loadClass.

      final MemoryClassLoader memoryClassLoader = new MemoryClassLoader();
      memoryClassLoader.addDefinition("MyClass", instrumentedMyClass);
      memoryClassLoader.addDefinition("MySubClass", instrumentedMySubClass);
      final Class<?> myClass = memoryClassLoader.loadClass("MyClass");
      

      Here, the order of the addDefinition invocations doesn’t matter and neither does it matter whether you call loadClass("MyClass") or loadClass("MySubClass") first. This even works with circular dependencies.