lihaoyi test$ tree
.
└── Foo.scala
0 directories, 1 file
lihaoyi test$ cat Foo.scala
object Main{
def main(args: Array[String]): Unit = {
println(getClass.getClassLoader.getResourceAsStream("java/lang/String.class"))
println(getClass.getClassLoader.getClass)
println(Thread.currentThread().getContextClassLoader.getResourceAsStream("java/lang/String.class"))
println(Thread.currentThread().getContextClassLoader.getClass)
}
}
lihaoyi test$ sbt run
[info] Loading global plugins from /Users/lihaoyi/.sbt/0.13/plugins
[info] Set current project to test (in build file:/Users/lihaoyi/Dropbox/Workspace/test/)
[info] Updating {file:/Users/lihaoyi/Dropbox/Workspace/test/}test...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to /Users/lihaoyi/Dropbox/Workspace/test/target/scala-2.10/classes...
[info] Running Main
sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@18e38ff2
class sbt.classpath.ClasspathUtilities$$anon$1
null
class sbt.classpath.ClasspathFilter
[success] Total time: 2 s, completed 29 May, 2017 4:14:11 PM
lihaoyi test$
Here, we can see that the getClass.getClassLoader
and the Thread.currentThread.getContextClassLoader
are returning different values. What's more, the Thread.currentThread.getContextClassLoader
seems to be refusing to load java/lang/String.class
, while the other can.
Notably, when I run the jar file using an external tool like scalac
/scala
, or java
, both classloaders are able to load the classfile as a resource
lihaoyi test$ scalac Foo.scala
lihaoyi test$ scala Main
sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@1b28cdfa
class scala.reflect.internal.util.ScalaClassLoader$URLClassLoader
sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@7229724f
class scala.reflect.internal.util.ScalaClassLoader$URLClassLoader
I would expect SBT to behave similarly: to have the Main.getClass.getClassLoader
and the Thread.currentThread().getContextClassLoader
both be able to load java/lang/String.class
as a resource. What gives?
Some hints provided by Jason Zaugg (retronym)'s sbt launcher notes.
sbt/launcher is a small Scala application that bootstraps an arbitrary Scala program (typically, SBT) described a config file and sourced via Ivy dependency resolution.
This creates a child classloader containing Scala 2.10.6. A child of this contains SBT itself and xsbti/interface-0.13.11.jar.
SBT needs to use non-standard classloader delegation to selectively hide classes when creating child classloaders for plugin code, for the Scala compiler, or for user code.
Some more hints in the sbt 0.13 sources:
def makeLoader(classpath: Seq[File], instance: ScalaInstance, nativeTemp: File): ClassLoader =
filterByClasspath(classpath, makeLoader(classpath, instance.loader, instance, nativeTemp))
def makeLoader(classpath: Seq[File], parent: ClassLoader, instance: ScalaInstance, nativeTemp: File): ClassLoader =
toLoader(classpath, parent, createClasspathResources(classpath, instance), nativeTemp)
Basically sbt is a kitchen sink of a Java application that has an arbitrary Scala versions and your code, and your test libraries along with the Oracle/OpenJDK's Java library. To construct a classpath that makes sense without loading them over and over again, it's creating a hierarchy of classloaders each filtered by some criteria. (I think)
Notably, one way to work around this issue is to set
(fork in run) := true,
(connectInput in run) := true,
(outputStrategy in run) := Some(StdoutOutput),
That seems to solve this problem, (Thread.currentThread().getContextClassLoader.getResourceAsStream("java/lang/String.class")
now works) but introduces other unrelated problems (forked JVM takes a moment to boot, boots cold and takes time to warm up...)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With