Search code examples
scalaunit-testingjunitclassloader

Class A cannot be cast to Class A after dynamic loading


Let's say I have:

object GLOBAL_OBJECT{
  var str = ""
}

class A(_str: String){
  GLOBAL_OBJECT.str = _str 
}

and I would like to create 2 copies of GLOBAL_OBJECT (for tests), so I am using different classloader to create obj2:

    val obj1 = new A("1")

    val class_loader = new CustomClassLoader()
    val clazz = class_loader.loadClass("my.packagename.A")

    val obj2 = clazz.getDeclaredConstructor(classOf[String]).newInstance("2")

    println("obj1.getSecret() == " + obj1.getSecret())                 // Expected: 1
    println("obj2.getSecret() == " + obj2.asInstanceOf[A].getSecret()) // Expected: 2

which results following error: my.packagename.A cannot be cast to my.packagename.A.

IntelliJ Idea seems to do it correctly, I can run obj2.asInstanceOf[A].getSecret() in "expression" window during debug process without errors.

PS. I have seen similar questions, but I could not find any not regarding loading class from .jarfile.


Solution

  • You're not going to be able to get around Java's class casting, which requires strict typing, within the same ClassLoader. Same with traits/interfaces.

    However, Scala comes to the rescue with structural typing (a.k.a. Duck Typing, as in "it quacks like a duck.") Instead of casting it to type A, cast it such that it has the method you want.

    Here's an example of a function which uses structural typing:

    def printSecret(name : String,  secretive : { def getSecret : String } ) {
      println(name+".getSecret = "+secretive.getSecret)
    }
    

    And here's sample usage:

    printSecret("obj1", obj1) // Expected: 1
    printSecret("obj2", obj2.asInstanceOf[ {def getSecret : String} ]) // Expected: 2
    

    You could, of course, just call

    println("secret: "+ obj2.asInstanceOf[ {def getSecret : String} ].getSecret
    

    Here's full sample code that I wrote and tested.

    Main code:

    object TestBootstrap {
    def createClassLoader() = new URLClassLoader(Array(new URL("file:///tmp/theTestCode.jar")))
    }
    
    trait TestRunner {
      def runTest()
    }
    
    object RunTest extends App {
      val testRunner = TestBootstrap.createClassLoader()
        .loadClass("my.sample.TestCodeNotInMainClassLoader")
        .newInstance()
        .asInstanceOf[TestRunner]
    
      testRunner.runTest()
    }
    

    In the separate JAR file:

      object GLOBAL_OBJECT {
        var str = ""
      }
    
      class A(_str: String) {
        println("A classloader: "+getClass.getClassLoader)
        println("GLOBAL classloader: "+GLOBAL_OBJECT.getClass.getClassLoader)
        GLOBAL_OBJECT.str = _str
    
        def getSecret : String = GLOBAL_OBJECT.str
      }
    
    
      class TestCodeNotInMainClassLoader extends TestRunner {
        def runTest() {
          println("Classloader for runTest: " + this.getClass.getClassLoader)
          val obj1 = new A("1")
    
          val classLoader1 = TestBootstrap.createClassLoader()
          val clazz = classLoader1.loadClass("com.vocalabs.A")
          val obj2 = clazz.getDeclaredConstructor(classOf[String]).newInstance("2")
    
          def printSecret(name : String,  secretive : { def getSecret : String } ) {
            println(name+".getSecret = "+secretive.getSecret)
          }
    
          printSecret("obj1", obj1) // Expected: 1
          printSecret("obj2", obj2.asInstanceOf[ {def getSecret : String} ]) // Expected: 2
        }
      }
    

    Structural typing can be used for more than one method, the methods are separated with semicolons. So essentially you create an interface for A with all the methods you intend to test. For example:

    type UnderTest = { def getSecret : String ; def myOtherMethod() : Unit }