Play 2.0 コードリーディング - プラグインの読み込み

最近はボルダリングに嵌っています。

提示された課題に対して、自身が持つリソースを使っていかに解決するかを
考えるのはプログラミングに近い楽しさだなぁ、とは思っていません。


先日、「Play Framework 2.0 ソースコードリーディングの会」でなんか話せって言われました。

http://partake.in/events/310d72cb-b5b2-4503-98c7-df5887582c27

話す内容はまだ決っていません。

そんなわけでPlay 2.0のコードをぼちぼち読み始めているのですが、
折角なのでその内容を残しておこうと思いました。
一人Play Framework 2.0 ソースコードリーディングの会結成の瞬間です。

記念すべき第一回はModule(Plugin)の読み込みです。
いきなりマニアックな部分から始まっているのは僕がたまたま興味があったからです。

Moduleを自作する方法についてはfitsさんの記事など参照してください。

http://d.hatena.ne.jp/fits/20120324/1332584629


さて、Moduleの読み込みはplay.api.Applicationが行なわれます。
まず始めに、Application.pluginClassesメソッドを使用して設定ファイルを読み込みます。

class Application {
  private[api] def pluginClasses: Seq[String] = {

    import scalax.file._
    import scalax.io.JavaConverters._
    import scala.collection.JavaConverters._

    val PluginDeclaration = """([0-9_]+):(.*)""".r

    val pluginFiles = 
      classloader.getResources("play.plugins").asScala.toList ++ 
      classloader.getResources("conf/play.plugins").asScala.toList

    pluginFiles.distinct.map { plugins =>
      (plugins.asInput.slurpString.split("\n").map(_.trim))
          .filterNot(_.isEmpty).map {
        
        case PluginDeclaration(priority, className) => 
          (priority.toInt, className)
      }
    }.flatten.sortBy(_._1).map(_._2)
  }

設定ファイルのフォーマットは 450:plugins.SamplePlugin のように、
読み込み順のプライオリティとクラス名をコロン区切りにしたもので、
正規表現を使ってこれをパースし、プライオリティ順に並びかえています。
実際にpluginClassesを使っているのは、同じくApplicationのpluginsを定義する所です。

class Application {
  val plugins: Seq[Plugin] = Threads.withContextClassLoader(classloader) {
    pluginClasses.map { className =>
      try {
        val plugin = classloader.loadClass(className)
              .getConstructor(classOf[Application])
              .newInstance(this).asInstanceOf[Plugin]
        if (plugin.enabled)
          Some(plugin)
      } catch {
        case e: java.lang.NoSuchMethodException => {
          val plugin = classloader.loadClass(className)
                .getConstructor(classOf[play.Application])
                .newInstance(new play.Application(this))
                .asInstanceOf[Plugin]
          if (plugin.enabled)
            Some(plugin)
        }
      }
    }.flatten
  }
}

pluginClassesで作ったクラス名のインスタンスを
ClassLoaderとリフレクションを使って順番に生成しています。

NoSuchMethodExceptionが発生した時は、
引数をplay.Applicationに変更して再実行していますが、
play.Applicationはjavaで書かれており、
恐らくjavaで書かれたModuleを読み込むためのものだと思います。


ここまででModuleの読み込みは完了です。

後は実際に、 play.api.Play.start や play.api.Play.stop によるハンドラの実行や、

object Play {
  def start(app: Application) {
    // First stop previous app if exists
    stop()

    _currentApp = app

    app.plugins.foreach(_.onStart)

    app.mode match {
      case Mode.Test =>
      case mode => 
        Logger("play").info("Application started (" + mode + ")")
    }
  }

  def stop() {
    Option(_currentApp).map {
      _.plugins.foreach { p =>
        try { p.onStop } catch { case _ => }
      }
    }
    _currentApp = null
  }
}

play.api.Application.plugin を使ったインスタンスの取得が行なわれます。

class Application {
  def plugin[T](implicit m: Manifest[T]): Option[T] =
    plugin(m.erasure).asInstanceOf[Option[T]]

  def plugin[T](pluginClass: Class[T]): Option[T] =
    plugins
      .find(p => pluginClass.isAssignableFrom(p.getClass))
      .map(_.asInstanceOf[T])
}