steps
sbt version: 1.3.7
$ git clone https://github.com/xerial/sbt-classloader-future
$ cd sbt-classloader-future
$ sbt run
// java.lang.ClassNotFoundException will be thrown
problem
- sbt's layered classloader is not properly used inside Scala Future's global context
Future { Thread.currentTHread.getContextClassLoader.loadClass("(class name path)") } fails unless Flat layering strategy or fork in run := true is used.
package sample
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}
case class A(id:Int)
object FutureSample {
def main(args:Array[String]): Unit = {
// OK
val cl = Thread.currentThread.getContextClassLoader.loadClass("sample.A")
println(cl.getName)
val f = Future {
// Fails other than setting ClassLoaderLayeringStorategy.Flat or fork in run = true
val cl = Thread.currentThread.getContextClassLoader.loadClass("sample.A")
println(cl.getName)
}
Await.result(f, Duration.Inf)
}
}
Workarounds
Use one of the following workarounds:
- Use Flat strategy (AllLibraryJars didn't work)
- Use
fork in run/test := true
- Use a user-defined ExecutionContext for Future, instead of
scala.concurrent.ExecutionContext.Implicits.global.
- I guess
global belongs to the class loader for loading Scala libraries and it was initialized outside the context of the application.
expectation
- LayeredClassLoader should be used when initializing
import scala.concurrent.ExecutionContext.Implicits.global
- The above example project should succeed
- Minimize the mean time to understanding this pecurier behavior of the layered class loader. This issue might not be a bug of sbt as we should not rely on a specific behavior of class loaders, but we needed a long time to figure out that this issue is caused by the layered class loader.
notes
Here are the hierarchies of class loaders:
Class loader hierarchy within applications:
-class sbt.internal.LayeredClassLoader
-class sbt.internal.classpath.WrappedLoader
-class sbt.internal.ScalaReflectClassLoader
-class sbt.internal.classpath.WrappedLoader
-class sbt.State$UncloseableURLLoader
-class xsbt.boot.LibraryClassLoader 0s
-class xsbt.boot.Launch$TestInterfaceLoader$3
-class xsbt.boot.BootFilteredLoaders
-class jdk.internal.loader.ClassLoaders$AppClassLoader
-class jdk.internal.loader.ClassLoaders$PlatformClassLoader
Class loader hierarchy within Future when using the global context. This cannot read application classes:
-class java.net.URLClassLoader
-class java.net.URLClassLoader
-class xsbt.boot.LibraryClassLoader
-class xsbt.boot.Launch$TestInterfaceLoader
-class xsbt.boot.BootFilteredLoader
-class jdk.internal.loader.ClassLoaders$AppClassLoader
-class jdk.internal.loader.ClassLoaders$PlatformClassLoader
steps
sbt version: 1.3.7
problem
Future { Thread.currentTHread.getContextClassLoader.loadClass("(class name path)") }fails unless Flat layering strategy orfork in run := trueis used.Workarounds
Use one of the following workarounds:
fork in run/test := truescala.concurrent.ExecutionContext.Implicits.global.globalbelongs to the class loader for loading Scala libraries and it was initialized outside the context of the application.expectation
import scala.concurrent.ExecutionContext.Implicits.globalnotes
Here are the hierarchies of class loaders: