2017年9月11日

scala play from 2.5 to 2.6

scala play framework 專案如果要由 2.5 升級到 2.6,必須調整一些設定項目,另外 action composition 部分的程式也有更新的寫法,Lightbend activator 在 2017/5/24 已經退役,現在都要直接使用 sbt 編譯及封裝專案。

build.sbt

scala play 2.6 相關的 library 版本都要更新,scala 也要由 2.11 版改為 2.12,以下是 build.sbt

name := """project"""
organization := "tw.com.maxkit"

version := "0.1.0"

lazy val root = (project in file(".")).enablePlugins(PlayScala, JavaServerAppPackaging)

scalaVersion := "2.12.2"

scalacOptions ++= Seq("-encoding", "UTF-8")

libraryDependencies += guice

// Adds additional packages into Twirl
//TwirlKeys.templateImports += "tw.com.maxkit.controllers._"

// Adds additional packages into conf/routes
// play.sbt.routes.RoutesKeys.routesImport += "tw.com.maxkit.binders._"

libraryDependencies ++= Seq(
  ws,
  filters,
  "com.typesafe.play" %% "play-slick" % "3.0.0",
  "com.typesafe.play" %% "play-slick-evolutions" % "3.0.0",

  // slick
  "com.typesafe.slick" %% "slick" % "3.2.1",
  "org.slf4j" % "slf4j-nop" % "1.6.4",
  "com.typesafe.slick" %% "slick-hikaricp" % "3.2.1",

  // 讓 slick 支援 Timestamp 轉換 slick-joda-mapper https://github.com/tototoshi/slick-joda-mapper
  "com.github.tototoshi" %% "slick-joda-mapper" % "2.3.0",
  "joda-time" % "joda-time" % "2.7",
  "org.joda" % "joda-convert" % "1.7",

  // akka remoteing
  "com.typesafe.akka" % "akka-remote_2.12" % "2.5.3",

  // smtp email plugin
  // https://github.com/playframework/play-mailer
  "com.typesafe.play" %% "play-mailer" % "6.0.0",

  // mariadb java client
  //"mysql" % "mysql-connector-java" % "5.1.36",
  "org.mariadb.jdbc" % "mariadb-java-client" % "2.0.3",

  // 使用 FileUtils
  "commons-io" % "commons-io" % "2.5",

  // 使用 Base64
  "commons-codec" % "commons-codec" % "1.10",

  // object pool
  "commons-pool" % "commons-pool" % "1.6",

  // CLI parser library scopt https://github.com/scopt/scopt
  "com.github.scopt" % "scopt_2.12" % "3.6.0",

  // redis for Play https://github.com/KarelCemus/play-redis
  play.sbt.PlayImport.cacheApi,
  // include play-redis library
  "com.github.karelcemus" %% "play-redis" % "1.5.1",

  // Test Framework
  "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.0" % Test,
  specs2 % Test
)

resolvers ++= Seq(
  "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/",
  "Typesafe Maven Repository" at "http://repo.typesafe.com/typesafe/maven-releases/",
  "Typesafe ivy" at "http://dl.bintray.com/typesafe/ivy-releases",
  "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
)

fork in run := true

// production settings
maintainer := "service <service@maxkit.com.tw>"
packageSummary := "project"
packageDescription := """"""

project/plugins.sbt

resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.2")

project/build.properties

sbt.version=0.13.15

Action Composition

Actions Composition 2.6 官方文件

如果要在 controller 裡面每一個 method 都進行的登入的驗證,可以用 ActionComposition 的方式實作,但 2.6 版的 ActionComposition 已經調整為以下的做法。

首先獨立實作一個 AuthAction.scala

package controllers.admin

import java.sql.Timestamp
import java.util.Date
import javax.inject.Inject

import model.Usr
import play.api.{Environment, Logger, Mode}
import play.api.cache.redis.CacheApi
import play.api.mvc._
import utils.ServerConstants

import scala.concurrent.{ExecutionContext, Future}

class AuthRequest[A](val usr: Option[Usr], request: Request[A]) extends WrappedRequest[A](request)

class AuthAction @Inject()(cache: CacheApi,
                           env: Environment,
                           val parser: BodyParsers.Default)
                          (implicit val executionContext: ExecutionContext)
  extends ActionBuilder[AuthRequest, AnyContent] with ActionTransformer[Request, AuthRequest] {
  val cacheTimeout = ServerConstants.cacheTimeout

  def transform[A](request: Request[A]) = Future.successful {
    //    val now: Timestamp = new Timestamp(new Date().getTime)
    //    val usr: Usr = new Usr(0, "", "", "", now, "", now, "")
    //
    //    new AuthRequest(Some(usr), request)

    (request.session.get("key").flatMap { key =>
      cache.get[Usr](key)
    } map { usr =>

      // 需要再設定一次 cache,否則會發生 cache timeout
      cache.set(request.session.get("key").get, usr, cacheTimeout)
      new AuthRequest(Some(usr), request)

    }).orElse {

      env.mode match {
        case Mode.Dev | Mode.Test => {
          Logger.info("Mode.Dev don't check admin login status")

          val now: Timestamp = new Timestamp(new Date().getTime)
          val usr: Usr = new Usr(0, "", "devusr", "", now, "", now, "")
          Some( new AuthRequest(Some(usr), request) )
        }
        case Mode.Prod => {
          Some( new AuthRequest(None, request) )
        }
      }

    }.get
  }
}

在 controller 中,要用 injection 的方式將 AuthAction 引用進來。

@Singleton
class MyController @Inject()(
                               authAction: AuthAction,
                               actorSystem: ActorSystem, env: Environment,
                               implicit val executionContext: ExecutionContext,
                               cc: ControllerComponents) extends AbstractController(cc) {
    def listCdrs = authAction.async { request: Request[AnyContent] =>

        val body: AnyContent = request.body
        val formdata: Option[Map[String, Seq[String]]] = body.asFormUrlEncoded
        ........
    }
}

移除 Play.current

在 scala play 2.5 就已經不能用 Play.current,這裡記錄怎麼利用 injector 直接產生 instance。

在一般的 scala class 直接使用 database 的 model,已經不能用以下這種寫法,Play.current、DatabaseConfigProvider.get 都已經是 deprecated method。

val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current)

首先建立一個新的 GlobalContext

package modules

import play.api.inject.Injector
import javax.inject.{Inject,Singleton}

@Singleton
class GlobalContext @Inject()(playInjector: Injector) {
  GlobalContext.injectorRef = playInjector
}

object GlobalContext {
  private var injectorRef: Injector = _

  def injector: Injector = injectorRef
}

在自訂的 Guice Module 中,產生 GlobalContext

bind(classOf[GlobalContext]).asEagerSingleton()

然後就能直接使用 injector 產生 database Model

 val npRepo = GlobalContext.injector.instanceOf[NpRepo]

ref: How to access Play Framework 2.4 guice Injector in application?

Lightbend activator 在 2017/5/24 終止

因為 LIGHTBEND ACTIVATOR TEMPLATES 發布,未來已經不會再用 activator 進行 project template 的管理,要求大家改用 giter8 templates

以往用 activator 產生新的 project 的指令,都要用 sbt 取代。

根據 giter8 template 產生新的 project

sbt new playframework/play-scala-seed.g8

在過程中,要填寫 project name, organization 等資料

This template generates a Play Scala project

name [play-scala-seed]: projectname
organization [com.example]: tw.com.maxkit
scalatestplusplay_version [3.1.1]:
play_version [2.6.2]:

在 poject 中,以往使用 activator 的指令,都要改成 sbt

編譯

sbt compile

啟動 server

sbt run

封裝整個 project

sbt clean update compile stage dist

giter8

Giter8 是一個基於發佈在 Github 或任何 git 上的template來生成文件或目錄的命令行工具,它是以 Scala 實作並由 sbt launcher 運行。

除了一個官方的 giter8 project templates 集散地 之外,我們可以自己建立自己的 project template 並以 git 形式存放及分享在 git server 中。

References

Giter8 gitbook

Giter8

使用Scalatra創建Scala WEB工程

action-composition 2.5