2017年9月18日

Guice

Guice 是 google 推出的一個輕量級依賴注入框架,解決Java中的 Dependency Injection 依賴注入問題,這個功能就像是 Spring 的 DI IoC。但因為 Spring 的框架 scope 龐大,如果只是想要一個單純的 DI library,那麼 Guice 是一個很好的選擇。

JSR 330 DI

在 Spring 誕生後,Google 也提供了另一個 DI 的實作 Guice,後來在 2009 年,定義了 JSR 330 DI 的規範,隨後 Spring 與 Guice 也都支援了 JSR 330。

javax.inject.* 提供了依賴注入的定義類別,但沒有限制依賴配置方式,
依賴配置方式取決於注入器的實作,injector 可以有多種配置設定的方式,可以基於XML、annotation、DSL(Domain-specific language),甚至是Java代碼,在 injector 實作的部分,可以採用反射、代碼生成技術等等,不受限制。


@Inject

可在constructor、field、method上使用,也可以在static 的非 final 的field、method上使用。使用該註解標註的constructor、field、method訪問修飾符 (private、package- private、protected、public 中任意一種) 不受限制。Injector在進行注入時,要按照constructors、fiedls、methods的順序進行。

對被標註 @Inject 的constructor的要求:

  • 在滿足上述說明的情況下,可以有其他的依賴作為方法的參數,別的要求倒沒有什麼。

對被標註 @Inject 的field的要求:

  • 不能是final

對被標註 @Inject 的method的要求:

  • 方法不能是abstract
  • 可以有其他的依賴作為該方法的參數

  • 當一個方法標註了 @Inject 並覆寫了其他標註了 @Inject 的方法時,對於每一個實例的每一次注入請求,該方法只會被注入一次。

  • 當一個方法沒有標註 @Inject 並覆寫了其他標註了 @Inject 的方法時,該方法不會被注入。


@Qualifier

用於標記限定器 annotation,用來指定採用哪個 class

假設 class A 有兩個 subclass A1,A2。B 依賴了A,那麼DI容器在為 B 的實例注入 A 時到底該注入 A1 或 A2 呢?

class B {
    @Inject
    A a;
}

解決方式是在 A1 及 A2 分別寫上 Qualifier 標記

@A_1
public class A1{
}

@A_2
public class A2{
}

在 class B 中指定 A1 或是 A2

class B{
    @Inject
    @A_1
     A a;
}

也可以用 @Named 進行標記

@Named("A1")
Public class A1{
}

@Named("A2")
Public class A2{
}

class B{
    @Inject
    @Named("A1")
    A a;
}

@Scope @Singleton

@Scope 用在 class 上,用來告訴 injector,為該 class 建立多少個 instances。

@Singleton 就是指產生一個 instance。

Guice example in scala sbt project

在 build.sbt 中加上 guice library

libraryDependencies += "com.google.inject" % "guice" % "4.1.0"

定義兩個 Service 介面,分別有 UserServiceImpl 及 LogServiceImpl 實作。

trait UserService {
  def process(): Unit
}

class UserServiceImpl extends UserService {
  override def process(): Unit = {
    System.out.println("UserServiceImpl in process")
  }
}

trait LogService {
  def log(msg: String): Unit
}

class LogServiceImpl extends LogService {

  override def log(msg: String): Unit = {
    System.out.println("log message:" + msg)
  }
}

定義 Application 介面,在 MyApp 實作的 constructor 中,引用了 UserService 及 LogService,將來由 guice 動態指定 UserServiceImpl 及 LogServiceImpl 實作。

import javax.inject.Inject

trait Application {
  def work(): Unit
}

class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

Guice 的 Module 定義,必須要 extends AbstractModule,在 configure 中,設定 class 實作的 Denpendency 關係。

import com.google.inject.AbstractModule

class AppModule extends AbstractModule {
  override protected def configure(): Unit = {
    bind(classOf[UserService]).to(classOf[UserServiceImpl])
    bind(classOf[LogService]).to(classOf[LogServiceImpl])

    bind(classOf[Application]).to(classOf[MyApp])
  }
}

scala 主程式,以 Guice.createInjector(new AppModule) 產生 injector,藉由 inject 取得 Application 的 instance,然後就能呼叫 work。

import com.google.inject.Guice

object Main {
  def main(args: Array[String]) {
    println("Main")

    val injector = Guice.createInjector(new AppModule)

    val myApp = injector.getInstance(classOf[Application])

    myApp.work()
  }
}

執行結果

Main
UserServiceImpl in process
log message:MyApp is working

如果要限制 MyApp 為 Singleton,可以在 MyApp 上加上 @Singleton

import javax.inject.{Inject, Singleton}

trait Application {
  def work(): Unit
}

@Singleton
class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

也可在 Module 中設定 class dependency 的地方,加上 .in(classOf[Singleton]) 的限制

bind(classOf[Application]).to(classOf[MyApp]).in(classOf[Singleton])

或是寫成 asEagerSingleton,在程式啟動時,就馬上產生 MyApp

bind(classOf[Application]).to(classOf[MyApp]).asEagerSingleton

Spring vs Guice

關於選擇Spring還是Google-Guice的一些想法

SpringComparison

以往的 Spring 是使用 xml 的方式定義 java bean,一般會認為 Guice 處理速度比 Spring 快,但可能只在啟動的時候有差異,因為 spring 需要讀取 xml 設定檔,而 guice 完全都是用程式碼處理的。

Guice 是由 Google 的 AdWords 專案誕生的,他不像是 Spring 整合了許多不同的 Java EE Framework,只是單純且專注在處理 Dependency Injection 的問題。在官方 Spring Comparison 文件中提到一個例子,他是由 Spring 轉換到 Guice,發現大約有 3/4 的程式碼是不需要的,用 Guice 寫的 module 程式碼短,且容易閱讀。

Guice 不支援以設定檔的方式設定 DI,完全是以 annotations 及 generics 程式碼的方式處理,因此可以達成動態 DI 的功能。

References

Guice簡明教程

Guice 快速入門

Guice Getting Started

Google Guice的動機

Java 依賴注入標準(JSR-330)簡介

JSR330 DI

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

2017年9月4日

monorepos

Monorepo 是一種管理企業代碼的方式,在這種方式下會摒棄原先一個 module 一個 git repo 的方式,而是把所有的 modules 都放在一個 repo 內來管理。單體倉庫 monorepos 是一個包含了多個獨立 project 的代碼倉庫,一個代碼 repository 包含一個單體倉庫。

目前有 Babel, React, Angular, Ember, Meteor 等等專案都使用了這個專案管理方式。

multirepos

當一個軟體專案隨著功能跟開發人員的擴增,漸漸地會發生共用程式碼的問題,一個大型的專案會拆分出多個小型 repos,每個 repo 代表了一個單獨的離散想法。

但也因為多個平行專案的發展,會發生一些問題:

  1. 架構孤島: 因為在一個整合專案中,拆分了功能並分配給不同的開發團隊處理及發展,每個團隊在不同的精進道路上,使用了不同的 library,好處是工程師可以根據自由選擇適當的 library,缺點是越來越多的 library,表示開發人員必須花更多時間學習不同的架構。

  2. 依賴地獄(Dependency Hell): 某個程式的修正,會影響到多少專案,在專案整合建構時,會需要很多時間找出整合的問題並提出修正。

  3. 建構耗時: 傳統的循序建構方式,會需要數十分鐘的時間,因應 monorepo 提供的新 build tool,可以平行建構加速建構的過程,也有更快的 incremental build 模式。

build tool

在 monorepo 中會搭配使用一個適當的 build tool,原因當然是為了在這樣的環境下,縮短建構的時間。

沒有萬靈丹

在一個專案建置初期,會因為開發維護的工程師人數不多,傾向於建置一個單一的專案進行開發,隨著應用本身的演進,公司的業務成長,會慢慢增加這個專案開發的參與人員數量,甚至會成長到有一個或多個開發 team。

在多人分工開發的環境下,就會面臨切割專案,抽取共用的函式庫的過程,一般的直覺,就是讓不同的專案模組有各自獨立負責的人員/團隊,也就是有各自獨立的版本演進,最後再整合在一起。

但如果分工是用垂直方式分工,以功能的方式分工,就有可能會造成程式碼的衝突,或是撰寫出來的程式運作的邏輯不同,或是使用了不同的輔助函式庫的問題。

這情況又有 monorepos 這樣的解決方案,想要解決這種問題。

應該說不管什麼樣的方案,都會有相對應的優點及缺點,那就要看每一個使用的情境來決定,該用什麼方式處理,讓優點多一點,缺點少一點,也沒有必要換來換去,畢竟轉換作業方式,加上適應的過程,也需要花上不少時間。

《三國演義》的第一句話就說:「話說天下大勢,分久必合,合久必分。」整體的局勢就是在分分合合中,不斷地來來往往反覆進行。

References

單體代碼倉庫:Uber的Android代碼倉庫演化史

Monorepos in Git

monorepo 新浪潮 | introduce lerna

多包存儲庫管理工具 Lerna

語意化版本 2.0.0