2013年12月7日土曜日

Play Frameworkでサブプロジェクトを使い倒す

Play framework 2.x Java and 1.x Advent Calendar 2013の7日目です。

さて今日はサブプロジェクトをうまいこと使おうよ、というお話です。
1つのプロジェクト内のcontrollerにadminディレクトリを掘っていた時期もありました。。。
運用の話はどちらかと言うと、他の人から色々聞いてみたい。。。多分自分のスクリプトはかなりしょっぱい。。。。(メモリエラー対策のため1時間に1回Jenkinsでサービスの再起動をさせている・・・)


で、今回の話はPlayのドキュメントだとWorking with sub-projectsに当たる部分になります。
Playはsbtのmulti-buildをよろしく使っているようです。
間違っている部分やもっといい方法などがあれば指摘していただけると嬉しいです。

Playは個人プロジェクトとか小規模なプロジェクトでこそ、その開発スピードなどの威力を発揮していると個人的には感じているのですが、チームで大規模な開発をしていくケースもあるかと思います。そういった場合にこのサブプロジェクトが多いに役立つのではないかと思います。



対象者によってディレクトリを区切るケース


例えばPlayで求人のマッチング系のサービスを作っていくというケースを考えてみます。
  • 求職者側は http://hoge.com/
  • 採用者側は http://hoge.com/staff
  • サービス運用者側は http://hoge.com/manager
のURLで使うケースを想定してステップごとにサブプロジェクト対応をしていく方法を考えてみます。



STEP1.ライブラリのサブプロジェクト化


Javaの開発をやっていく中でそのチームで共通的に使用しているライブラリ的な物があるかと思います。
例えば、日付のフォーマッターやUser-Agentの切り分けルール、テキストにwbrを埋め込む処理などのPlayにかかわらず使用できるライブラリです。
これらの資産をPlayでも使っていく場合に、なるべくパッケージ名は固定にしたいとか、プロジェクトに依存せずに使いたい、という要望が出てくるかと思います。

そんな時に使うのがサブプロジェクトになります。
もちろんプラグインとして利用する、jarファイルとして読み込む、などの方法も可能かと思います。ただプロジェクトを進める中で共通ライブラリも合わせて強化してフィードバックをしていく場合は、サププロジェクト化してすぐに編集できる状態にしておくのも有用かと思います。

自前ライブラリはproject/Build.scala に以下のように追記することで、Playの他のプロジェクトに依存せずに使えるようになります。
import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName = "hoge.com"
  val appVersion = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean)

  val common = Project(appName + "-common", appVersion, appDependencies, path = file("modules/common"))

  lazy val main = play.Project(appName, appVersion, appDependencies).settings( // Add your own project settings here
  ).dependsOn(common)

}


やっている内容としては、

  • val common の行でサブプロジェクトの追加
  • lazy val main にdependsOn(common)で依存関係の追加

になります。
この場合のフォルダ構成は以下のようになります。

app
conf
public
modules
 └ common
    └ src
       └ main
           └ java
project
 └ Build.scala

Adding a simple library sub-projectを見てみると、サブプロジェクトにすることで、コンパイルする単位をサブプロジェクトごとに出来るようです。Java,Scalaそれぞれ100ファイルを超えてくるとコンパイルに時間がかかるので、これは地味に嬉しい・・・。まあデバッグモードだと依存関係のあるプロジェクトもビルドされ直すようですが。


STEP2.Playの拡張機能のサブプロジェクト化


Playで開発をしていると、Controllerに共通の処理を埋め込みたくなる場合があると思います。セッション系の処理をゴニョゴニョしたりサイトのtitleを出力させたりFacebook用のタグを生成したりなどなど全ページ共通処理を行う場合ですね。
そんな時にはControllerをextendsしたBaseControllerを作って、プロジェクトではそれをさらにextendして各種Controllerを呼ぶ、なんてことがあると思います(この基底になるプロジェクトをbaseプロジェクトとします)。
この場合だとsbtプロジェクトではなくなり、Playプロジェクトになるのでproject/Build.scala に以下のように記載することになります。

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName = "hoge.com"
  val appVersion = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean)

  val common = Project(appName + "-common", appVersion, appDependencies, path = file("modules/common"))
  lazy val base = play.Project(appName + "-base", appVersion, appDependencies, path = file("modules/base"))

  lazy val main = play.Project(appName, appVersion, appDependencies).settings( // Add your own project settings here
  ).dependsOn(common,base).aggregate(base)

}

lazy val baseの行にありますが、プロジェクトが Projectからplay.Projectになりました。Playに依存するプロジェクトの場合、play.Projectにする必要があります。

Playは必要最低限の準備しかしてないので、みんな独自の拡張をしていると思います。
これで自分用のPlayプロジェクトのベースが準備出来ました。
このサブプロジェクトに直接アクセスされちゃうんじゃないの?という心配ですが、routesファイルが記載されていなければ、大丈夫かと思います。

STEP3.ディレクトリ区切りのサブプロジェクト化

さて、チームで開発していく中で問題になるのが、routesファイルのコンフリクト問題です。
モジュール(controller)別で開発担当者が違う場合にroutesファイルがコンフリクトを起こしまくって色々な感情と問題を引き起します。

そんな時にサブプロジェクトにします。そうすることで多少なりともroutesファイルのコンフリクトを避ける事が出来るようになります。

先述の
  • 求職者側は http://hoge.com/
  • 採用者側は http://hoge.com/staff
  • サービス運用者側は http://hoge.com/manager
の場合、staff,managerをそれぞれサブプロジェクトにするイメージです。
この場合もサブプロジェクトはsbtのプロジェクトではなくPlayのプロジェクトになります。
・・・とここで気がつくわけです。Ebeanの処理は共通ではないだろうか・・・?と。
Modelは求職者側だろうと採用者側だろうとだろうと同じになります。プロジェクト内で共通になるModelをさらに別出ししてサブプロジェクト(この場合、coreプロジェクトとします)にします。

project/Build.scala に以下のように記載することになるかと思います。
import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName = "hoge.com"
  val appVersion = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean)

  val common = Project(appName + "-common", appVersion, appDependencies, path = file("modules/common"))
  lazy val base = play.Project(appName + "-base", appVersion, appDependencies, path = file("modules/base"))
  lazy val core = play.Project(appName + "-core", appVersion, appDependencies, path = file("modules/core"))

  lazy val staff = play.Project(appName + "-staff", appVersion, appDependencies, path = file("modules/staff")).dependsOn(common,base,core)
  lazy val manager = play.Project(appName + "-manager", appVersion, appDependencies, path = file("modules/manager")).dependsOn(common,base,core)

  lazy val main = play.Project(appName, appVersion, appDependencies).settings( // Add your own project settings here
  ).dependsOn(common,base,core,staff, manager).aggregate(base, core,staff, manager)

}

これでroutesファイルをサブプロジェクトごとに分割して管理できるようになりました。routesファイルもこのようにして分けることが出来るようになります。

conf/routesはこうなります。
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.Application.index()

->  /staff staff.Routes
->  /manager manager.Routes

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file)

上記の/staff staff.Routesで指定されていた、modules/staff/conf/staff.routesはこうなります。
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                           controllers.staff.Application.index()
GET     /detail/:id     controllers.staff.Detail.index(id:Long, page: Integer ?= 1)


# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file)

staff.routesはあらかじめprefexとして /staff がつくことがconf/routesで指定されているので、modules/staff/conf/staff.routesに記載されている
GET     /detail/:id     controllers.staff.Detail.index(id:Long, page: Integer ?= 1)
は実質的に
http://hoge.com/staff/detail/1
とかのURLに該当するようになります。

ちなみにhttp://www.playframework.com/documentation/2.1.x/SBTSubProjectsにあるように、
GET     /assets/*file               controllers.admin.Assets.at(path="/public", file)
のような書き方は試してみたもののうまく行かず。。サブプロジェクごとにAssetsを指定することで、読み込むディレクトリを切り替えられる(?)みたいです。
jQueryとかbootstrapは共通の呼び出してるからいいか・・と
GET     /assets/*file               controllers.Assets.at(path="/public", file)
のように共通の場所を呼び出す形で放置をしています。

このようにサブプロジェクトに分けることで、プロジェクトごとに開発担当者を分けて開発をしやすくなります。

最終的にディレクトリ構成はこのようになります。

app
  └ controllers
  └ models
  └ views
conf
  └ application.conf
  └ routes
modules
  └ base
    └ app/controllers
 └ common
    └ src
       └ main
           └ java
  └ core
    └ app/models
  └ manager
    └ conf/manager.routes
    └ app/controllers
    └ app/views  
  └ staff
    └ conf/staff.routes
    └ app/controllers
    └ app/views  
project
 └ build.properties
 └ Build.scala
 └ plugins.sbt


このようにサブプロジェクトにすることで、大規模開発もしやすくなるのではないかと思います。

明日は


@s_kozakeさんが 中堅SIerがPlay1系を導入した感想を とのことです。
Play1系は触ったことがないのですが、Play2系とはまた違って業務向けにはいいという話を聞くので、楽しみです。