Search code examples
classloaderbyte-buddy

My ByteBuddy generated classes can't see each other


I try to basically implement deep mocking graphs of classes (which I won't control in the wild) in order to record what is being called. The quick and dirty version using simply Mockito was surprisingly robust, but I ran into a wall trying to implement it 'properly' (and not have people ask funny questions why they need mockito in the runtime classpath). Because this is scala, default constructors are unheard of, null values are very rarely handed gracefuly and deeply nested member classes are a norm, hence when I need to instrument a class, I need to provide actual arguments for the constructor, requiring me to instrument those in turn. Relevant snippets from my code:

    private def createTracerClass(tpe :Type, clazz :Class[_]) :Class[_] = {
    val name = clazz.getName + TracerClassNameSuffix + tpe.typeSymbol.fullName.replace('.', '_')
    val builder =
        if (clazz.isInterface) //todo: implement abstract methods!!!
            ByteBuddy.subclass(clazz, new ForDefaultConstructor).name(name)
        else {
            val constructors = clazz.getDeclaredConstructors
                .filter { c => (c.getModifiers & (PUBLIC | PROTECTED)) != 0 }.sortBy(_.getParameterCount)
            if (constructors.isEmpty)
                throw new PropertyReflectionException(
                    s"Can't instrument a tracer for class ${clazz.getName} as it has no accessible constructor."
                )
            val best = constructors.head

            new ByteBuddy().subclass(clazz, NO_CONSTRUCTORS).name(name)
                .defineConstructor(PUBLIC).intercept(
                    invoke(best).onSuper.`with`(best.getParameterTypes.map(createInstance):_*)
                )
        }
    println("instrumenting " + name + "; class loader: "+clazz.getClassLoader)
    val mockClass = builder
        .method(not(isConstructor[MethodDescription]())).intercept(to(new MethodInterceptor()))
        .defineMethod(TypeMethodName, classOf[AnyRef], PUBLIC).intercept(FixedValue.value(tpe))
        .defineField(TraceFieldName, classOf[Trace], PUBLIC)
        .make().load(getClassLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT).getLoaded
    println("created class " + mockClass +"with classloader: " + mockClass.getClassLoader)
    mockClass
}


private def instrumentedInstance(clazz :Class[_], modifiers :Int = PUBLIC | PROTECTED) :AnyRef =
    if ((clazz.getModifiers & FINAL) != 0)
        null
    else {
        val mockClass = MockCache.getOrElse(clazz,
            clazz.synchronized {
                MockCache.getOrElse(clazz, {
                    println("creating mock class for "+clazz.getName)
                    clazz.getDeclaredConstructors.filter { c => (c.getModifiers & modifiers) != 0 }
                        .sortBy(_.getParameterCount).headOption.map { cons =>
                            val subclass = ByteBuddy.subclass(clazz, NO_CONSTRUCTORS)
                                .name(clazz.getName + MockClassNameSuffix)
                                .defineConstructor(PUBLIC).intercept(
                                    invoke(cons).onSuper.`with`(cons.getParameterTypes.map(createInstance) :_*)
                                ).make().load(getClassLoader, ClassLoadingStrategy.Default.WRAPPER_PERSISTENT).getLoaded
                            MockCache.put(clazz, subclass)
                        subclass
                    }.orNull
                })
            }
        )
        println("creating a mock for " + clazz.getName + "; class loader: " + mockClass.getClassLoader)
        mockClass.getConstructor().newInstance().asInstanceOf[AnyRef]
    }

The issue is in the constructor generator at the bottom of the first method, which uses createInstance to create. That method in turn falls back to instrumentInstance.

The result is that I get a NoClassDefFoundError during load (LoadedTypeInitializer$ForStaticField.onLoad()) because each class is loaded with its own class loader. Unfortunately, though the reason was immediately apparent, it helped me not a bit in trying to make ByteBuddy share a class loader or somehow else make those classes available. I played with all provided arguments to make, load but to no avail; having all calls share a TypePool, different type resolution strategies - nothing except the INJECTION ClassLoaderStrategy which I don't want to use due to its reliance on private APIs which wouldn't make investing my effort into this strategical.

It seems like its a very basic issue and simple to solve, but I browsed through many code samples from other projects and I can't see anything they do differently that should make any difference. Ideas?


Solution

  • It's very likely related to your use of ClassLoadingStrategy.Default.WRAPPER_PERSISTENT. Using this strategy, classes are loaded in an isolated class loader that makes classes invisible to anybody not inheriting from that classes class loader.

    For loading a group of classes, you'd probably want to combine all unloaded classes (.merge) and load them alltogether in a single class loader.

    Note that you can also create a ByteArrayClassLoader yourself and leave it open for injection. This way later classes can be added using the injecting class loading strategy.