Search code examples
scalasbtclassloader

ClassCastException when running code in SBT


I have code which roughly looks like this:

val classLoader = new URLClassLoader(entry.jars, Thread.currentThread.getContextClassLoader)
val env = classLoader.loadClass(entry.name).asSubclass(classOf[Environment])

For context: An "entry" is a collection of JAR files and a class name. The class is expected to extend Environment. Environment is defined in the same package and doesn't use any classloading tricks. The collection of JAR files only contains the classes required for the specified class, and nothing more (especially nothing which has already been loaded by any parent ClassLoader).

When I run this piece of code from within sbt with run, all works fine. But if I run it a second time in the same sbt shell, I get:

[error] (run-main-1) java.lang.ClassCastException: class edu.tum.cs.isabelle.impl.Environment
java.lang.ClassCastException: class edu.tum.cs.isabelle.impl.Environment
    at java.lang.Class.asSubclass(Class.java:3404)

I fail to understand the problem here: There was no recompilation in between the runs or anything. I have similar code being executed for test, and it works fine there. It does work when I set fork to true, but there's no reason I should have to.

For debugging, I checked getClassLoader for classOf[Environment] and for the superclass of the loaded class. Sure enough, in the first run, they are equivalent, but not in the second run. Their toString representation is indentical, though.

Edit: More debugging. I've printed out the System.identityHashCode of all involved classloaders:

println(s"self: " + System.identityHashCode(classOf[Environment].getClassLoader))
println(s"fresh: " + System.identityHashCode(classLoader))
println(s"impl: " + System.identityHashCode(classLoader.loadClass(entry.name).getClassLoader))
println(s"impl.parent: " + identityHashCode(classLoader.loadClass(entry.name).getSuperclass.getClassLoader))

Results of the first run:

self: 1209891953
fresh: 1734438968
impl: 1734438968
impl.parent: 1209891953

Results of the second run:

self: 1952422714
fresh: 1110295313
impl: 1110295313
impl.parent: 1209891953

Solution

  • Rob Norris suggested a solution elsewhere. Since the current Thread's classloader can be "literally anything", I should make sure to use the classloader used to load Environment. Sure enough, if I change the code to

    val classLoader = new URLClassLoader(entry.jars, getClass.getClassLoader)
    

    the code is working as expected.