Scala 脚本:解释此 class 将错误转换为 Windows 上的 ScalaClassLoader

Scala Script: Explain this class cast error to ScalaClassLoader on Windows

考虑以下 Scala 脚本:

import scala.reflect.internal.util.ScalaClassLoader

object Test {
  def main(args: Array[String]) {
    val classloaderForScalaLibrary = classOf[ScalaClassLoader.URLClassLoader].getClassLoader
    println(classloaderForScalaLibrary)
    val classloaderForTestClass = this.getClass.getClassLoader
    println(classloaderForTestClass)
    this.getClass.getClassLoader.asInstanceOf[ScalaClassLoader.URLClassLoader]
  }
}

输出为:

scala.reflect.internal.util.ScalaClassLoader$URLClassLoader@71c8becc
scala.reflect.internal.util.ScalaClassLoader$URLClassLoader@71c8becc
java.lang.ClassCastException: scala.reflect.internal.util.ScalaClassLoader$URLClassLoader cannot be cast to scala.reflect.internal.util.ScalaClassLoader$URLClassLoader
        at Main$.main(Test.scala:8)
        at Main.main(Test.scala)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at scala.reflect.internal.util.ScalaClassLoader.$anonfun$run(ScalaClassLoader.scala:98)
        at scala.reflect.internal.util.ScalaClassLoader.asContext(ScalaClassLoader.scala:32)
...

为什么我不能将 ScalaClassLoader$URLClassLoader 转换为 ScalaClassLoader$URLClassLoader

编辑:

在 运行 上:

scala -J-verbose:class Test.scala | grep ScalaClassLoader

输出为:

[Loaded scala.reflect.internal.util.ScalaClassLoader$URLClassLoader from file:/C:/Development/Software/scala-2.12.2/lib/scala-reflect.jar]
...
...
[Loaded scala.reflect.internal.util.ScalaClassLoader$URLClassLoader from file:/C:/DEVELO~1/Software/SCALA-~1.2/lib/scala-reflect.jar]

所以肯定有一些可疑的 class 加载正在进行。现在试图调查为什么会这样

如果您按以下方式进一步扩展代码:

import scala.reflect.internal.util.ScalaClassLoader

object test {

  def main(args: Array[String]) {
    val cl1 = this.getClass.getClassLoader
    println(cl1)
    val c1 = cl1.getClass 
    println(cl1.getClass)
    println(cl1.getClass.getClassLoader)

    println("-------")

    var c2 = classOf[ScalaClassLoader.URLClassLoader]
    println(c2)
    println(c2.getClassLoader)
    println("-------")
    println(c1 == c2)

  }
}

您将得到以下输出:

scala.reflect.internal.util.ScalaClassLoader$URLClassLoader@5cee5251
class scala.reflect.internal.util.ScalaClassLoader$URLClassLoader
sun.misc.Launcher$AppClassLoader@4554617c
-------
class scala.reflect.internal.util.ScalaClassLoader$URLClassLoader
scala.reflect.internal.util.ScalaClassLoader$URLClassLoader@5cee5251
-------
false

注意匹配的哈希 @5cee5251。 这意味着第一个 Scala 解释器加载 ScalaClassLoader$URLClassLoader 使用根 Java class 加载器然后使用那个 class 加载器加载脚本中的所有 classes 以及何时你在你的代码中请求 ScalaClassLoader$URLClassLoader 它加载了 ScalaClassLoader$URLClassLoader 的另一个(已经加载的)实例。通过这种方式,您的脚本与执行它的 "runtime environment" 隔离。

您可以在 ScalaClassLoader.asContext method, that you can see in your stack trace, that uses Thread.setContextClassLoader 找到一些详细信息,以将其自身设置为执行脚本的线程的主要 class加载程序。

Update(为什么它适用于 Mac 但不适用于 Windows)

*nix 的 scala shell 脚本和 Windows 的 scala.bat 脚本之间的主要区别是默认情况下在 *nix 平台上标准 Scala 库被添加到引导Class路径(参见脚本中的 usebootcp),而在 Windows 上,它们被添加到 "Usual Classpath"。这很重要,因为它定义了哪个 class 加载器将加载 scala.tools.nsc.MainGenericRunner 使用的 scala.reflect.internal.util.ScalaClassLoader:它将是根 class 加载器(表示为 null 如果您调用 getClassLoader) 或应用程序 Class 加载程序(即 sun.misc.Launcher$AppClassLoader 的实例)。这很重要,因为 CommonRunner.run 仅使用 urls 而没有 parent

创建 ScalaClassLoader 的实例
def run(urls: Seq[URL], objectName: String, arguments: Seq[String]) {
  (ScalaClassLoader fromURLs urls).run(objectName, arguments)
} 

这意味着 "main" ScalaClassLoader 的父 class 加载程序将是引导 class 加载程序而不是 sun.misc.Launcher$AppClassLoader 因此当你问这个"main" ScalaClassLoader 对于 class scala.reflect.internal.util.ScalaClassLoader 它无法在其 class 加载程序链加载的 class 中找到它,因此必须加载它再次。这就是为什么您的脚本中有两个不同的 ScalaClassLoader class 实例的原因。

有两个明显的解决方法(但都不太好):

  • 更改 Scala 源代码中的 CommonRunner.run 以实际将当前上下文 class 加载器作为父级传递给新的 ScalaClassLoader(可能没那么容易 ☺)
  • scala.bat 更改为对 %_TOOL_CLASSPATH% 使用 -Xbootclasspath/a: 而不是 -cp。但是查看 *nix 脚本中的 usebootcp 我可以看到以下评论:
# default to the boot classpath for speed, except on cygwin/mingw/msys because
# JLine on Windows requires a custom DLL to be loaded.
unset usebootcp
if [[ -z "$cygwin$mingw$msys" ]]; then
  usebootcp="true"
fi

所以我怀疑如果您想对 REPL 使用 scala.bat,将所有 Scala 库移动到 Boot Class 路径可能不是一个好主意。如果是这种情况,您可能需要创建 scala.bat 的副本(例如 scala_run_script.bat)更改它并将其用于 运行 您的 Scala 脚本,留下标准 scala.bat REPL.