decadence

個人のメモ帳

Play2.3のPlugin

Scala + Play framework 2.3 ってとこでアルバイト初めて一月半ぐらい経とうとしてる。 OOSC読んだり、ポスター発表してたりもしたけど落ち着き始めたので、Playの中身ちゃんと知ろうとか考えてる

Playで設定を読み込み、リクエストを受け付ける前にする云々、特にPluginを見る。 PlayはPluginという機構を使って様々な前準備及び終了時の後処理を行う。 (コレ以外にもDBPluginなど直接Application.pluginを用いて呼び出す事も出来るが今回の範疇ではない)

playframeworkが提供している様々なPluginについて見て行きたいと思うが、本題に入る前にPluginとは何なのかにあたる部分について触れる

ちなみに2.4からはPluginの機構は使われなくなるので、これは2.3.xまでの話。 (さすがにまだ2.4系を本番で使ってる人はいない...?) 一応2.4系はplay moduleってのをDIしてく、って世界観らしい

以下で触れるのは2.3.xブランチのこのcommit上の話、ただコードを読むだけ

前振り

PlayApplication

  • consoleとかで使える
    • StaticApplication extends ApplicationProvider
  • Test Mode
    • play.core.TestApplication extends ApplicationProvider
  • Dev Mode
    • play.core.ReloadableApplication extends ApplicationProvider

これらの中でPlay.start(application)が呼ばれてる。 startの中では、routesを遅延評価で構築したり、pluginを順に読み込んだりしている(読み込むpluginについては後述)。 applicationはplay.api.DefaultApplciationなどが用いられ、色んな設定とかこいつが用意してる

play.api.DefaultApplication

class DefaultApplication(
  override val path: File,
  override val classloader: ClassLoader,
  override val sources: Option[SourceMapper],
  override val mode: Mode.Mode) extends Application with WithDefaultConfiguration with WithDefaultGlobal with WithDefaultPlugins

Application, WithDefaultConfiguration, WithDefaultGlobal, WithDefaultPluginsで成り立ってる

  • Applicationの主要な構成要素

    • Applicationのルートとなるpath
    • Globalなどのクラスを読み込むためのclassloader
    • エラー時にブラウザにソースを載せるために用いられるsources
    • 実行時のmode(Dev/Test/Prod)を示すmode
    • Global.scalaから得られるglobal
      • これは後ほど述べるpluginsの重み10000で実行されるものとなる
      • getControllerInstanceとかあったのか
    • application.confなどを読み込み生成されるconfiguration
    • アプリケーション実行時などに利用されるplugins
    • 後のPlay.start時に評価されるルーティングを保持するroutes
    • loggerの設定
    • エラーハンドル
    • そしてpluginメソッドによる別途Pluginの呼び出し(今回の対象外)
    • ...
  • WithDefaultConfigurationとは

    • Applicationをself type annotationとして、上記configurationを読み込むtrait
    • com.typesafe.config.Config軽くをラップしたものを利用している
  • WithDefaultGlobalとは

    • WithDefaultConfigurationを持つApplicationをself type annotationに持つtrait
    • configurationに書かれた設定からGlobalPluginとなるクラスを探す
    • 特に指定がなければ基本的なエラー処理等のみが定義されたGlobalSettingsをそのまま用いるDefaultGlobalが用いられる
    • globalInstanceではjavaGlobalを取りにいき、無ければscalaGlobalを取りに行く
    • 得られたものはGlobalPluginとして、実行される
  • WithDefaultPluginsとは

    • 設定ファイル(play.plugins)に書かれた文字列から各種Pluginをclassloaderを用いて用意する
    • 自身が定義したPluginや、先に説明したGlobalPlugin、その他様々なライブラリが提供するPlayのPluginを用意する

playframework/playframeworkのプロジェクトにはいくつものサブプロジェクトなども含まれているが、通して以下のようなplugin設定ファイルが存在する

./framework/src/play/src/main/resources/play.plugins
./framework/src/play-cache/src/main/resources/play.plugins
./framework/src/play-java/src/main/resources/play.plugins
./framework/src/play-java-ebean/src/main/resources/play.plugins
./framework/src/play-java-jdbc/src/main/resources/play.plugins
./framework/src/play-java-jpa/src/main/resources/play.plugins
./framework/src/play-java-ws/src/main/resources/play.plugins
./framework/src/play-jdbc/src/main/resources/play.plugins
./framework/src/play-ws/src/main/resources/play.plugins
./framework/test/integrationtest/conf/play.plugins

Pluginについて

ここまでで、設定や各種Pluginを使う用意が出来た。 では、Pluginとは何なのか。

見やすいようにdocumentを削ったりしたが、以下のような宣言がなされている。 全てのPluginはこのPluginを実装している。

trait Plugin {
  // Called when the application starts.
  def onStart() {}

  // Called when the application stops.
  def onStop() {}

  // Is the plugin enabled?
  def enabled: Boolean = true
}

そして、Play.startの中でonStart()が、Play.stopの中でonStop()が順に実行される。

ここで順に、と述べたのば、pluginの設定ファイルの行頭についた数字がpriorityとなり値の小さい順onStart()では実行される。 逆にPlay.stopでは数字の大きい順にPluginのonStop()が実行される。

Playframeworkのドキュメントの中でも説明がある通り、他のPluginに依存するPluginを作る際にはこのpriorityに注意する必要がある。 特に他のPluginに影響を及ぼさないPluginならば10000より大きいpriorityを設定するのが良い。

実行前には各種pluginが設定ファイルなどを眺めてenabledとなるもののみが実行される。 例えばなぜかEhCachePluginが入った状態で、mumoshu/play2-memcached - GitHubを使いたい場合には、READMEにある通り、application.confにenhcacheplugin=desabledと書いてEhCachePluginが実行されないように出来る。

各種Pluginを眺める

libraryDependenciesに何も書かないPlay projectの場合./framework/src/play/src/main/resources/play.pluginsに書かれたPluginのみが実行される。 jdbcをlibryDependenciesに含めた場合には./framework/src/play-jdbc/src/main/resources/play.pluginsに書かれたPluginも実行される。 1つ1つ見ていくわけだが、全部見るのは億劫なので良く使うものだけ今回は眺める。

==> ./framework/src/play/src/main/resources/play.plugins <==
1:play.core.system.MigrationHelper
100:play.api.i18n.DefaultMessagesPlugin
1000:play.api.libs.concurrent.AkkaPlugin
10000:play.api.GlobalPlugin

==> ./framework/src/play-cache/src/main/resources/play.plugins <==
600:play.api.cache.EhCachePlugin

==> ./framework/src/play-jdbc/src/main/resources/play.plugins <==
200:play.api.db.BoneCPPlugin
500:play.api.db.evolutions.EvolutionsPlugin

こんなとこ

1:play.core.system.MigrationHelper

2.2.xにはなかったけど2.3.xになって増えたもの。 名前の通り、Play frameworkのversion移行時に設定ファイルの単位が変わった事で挙動が変わりそうな事を、起動前にチェックしwarningを出してくれる。 2.3.xへの移行時にチェックしてる内容はsession.maxAgeの単位が数値(integer)から期間表記(e.g. 1h 30m)になる事である。

100:play.api.i18n.MessagesPlugin

多言語用Pluginで、Lang.availablesでconfから有効なlangを取得し、多言語化用の変換Mapを用意する
有効なlangを用いたmessages.langmessagesmessages.defaultなどをパースして後者優先で上書きしたMapを作る
translate時はこいつを見てkeyを渡し、java.text.MessageFormatに食わせて変換結果を返す
MessageFormatがそのまま使われるからPlayframework の I18N で 1st, 2nd, 3rd, 4th, ... - Qiitaみたいなことも出来る

200:play.api.db.BoneCPPlugin

こちらのissueでHikariCPとどちらを使うかを争って、以前使われ続けてるBoneCP。 (知らなかったけど1週間前にこんなコメント増えてた)

DBへのコネクションを取得出来るDataSourceを作るのが目的

onStart()では設定ファイルの情報を基に、DBのコネクションを生成出来るかをチェックしている。 チェックする時にBoneCPApiオブジェクトが作られるわけだが、作られる際にDataSourceを作り、DBのコネクションプール周りの設定を入れまくったConnectionHookをこいつに登録している。 BoneCPの場合javax.sql.DataSourceを継承したBoneCPDataSourceを持っており、こいつからコネクションを取得する。 BoneCPDataSourceはBoneCP(ConnectionPool)を持っており...、これ以上は今回は関係ない

onStop()では全てのDataSourceを閉じる処理を行ってる、なるほど

ちなみに... slickのplay用Pluginplayframework/play-slick...play.plugins - GitHub*1はpriorityが400,401となっており、この箇所で実行される。 400の方では、(Playから使えるコネクション生成出来る)Databaseオブジェクトを全てclearして、401の方ではevolutionを生成している。 "# --- Created by "というコメントで始まるかどうかでevolutionをするかを判定してるのが意味わからん。 が、スキーマが変わる毎にevolutionを自動生成してくれるのはありがたい

ちなみに確認ではあるが... コネクションを取得するのはplay.api.DB.getConnectionからApplicaiton.plugin[DBPlugin].map(_.api.getConnection(...))とした時などに行われ、処理が終了した時に開放される。

500:play.api.db.evolutions.EvolutionsPlugin

priorityが500未満のPluginでevolutionファイルが生成されているはずである。 もしevolutionを吐くpluginを書くなら500未満にしなければ動かない。

設定のevolutions.use.locksを見てあればロックしながら、無ければロックせずにevolutionを実行する*2。 evolutionの管理はplay_evolutionsというテーブルを勝手に作って用いている。 これらはonStart()で行っており、onStop()では何もしていない。

600:play.api.cache.EhCachePlugin

お手軽メモリキャッシュ、EhCacheのためのPlugin。 初めにehcache.xmlのような設定ファイルを読みつつ、保存先を管理するCacheManagerを作成し、playと名づけた保存先を作る。 *3 onStop()では管理してるCacheManagerそのものを破棄してる。

1000:play.api.libs.akka.AkkaPlugin

onStart()は何もしない。 最初のAkka.system.actorOf[Props[MyActor]]のような呼び出しがあった時に初めてActorSystemが作られる。 ログにも出るんだけど、GlobalPluginのonStartでスケジューラなど起動してるから気が付かなかった... *4

`onStop()はActorSystem自体を閉じる

10000:play.api.GlobalPlugin

onStart()時にはユーザが定義したGlobalのonStart()を中で呼び出す。 onStop()時も同様にユーザが定義したGlobalのonStop()を中で呼び出す。

ちなみにユーザ定義のGlobalにだけbeforeStart()という関数があるのだが、これはGlobalPluginオブジェクトが生成された時に実行されるため、上記で上げたような各種PluginのonStart()が実行される前に行う処理を定義出来る。 逆にこの中ではDBに対する処理などは行えないので注意。

締め

2.4系にあげる前にplay moduleも読むか、play-core読むかNettyあたり読みたい

*1:0.8.xがPlay2.用

*2:マジか、知らなかった

*3:lazy valで定義しておいた変数を評価して実際に処理を実行させる感じ、なんとなく好きだ

*4:関係無いけど、actorOfした時にmakeChildで子供が作られるとこまで軽くみたら普通にmailboxみたいな変数やクラスがあってこれ例えで出した名前じゃなかったんだって