Search code examples
javascalaclassloader

How can I use a Scala class as the shared root object across class loaders?


I have implemented a custom class loader in Scala that serves to isolate plugins from the main application. At present I require a Java interface to act as the shared root object so my main application can use the plugin code.

Interface acting as shared root (I would like this to be Scala):

public interface Handler {
    public List<HandlerInfo> getHandlers();
}

Example plugin:

class MyPlugin extends Handler {
  def getHandlers: java.util.List[HandlerInfo] = // get some handlers
}

Usage in application:

val jarFile     = new File(System.getProperty("user.dir") + "/plugins/" + jarName)
val cl          = new PluginLoader(jarFile, this.getClass.getClassLoader) // my custom classloader
val classToLoad = Class.forName(className, true, cl)
val handler     = classToLoad.newInstance.asInstanceOf[Handler]
val handlers    = handler.getHandlers

This works fine, but my problem is that I have to keep this one Java class around (and the resulting build configuration). I would like instead to use a Scala trait or abstract class, like this:

trait Handler {
  def getHandlers : List[HandlerInfo]
}

Then my plugin could look like this:

class MyPlugin extends Handler {
  def getHandlers: List[HandlerInfo] = // no more java.util.List
}

But I can't do this, because this line

val handler     = classToLoad.newInstance.asInstanceOf[Handler]

throws a ClassCastException, presumably because the Scala compiler doesn't generate a nice clean Java interface. Is there any way around this, so I can have a Scala-only project?


Solution

  • The problem is not where you think it is; it's somewhere in your class loader. The default class loader works just fine, even the Java one. Evidence:

    // File Handler.scala
    trait Handler { def getHandlers: List[String] }
    
    // File MyPlugin.scala
    class MyPlugin extends Handler { def getHandlers = List("salmon", "cod") }
    
    // File Interop.java
    public class Interop {
      public static void main(String[] args) {
        try {
          Class classToLoad = Class.forName("MyPlugin");
          Handler handler = (Handler)classToLoad.newInstance();
          System.out.println("Class loader = "+handler.getClass().getClassLoader());
          System.out.println(handler.getHandlers());
        }
        catch (Exception e) { System.out.println("Uh-oh: "+e); }
      }
    }
    

    And here we run it:

    $ java -cp .:/usr/share/scala/2.10/lib/scala-library.jar Interop
    Class loader = sun.misc.Launcher$AppClassLoader@1f3e8d89
    List(salmon, cod)
    $
    

    with the Scala List and everything, and the new class loaded by the default Java class loader.