larsrh
larsrh

Reputation: 2659

Using a custom class loader for a module dependency in SBT

I have a multi-module SBT build consisting of api, core and third-party. The structure is roughly this:

api
|- core
|- third-party

The code for third-party implements api and is copied verbatim from somewhere else, so I don't really want to touch it.

Because of the way third-party is implemented (heavy use of singletons), I can't just have core depend on third-party. Specifically, I only need to use it via the api, but I need to have multiple, isolated copies of third-party at runtime. (This allows me to have multiple "singletons" at the same time.)

If I'm running outside of my SBT build, I just do this:

def createInstance(): foo.bar.API = {
  val loader = new java.net.URLClassLoader("path/to/third-party.jar", parent)
  loader.loadClass("foo.bar.Impl").asSubclass(classOf[foo.bar.API]).newInstance()
}

But the problem is that I don't know how to figure out at runtime what I should give as an argument to URLClassLoader if I'm running via sbt core/run.

Upvotes: 9

Views: 2050

Answers (1)

knutwalker
knutwalker

Reputation: 5974

This should work, though I didn't quite tested it with your setup.

The basic idea is to let sbt write the classpath into a file that you can use at runtime. sbt-buildinfo already provides a good basis for this, so I'm gonna use it here, but you might extract just the relevant part and not use this plugin as well.

Add this to your project definition:

lazy val core = project enablePlugins BuildInfoPlugin settings (
  buildInfoKeys := Seq(BuildInfoKey.map(exportedProducts in (`third-party`, Runtime)) {
    case (_, classFiles) ⇒ ("thirdParty", classFiles.map(_.data.toURI.toURL)) 
  })
  ...

At runtime, use this:

def createInstance(): foo.bar.API = {
  val loader = new java.net.URLClassLoader(buildinfo.BuildInfo.thirdParty.toArray, parent)
  loader.loadClass("foo.bar.Impl").asSubclass(classOf[foo.bar.API]).newInstance()
}

exportedProducts only contains the compiled classes for the project (e.g. .../target/scala-2.10/classes/). Depending on your setup, you might want to use fullClasspath instead (which also contains the libraryDependencies and dependent projects) or any other classpath related key.

Upvotes: 6

Related Questions