TL;DR
Ubuntu 的 Scala 包曾经与 Java 8 不兼容(这已在 2.11.12-4 中修复)。解决方案是卸载 Ubuntu 的 Scala 软件包并安装其中之一官方 Scala 包 https://www.scala-lang.org/download/all.html。这次你可能仍然想这样做,不是因为与 Java 不兼容,而是因为 Ubuntu 最新打包的 Scala 版本仍然是 2.11,而 Scala 的最新版本目前是 2.13。
sudo apt remove scala-library scala
wget https://downloads.lightbend.com/scala/2.13.4/scala-2.13.4.deb
sudo dpkg -i scala-2.13.4.deb
由于很多人都在问这个问题背后的原因,我也很好奇是什么原因造成的,所以我做了一些挖掘......
问题的根源
在 Java 9 中,Buffer 子类(包括 ByteBuffer)已更改为重写超类中返回 Buffer 的方法,以返回相应的子类型。
Bug: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-4774077 https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-4774077
犯罪:https://github.com/AdoptOpenJDK/openjdk-jdk9/commit/d9d7e875470bf478110b849315b4fff55b4c35cf https://github.com/AdoptOpenJDK/openjdk-jdk9/commit/d9d7e875470bf478110b849315b4fff55b4c35cf
此更改不向后兼容二进制。如果在 Buffer 的子类之一中直接调用这些方法之一的某些 Java 代码是使用 JDK9+ 编译的,则生成的字节码将无法在 JRE8 中运行(即使根本不使用返回值)。发生这种情况是因为调用时方法的签名将被编译为java.nio.ByteBuffer.clear()Ljava/nio/ByteBuffer
JRE8 中不存在。但是,如果使用JDK8编译,编译成字节码的签名将是java/nio/ByteBuffer.clear:()Ljava/nio/Buffer
它存在于 JRE8 和 JRE9+ 的 Buffer 类中。
Scala哪里出了问题? (或者做到了?)
Scala 编译器确实使用了一些受上述更改影响的方法。特别是,在源阅读器 https://github.com/scala/scala/blob/2.11.x/src/compiler/scala/tools/nsc/io/SourceReader.scala#L61OP问题中发生错误的类。
看着Scala 的兼容性矩阵 https://docs.scala-lang.org/overviews/jdk-compatibility/overview.html,它说我们至少需要 Scala 2.11.12 才能使用 JDK11,但它并没有明确说明相反的兼容性方向。它确实说“Scala 2.12+ 绝对不能在 JDK 6 或 7 上工作”,所以我们可以预期 2.12+ 仍然与 JDK8 兼容,甚至 Scala 2.11 也兼容。
那么为什么他们会破坏兼容性呢?难道他们不能用旧的 JDK 版本编译 Scala 的源代码吗?他们没有,但他们可以,所以他们仍然这样做。
如果我们下载官方 Scala 包之一并检查清单文件scala-compiler.jar
,这就是我们发现的:
斯卡拉 2.11.12:
Bundle-Name: Scala Compiler
Bundle-RequiredExecutionEnvironment: JavaSE-1.6, JavaSE-1.7
Bundle-SymbolicName: org.scala-lang.scala-compiler
Bundle-Version: 2.11.12.v20171031-225310-b8155a5502
Created-By: 1.6.0_45 (Sun Microsystems Inc.)
斯卡拉 2.13.4:
Bundle-Name: Scala Compiler
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-SymbolicName: org.scala-lang.scala-compiler
Bundle-Version: 2.13.4.v20201117-181115-VFINAL-39148e4
Created-By: 1.8.0_275 (AdoptOpenJDK)
所以看来 Scala 2.11 仍然使用 JDK6 编译,Scala 2.13 仍然使用 JDK8 编译。这不应该意味着它们都与 JRE8 兼容吗?是的,确实如此。那么错误从何而来呢?
Ubuntu哪里出了问题?
与大多数其他 Linux 发行版一样,Ubuntu 喜欢构建自己的软件包,并通过其软件包管理器提供这些软件包。这样做是为了确保操作系统生态系统中的一切正常运行,这通常意味着修补上游项目的源代码。
特别是对于 Scala 包,Ubuntu 决定放弃用于编译 Scala 源代码的 JDK 版本的上游选择,并一段时间以来一直在使用较新的 JDK 版本来编译 Ubuntu 的 Scala 包 https://launchpad.net/ubuntu/+source/scala/+changelog.
如果我们检查清单文件scala-compiler.jar
在Ubuntu的Scala 2.11.12-4中,我们可以看到它是用JDK11编译的:
Created-By: 11.0.2+9-Ubuntu-3ubuntu1 (Oracle Corporation)
Bundle-Name: Scala Distribution
Bundle-SymbolicName: org.scala-ide.scala.compiler;singleton:=true
Bundle-Version: 2.11.12
你不是说这个问题在2.11.12-4已经解决了吗?是的,我做到了。
Ubuntu针对这个问题的解决方案是不使用JDK8编译Scala,而是对Scala的源代码进行补丁,避免直接在子类中调用有问题的方法。这是通过在调用这些方法之前将 ByteBuffer(和 CharBuffer)转换为其超类 Buffer 来实现的。实际上,这意味着将 Scala 的源代码从bytes.clear()
to bytes.asInstanceOf[Buffer].clear().asInstanceOf[ByteBuffer]
(不知道为什么当结果来自时他们将其转换回 ByteBufferclear()
好像根本没用过)。这是Ubuntu的补丁 http://launchpadlibrarian.net/398758355/scala_2.11.12-3_2.11.12-4.diff.gz.
Ubuntu 的方法似乎有点危险,因为其他不兼容的来源可能会被忽视,并且在某些非常特定的情况下仍然存在等待发生。此外,他们自己的设置与官方 Scala 版本不同,意味着整个 Scala 社区无法在实际场景中测试这些更改。