Scala脚本:在Windows上向ScalaClassLoader解释此类强制转换错误

时间:2017-05-25 19:37:29

标签: scala scala-2.12

请考虑使用以下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$2(ScalaClassLoader.scala:98)
        at scala.reflect.internal.util.ScalaClassLoader.asContext(ScalaClassLoader.scala:32)
...

为什么我无法将ScalaClassLoader$URLClassLoader投射到ScalaClassLoader$URLClassLoader

enter image description here

修改

跑步时:

  

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]

所以肯定会有一些阴暗的类加载。现在试图调查为什么会这样

1 个答案:

答案 0 :(得分:2)

如果您将代码扩展得更多,如下所示:

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   
类scala.reflect.internal.util.ScalaClassLoader $ URLClassLoader   
sun.misc.Launcher$AppClassLoader@4554617c   
-------   
类scala.reflect.internal.util.ScalaClassLoader $ URLClassLoader   
scala.reflect.internal.util.ScalaClassLoader$URLClassLoader@5cee5251   
-------   

注意匹配哈希@5cee5251。 这意味着第一个Scala解释器使用根Java类加载器加载ScalaClassLoader$URLClassLoader,然后使用该类加载器加载脚本中的所有类,当您在代码中请求ScalaClassLoader$URLClassLoader时,它将加载另一个(已加载)ScalaClassLoader$URLClassLoader的实例。通过这种方式,您的脚本将与执行它的“运行时环境”隔离开来。

您可以在ScalaClassLoader.asContext方法中找到一些详细信息,您可以在堆栈跟踪中看到,使用Thread.setContextClassLoader将自己设置为执行脚本的线程的主类加载器。

更新(为什么它在Mac上运行但在Windows上不起作用)

* nix的scala shell脚本和Windows的scala.bat之间的主要区别在于,默认情况下* nix平台上的标准Scala库被添加到Boot Classpath(请参阅usebootcp在Windows上,它们被添加到“常用类路径”中。这很重要,因为它定义了哪个类加载器将加载scala.reflect.internal.util.ScalaClassLoader使用的scala.tools.nsc.MainGenericRunner:它是否为根类加载器(如果调用null,则表示为getClassLoader })或Application Class Loader(即sun.misc.Launcher$AppClassLoader的实例)。这很重要,因为CommonRunner.run只使用ScalaClassLoader而不使用urls

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

这意味着“main”ScalaClassLoader的父类加载器将是引导类加载器而不是sun.misc.Launcher$AppClassLoader,因此当您向类{{ScalaClassLoader询问此类scala.reflect.internal.util.ScalaClassLoader时1}}它无法在类加载器链加载的类中找到它,因此必须再次加载它。这就是您在脚本中有两个不同的ScalaClassLoader 实例的原因。

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

  • 将Scala源中的CommonRunner.run更改为实际将当前上下文类加载器作为新ScalaClassLoader的父级传递(可能不那么容易<)
  • scala.bat更改为使用-Xbootclasspath/a:而不是-cp %_TOOL_CLASSPATH%。但是,查看* 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

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