Search code examples
scalasbtclassloader

sbt using multiple classloaders


Sbt seems to be using different classloaders, making some tests failing when run more than once in an sbt session, with the following error:

[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)

I tried equivalent code using pattern matching instead of asInstanceOf and I get the same result.

How can I make sure sbt uses the same class loader for all test executions in the same session?


Solution

  • I think it's related to this: Do security providers cause ClassLoader leaks in Java?. Basically Security is re-using providers from old class-loaders. So this could happen in any multi-classpath environment (like OSGi), not just SBT.

    Fix for your build.sbt (without forking):

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

    Experiment:

    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
    

    Explanation:

    As I can see from your code (published here):

    Security.addProvider(new BouncyCastleProvider)
    

    you're reusing the same BouncyCastleProvider provider every-time you run a test, as your Security.addProvider works only first time. As sbt creates new class-loader for every "test" run, but re-uses the same JVM - Security is kind-of JVM-scoped singleton as it was loaded by JVM-bootstrap, so classOf[java.security.Security].getClassLoader() == null and sbt cannot reload/reinitialize this class.

    And you can easily check that

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

    org.bouncycastle classes are loaded with custom classloader (from sbt) which changes every-time you run test.

    So this code:

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

    gets instance of class loaded from old classloader (the one used for first "test" run) and you're trying to initialize it with spec from new classloader:

    generator.initialize(ecSpec)
    

    That's why you're getting "parameter object not a ECParameterSpec" exception. The reasoning around "net.i2p.crypto.eddsa.EdDSAPublicKey cannot be cast to net.i2p.crypto.eddsa.EdDSAPublicKey" is basically same.