Building a Plugin Based Architecture in Scala

Posted on Thursday, September 15, 2011

0


A plugin based architecture has many advantages. Some of the common ones include

  • Extending an application’s functionality without compiling it again
  • Adding functionality without requiring access to the original source code.
  • Replacing or adding new functionality becomes easy
  • Help in organizing large projects
  • Help in extending the functionality of the system to unimagined areas. I know this one sounds extreme but believe me it is true. For our framework, plugins were written by third-party library providers whose domain ranged from health-care to natural language processing.

The major components of our plugin system are the following

  1. Plugin trait – Which all the plugins would extend
  2. PluginManager Object – Responsible for loading the plugin at the time of instantiation of the framework
  3. MessageContext trait – All messages would extend this trait. This makes easy to pass a variety of messages to the plugin without restricting it to a few

Let us look at each of these one by one. Our Plugin trait looks like this

trait VajraPlugin {

  def name:String
  def performAction(context:MessageContext):Boolean
  def result:String

}

As you would notice, the implementation class would be defining a name, it would have an implementation for a performAction method which takes input as a MessageContext. We expect all our plugins to finally return the result with the result method.

A sample implementation of the plugin would look like this

class ZeroToHalfPlugin extends VajraPlugin {

  def name(): String = { "ZeroToHalf" }

  def performAction(context:MessageContext):Boolean = {
    val sleepTime = java.lang.Math.random() * 501
    val payload = context.getPayload()
    println("")
    println("I am busy for " + sleepTime/1000 + " seconds")
    Thread.sleep(sleepTime.toLong)
    println("")
    return true
  }

  def result(): String = { //return some result here }
}

Now, let us look at the Plugin manager

object PluginManager {

  var pluginMap = Map[String, String]()
  val logger = Logger(PluginManager.getClass())

  def init() {
    val classpath = List(".").map(new File(_))
    val finder = ClassFinder(classpath)
    val classes = finder.getClasses
    val classMap = ClassFinder.classInfoMap(classes)
    val plugins = ClassFinder.concreteSubclasses("com.vajra.plugin.VajraPlugin", classMap)

    plugins.foreach {
      pluginString =>
        val plugin = Class.forName(pluginString.name).newInstance().asInstanceOf[VajraPMPlugin]
        pluginMap += (plugin.name -> pluginString.name)
    }
  }

  def getPlugin(name: String): VajraPlugin = {
    if (pluginMap.isEmpty) init
    logger.debug("Fetching plugin from non-persistent pool.")
    Class.forName(pluginMap(name)).newInstance().asInstanceOf[VajraPMPlugin]

  }

Now, the consumer who needs to use a plugin,  would come to the PluginManager and ask for a plugin via the getPlugin() method. If you remember, each plugin defines its name.

When this method is called, the managers checks if the plugin map is empty. If it is then the init is called where we load the plugins into the framework. Loading the plugins is done on the basis of all classes which are extending the
com.vajra.plugin.VajraPlugin trait. For this, we use are using a utility (ClassFinder) provided by Clapper. More details can be found here.

The idea is simple. As soon as we are starting the framework, we should not have to specify the plugins that the framework would be using anywhere in any configuration. The framework should be able to pick up the plugins automatically. In our case, we pick up all the plugins which are implementing our plugin trait i.e. VajraPlugin

The last piece in our puzzle is the message which is passed to the plugin. Since we want to keep this also extensible, instead of passing the message directly, we use a trait MessageContext.

trait MessageContext {

  def toXml(): Elem
  def setPayload(payload:Payload): Unit
  def getPayload(): Payload
  def addOrAppendElement(elementName: String, data: String)
  def addOrReplaceElement(elementName: String, elementData: String)
  def getElementData(elementName: String): String

}

As you would notice we have a number of utility methods here which all the messages would be implementing. We have a large number of methods here because they are relevant to our system. You could have the relevant methods for your system. A typical message which extends the MessageContext would look like this

class MessageContextAdapter extends MessageContext {

  def toXml(): Elem = {
    null
  }
  def setPayload(payload: Payload): Unit = {}
  def getPayload(): Payload = { null }
  def addOrAppendElement(elementName: String, data: String) = {}
  def addOrReplaceElement(elementName: String, elementData: String) = {}
  def getElementData(elementName: String): String = { "" }
  def getPlugin(): Plugin = {
    return null
  }
  def removeStep {}
}

The main takeaways are that you should definitely have all the plugins extend a trait. The plugin manager is responsible for creation, destruction and maintenance of plugins. The object passed to the plugin should (could) be a trait so that the framework can be extensible.

Posted in: Architecture, Scala