Screaming Loud

日々是精進

Play2.4で非推奨のGlobalSettingをなくす

Play2.4で非推奨、かつPlay2.5ではなくなっているGlobalSettingの消し方を紹介します。
なかなか大変なので、ブログにまとめておきました。

Global.scalaで書いていたコード

Play2.3で書いていたGlobal.scalaのコードを記載します。

object Global extends WithFilters(CsrfFilter, AccessLoggingFilter, SystemAccessFilter) with GlobalSettings with JsonHelper {

  override def onStart(app: Application) {
    Akka.system.scheduler.schedule(1 hour, 1 hour) {
      Image.cleanTempImage()
    }
  }

  override def onError(request: RequestHeader, ex: Throwable): Future[Result] = {
    Logger.warn(s"Internal server error. Messages : ${e.getMessage} ExceptionMessage: ${e.getMessage()}")
  }
}

/**
 * アクセスログ出力フィルター
 */
object AccessLoggingFilter extends Filter {
  import models.component.RequestComponent._

  val accessLogger = Logger("access")
  def apply(next: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = {
    val resultFuture = next(request)
    val startTime = System.currentTimeMillis

    resultFuture.flatMap { result =>
      val takeTime = System.currentTimeMillis - startTime
      val msg = s"method=${request.method} uri=${request.uri} remote-address=${request.ipAddr} status=${result.header.status} user-agent=${request.userAgent} takeTime=${takeTime} ms"

      if (takeTime >= 1000) accessLogger.warn(s"*** ${msg} ***")
      else accessLogger.info(msg)

      resultFuture
    } recover {
      case NonFatal(ex) => {
        val takeTime = System.currentTimeMillis - startTime
        val msg = s"method=${request.method} uri=${request.uri} remote-address=${request.ipAddr} status=500 user-agent=${request.userAgent} takeTime=${takeTime}"
        accessLogger.error(msg)
        throw ex
      }
    }
  }
}

onErrorを無くす

まず簡単なところから。

GlobalSettingを継承しているclassでonErrorが定義されていると思いますが、
そこをErrorHandlerに移します。

以下は2.3で書いていたことです。

def onError(request: RequestHeader, ex: Throwable): Future[Result] = {
   Logger.warn(s"Internal server error. Messages : ${e.getMessage} ExceptionMessage: ${e.getMessage()}")
}

新しくErrorHandlerというクラスを作ります。
アプリケーションのロジック内で自前で発生させた例外はonServerErrorに入るので、onServerErrorのコードが多くなると思います。
個人的には、controller系と同じパッケージに作るほうがいいと思います。

import play.api.http.HttpErrorHandler
import play.api.mvc._
import play.api.mvc.Results._
import scala.concurrent._

class ErrorHandler extends HttpErrorHandler {

  def onClientError(request: RequestHeader, statusCode: Int, message: String) = {
    Future.successful(
      Status(statusCode)("A client error occurred: " + message)
    )
  }

  def onServerError(request: RequestHeader, exception: Throwable) = {
    Future.successful(
      InternalServerError("A server error occurred: " + exception.getMessage)
    )
  }
}

また、作ったErrorHandlerが呼び出されるようにapplication.confに以下を追記しましょう。

play.http.errorHandler="controllers.ErrorHandler"

各パラメータに対して、細かく処理を設定するのであれば、以下リンクにあるようにDefaultErrorHandlerを継承して作ったほうがいいかもしれません。
ScalaErrorHandling - 2.4.x

Filter系

以下のようなFilterを統合するclassを作るだけでOKです。(objectの場合はclassにしてあげましょう)
AccessFilterの実装などは、2.3で書いていたものと同じでOKです。

Global.scalaなどでwithFilterを継承していた場合は、それを取り除きましょう。

package filters

import javax.inject.Inject
import play.api.http.HttpFilters

class Filters @Inject() (
    csrf: CsrfFilter,
    access: AccessLoggingFilter,
    system: SystemAccessFilter) extends HttpFilters {
  val filters = Seq(csrf, access, system)
}

onErrorと同じようにapplication.confに以下を追記します。

play.http.filters="filters.Filters"

onStart

onStartはちょっと面倒です。

まず以下のようなclassを作ります。
これは、実際に起動時に実行する処理を記載します。
その際、Singletonアノテーションをつけます。
Singletonアノテーションをつけることでobjectのように一度しか生成されなくなります。
じゃあscalaなんだからobjectでいいじゃねーか。ってなるかもしれませんが、objectだとInjectができないので、Play内部では使いづらくなってしまうので、classで定義しましょう。

package modules

import javax.inject.{ Inject, Singleton }
import akka.actor.ActorSystem
import com.google.inject.ImplementedBy
import models.Image
import play.api.inject.ApplicationLifecycle
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@ImplementedBy(classOf[IntervalJobs])
trait Interval

@Singleton
class IntervalJobs @Inject() (
    system: ActorSystem,
    lifeCycle: ApplicationLifecycle) extends Interval {
  import scala.concurrent.duration._

  val hourJob = system.scheduler.schedule(1.hour, 1.hour)(Image.cleanTempImage())

  lifeCycle.addStopHook { () =>
    Future.successful(hourJob.cancel())
  }
}

更にこの起動時に実行するclassをmoduleというclassでまとめます。

package modules

import com.google.inject.AbstractModule
import com.google.inject.name.Names

class InitialModule extends AbstractModule {
  override def configure(): Unit = {
    bind(classOf[Interval])
      .annotatedWith(Names.named("interval")) // ここは名前をつけたくなければいらない。
      .to(classOf[IntervalJobs]).asEagerSingleton()
  }
}

そして、お決まりのapplication.confに以下を記載

play.modules.enabled += "modules.InitialModule"

Message系

Meesagesをつかっている場合、今まではimplicitをimportしておけば動いていましたが、2.4からはMessageApiを使わないとコンパイルエラーになります。
以下のようにMessagesApiをInjectし、I18nSupportをMixinしてあげれば、動きます。

import javax.inject.Inject
import scala.concurrent.ExecutionContext.Implicits.global
import consts.AppConfig
import play.api.i18n.{ I18nSupport, Messages, MessagesApi }
import play.api.mvc.{ Filter, RequestHeader, Result }
import play.api.mvc.Results._
import scala.concurrent.Future

class SystemAccessFilter @Inject() (val messagesApi: MessagesApi) extends Filter with I18nSupport {

  def apply(next: (RequestHeader) => Future[Result])(request: RequestHeader): Future[Result] = {
    if (!ipCheck(request)) {
      Future {
        NotFound(Messages("error.notFound"))
      }      
    } else {
      next(request)
    }
  }
}

まとめ

結構構造的に大幅に変わっていますが、Injectのイメージが付けば、理解は早いかなーと思います。
大体Playのドキュメントに沿った形で解説しました。
これを読んで本家のドキュメントの理解が進めば幸いです。