sbt 使用多个类加载器

sbt using multiple classloaders

Sbt 似乎使用了不同的 classloader,当在 sbt 会话中多次 运行 时,一些测试会失败,并出现以下错误:

[info]   java.lang.ClassCastException: net.i2p.crypto.eddsa.EdDSAPublicKey cannot be cast to net.i2p.crypto.eddsa.EdDSAPublicKey
[info]   at com.advancedtelematic.libtuf.crypt.EdcKeyPair$.generate(RsaKeyPair.scala:120)

使用模式匹配而不是 asInstanceOf 我得到了相同的结果。

如何确保 sbt 对同一会话中的所有测试执行使用相同的 class 加载器?

我认为这与此有关:Do security providers cause ClassLoader leaks in Java?。基本上 Security 是重新使用旧的 class-加载器的提供程序。所以这可能发生在任何多 class 路径环境(如 OSGi)中,而不仅仅是 SBT。

为您的 build.sbt 修复(不分叉):

testOptions in Test += Tests.Cleanup(() => 
  java.security.Security.removeProvider("BC"))

实验:

sbt-classloader-issue$ sbt
> test
[success] Total time: 1 s, completed Jul 6, 2017 11:43:53 PM
> test
[success] Total time: 0 s, completed Jul 6, 2017 11:43:55 PM

解释:

从你的代码中可以看出(已发布here):

Security.addProvider(new BouncyCastleProvider)

您每次 运行 测试时都在重复使用相同的 BouncyCastleProvider 提供程序,因为您的 Security.addProvider 有效 only first time。由于 sbt 为每个 "test" 运行 创建新的 class-loader,但重新使用相同的 JVM - Security 是一种 JVM 范围的单例,因为它是由JVM-bootstrap,所以classOf[java.security.Security].getClassLoader() == null和sbt不能reload/reinitialize这个class。

而且您可以轻松检查

classOf[org.bouncycastle.jce.spec.ECParameterSpec].getClassLoader()
res30: ClassLoader = URLClassLoader with NativeCopyLoader with RawResources

org.bouncycastle classes 加载了自定义 classloader(来自 sbt),每次你 运行 test.

所以这段代码:

val generator = KeyPairGenerator.getInstance("ECDSA", "BC")

获取从旧的 classloader 加载的 class 的实例(用于第一个 "test" 运行 的那个)并且你正在尝试使用规范初始化它新 class 装载机:

generator.initialize(ecSpec)

这就是您遇到 "parameter object not a ECParameterSpec" 异常的原因。 "net.i2p.crypto.eddsa.EdDSAPublicKey cannot be cast to net.i2p.crypto.eddsa.EdDSAPublicKey"的推理基本相同。