Today's blog post will be on the slightly weird side, because as Scala developers we don't usually need to reach into reflection to solve problems at hand. It's easy to fight most problems with the "there's a monad for that!" tactic, or simply "adding yet another layer solves any software development problem!".
Sadly today's task is not easily solvable by just that. The problem we're going to tackle using Scala's reflection involves a scala object
and a private field we need to access.
// Mongo.scala from net.liftweb:lift-mongodb_2.10:2.5.1
object MongoDB {
/*
* HashMap of MongoAddresses, keyed by MongoIdentifier
*/
private val dbs = new ConcurrentHashMap[MongoIdentifier, MongoAddress]
/*
* Define a Mongo db using a standard Mongo instance.
*/
def defineDb(name: MongoIdentifier, mngo: Mongo, dbName: String) {
dbs.put(name, MongoAddress(new MongoHostBase { def mongo = mngo }, dbName))
}
def close {
dbs.clear
}
// ...
}
Problem statement: We need to support parallel integration tests (sbt running tests in parallel), using Mongo (or Fongo). The MongoDB
object though, contains globaly shared mutable state, in form of the dbs
Map, which is used to determine which MongoAddress
to hit for which entity.
We are able to setup the mongo identifiers up properly for the threads executing the tests, so they don't interfer with each other, and we won't focus on the setup part today. Instead let's focus on what happens during teardown. A naive implementation is a simple (ScalaTest) afterAll
, like this:
trait MongoFlatSpec extends FlatSpec with BeforeAndAfterAll {
var mongo: Mongo = _ // either Mongo or Fongo supplied instance here, starts up during beforeAll()
abstract override protected def afterAll() {
super.afterAll()
// now it's tempting to use
MongoDB.close() // NOPE! Won't allow parallel execution, take a look at it's impl above.
getMongo.close()
}
}
class ExampleTest extends MongoFlatSpec with ShouldMatchers {
// tests...
}
So sadly the naive implementation with using MongoDB.close()
is not enough for us. Why? Imagine Two threads, running two MongoFlatSpec
s, assume each has it's own in memory Fongo instance even. There still is tha shared MongoDB.dbs
field deep in lift-mongodb's internals. If we'd use MongoDB.close()
we'll clean all MongoIdentifier
s, so we might break tests which are still in flight... We could try something among the lines of "wait for the right moment to clean up", but ... why wait!? There's a ConcurrentHashMap in there, and we just want to remove "the one MongoIdentifier I have been using for this MongoFlatSpec".
Reflection to the rescue! So we'll have to use reflection, in order to execute such statement: MongoDB.dbs.remove(myMongoIdentifier)
. The first problem is that we're dealing with a Scala object
, also known as "Module". Why is it also known as Module? Let's take a look into the ByteCode!
scala> :javap MongoDB
public class MongoDB$ extends java.lang.Object {
public static final MongoDB$ MODULE$; // actual instance of our `object`
public static {};
public MongoDB$();
// ...
}
Since we want the dbs
field, we will have to go through the static final MongoDB$ MODULE$
field of the MongoDB$
class. This is not so nice via plain Java reflection. Let's see how Scala's (still experimental) reflection can make this a bit nicer:
def removeFongoIdentifierFromMongoDBObject(ident: MongoIdentifier) {
val ru = scala.reflect.runtime.universe
val mirror = ru.runtimeMirror(getClass.getClassLoader)
type MongosMap = ConcurrentHashMap[MongoIdentifier, MongoAddress]
val mongoModuleSymbol = ru.typeOf[MongoDB.type].termSymbol.asModule
val moduleMirror = mirror.reflectModule(mongoModuleSymbol)
val instanceMirror = mirror.reflect(moduleMirror.instance)
val dbsTerm = ru.typeOf[MongoDB.type].declaration(ru.newTermName("dbs")).asTerm.accessed.asTerm
val fieldMirror = instanceMirror.reflectField(dbsTerm)
val mongosMap = fieldMirror.get.asInstanceOf[MongosMap]
mongosMap remove ident
}
As you can see, we were able to avoid going through the magic MODULE$
constant explicitly, it was abstracted away from us thanks to the types of mirrors we've been using - it's also worth noting that for example the reflectModule(ModuleSymbol)
takes a ModuleSymbol
which is obtained via the "safe cast", as one might think of it in the line above using the asModule
method. The nice thing about asModule
is that it will fail if the term you're calling it on is not a module. So the API is both designed to be as typesafe as possible, even in reflection-lang, as well as "fail fast" if it detects you're doing something wrong.
For more docs or further reference check out the docs about scala-reflection or ScalaDoc about scala.reflect.api.Mirrors.