Play! Framework 系列(三):依賴注入

ScalaCool發表於2017-11-15

本文由 Shaw 發表在 ScalaCool 團隊部落格。

Play! Framework 系列(二)中我們介紹了 Play 的專案結構。在日常處理業務邏輯的時候,我們都會用到依賴注入,本文將介紹一下 Play! 中的依賴注入以及如何合理地去使用她。

為什麼要使用「依賴注入」

在許多 Java 框架中,「依賴注入」早已不是一個陌生的技術,Play 框架從 2.4 開始推薦使用 Guice 來作為依賴注入。

採用依賴注入最大的好處就是為了「解耦」,舉個栗子:

上一篇文章的例子中,我們實現了一個 EmployeeService 用來對公司的員工進行操作:

package services

import models._

class EmployeeSerivce{
  ...
}
複製程式碼

在之前的實現中,我們沒有加入資料庫的操作,那麼現在我們想要引入一個資料庫連線的類:DatabaseAccessService 來對資料庫進行連線以便我們對相應的資料庫表進行操作,則:

新建一個資料庫連線操作的 Service:

package services

class DatabaseAccessService{}
複製程式碼

EmployeeSerivce 需要依賴 DatabaseAccessService:

package services

import models._

class EmployeeSerivce(db: DBService){
  ...
}
複製程式碼

好了,現在我們需要在 EmployeeController 中使用 EmployeeSerivce,如果不採用依賴注入,則:

class EmployeeController @Inject() (
  cc: ControllerComponents
) extends AbstractController(cc) {
  val db = new DatabaseAccessService()
  val employeeSerivce = new EmployeeSerivce(db)

  def employeeList = Action { implicit request: Request[AnyContent] =>
    val employees = employeeSerivce.getEmployees()
    Ok(views.html.employeeList(employees))
  }
}
複製程式碼

可以看到,為了例項化 EmployeeSerivce,DatabaseAccessService 也需要例項化,如果隨著需求的增加,EmployeeSerivce 所需要依賴的東西增加,那麼我們每次例項化 EmployeeSerivce 的時候都需要將她的依賴也例項化一遍,而且她的依賴也有可能會依賴其他東西,這樣就使得我們的程式碼變得非常冗餘,也極難維護。

為了解決這一問題,我們引入了依賴注入,Play支援兩種方式的依賴注入,分別是:「執行時依賴注入」以及「編譯時依賴注入」,接下來我們就通過這兩種依賴注入來解決我們上面提出的問題。

執行時依賴注入(runtime dependency)

Play 的執行時依賴注入預設採用 Guice,關於 Guice,我們後面的文章當中會介紹,這裡只需要知道她。為了支援 Guice 以及其他的執行時依賴注入框架,Play 提供了大量的內建元件。詳見 play.api.inject

那麼在 Play 中我們將如何使用這種依賴注入呢?回到我們文章剛開始講的那個栗子中,現在我們通過依賴注入的方式來重新組織我們的程式碼:

首先 EmployeeSerivce 需要依賴 DatabaseAccessService,這裡其實就存在一個「依賴注入」,那我們這樣去實現:

package services

import models._
import javax.inject._

class EmployeeSerivce @Inject() (db: DBService){
  ...
}
複製程式碼

在上面的程式碼中,我們引入了 import javax.inject._,並且可以看到多了一個 @Inject() 註解,我們實現執行時依賴注入就採用該註解。

那麼在 EmployeeController 中,我們的程式碼就變成了:

class EmployeeController @Inject() (
  employeeSerivce: EmployeeSerivce,
  cc: ControllerComponents
) extends AbstractController(cc) {
  def employeeList = Action { implicit request: Request[AnyContent] =>
    val employees = employeeSerivce.getEmployees()
    Ok(views.html.employeeList(employees))
  }
}
複製程式碼

可以看到我們不需要再去寫那麼多的例項了,我們只要在需要某種依賴的地方宣告一下我們需要什麼樣的依賴, Play 在執行時就會將我們需要的依賴注入到相應的元件中去。

tip:@Inject 必須放在類名的後面,構造引數的前面。

「執行時依賴注入」,顧名思義就是在程式執行的時候進行依賴注入,但是她不能在編譯時進行校驗,為了能讓程式在編譯時就能實現對依賴注入的校驗, Play支援了「編譯時依賴注入」。

編譯時依賴注入(compile time dependency injection)

為了實現編譯時依賴注入,我們需要知道 Play 提供的一個特質:ApplicationLoader,該特質中的 load 方法將會在程式啟動的時候載入我們的應用程式,在這個過程中,Play 框架本身以及我們自己的程式程式碼所依賴的東西都會被例項化。

預設情況下,Play 提供了一個 Guice 模組,該模組下的 GuiceApplicationBuilder 會根據 Play 框架給定的 context 去將該程式所依賴的所有元件聯絡在一起。

如果我們要自定義 ApplicationLoader,我們也需要一個像 GuiceApplicationBuilder 的東西,好在 Play 提供了這麼一個東西,那就是:BuiltInComponentsFromContext,我們可以通過繼承這個類來實現我們自己的 ApplicationLoader。

接下來我們通過相應的程式碼來作進一步的解釋:

import controllers._
import play.api._
import play.api.routing.Router
import services._
import router.Routes


//自定義 ApplicationLoader
class MyApplicationLoader extends ApplicationLoader {
  def load(context: Context): Application = {
    new MyComponents(context).application
  }
}

class MyComponents(context: Context)
    extends BuiltInComponentsFromContext(context)
    with play.filters.HttpFiltersComponents {

  lazy val databaseAccessService = new DatabaseAccessService

  lazy val employeeSerivce = new EmployeeSerivce(databaseAccessService)

  lazy val employeeController = new EmployeeController(employeeSerivce, controllerComponents)

  lazy val router: Router = new Routes(httpErrorHandler, employeeController)
}
複製程式碼

我們通過繼承 BuiltInComponentsFromContext 使得程式能夠根據 Play 所提供的 context 來載入 Play 框架本身所需要的一些元件。

那麼回到我們的「編譯時的依賴注入」中來,可以看到在 class MyComponents 中,我們將所有的 service 都例項化了,並且將這些例項注入到相應的依賴她們的模組中:

//將兩個 service 例項化
lazy val databaseAccessService = new DatabaseAccessService

//EmployeeSerivce 依賴 DatabaseAccessService,將例項 databaseAccessService 注入其中
lazy val employeeSerivce = new EmployeeSerivce(databaseAccessService)

//將 employeeSerivce 注入到 employeeController 中
lazy val employeeController = new EmployeeController(employeeSerivce, controllerComponents)
複製程式碼

使用 BuiltInComponentsFromContext 時,我們需要自己實現一下 router:

lazy val router: Router = new Routes(httpErrorHandler, employeeController)
複製程式碼

tip:需要注意的是,如果我們實現了自己的 ApplicationLoader,我們需要在 application.conf 檔案中宣告一下:

play.application.loader = MyApplicationLoader
複製程式碼

通過自定義 ApplicationLoader 我們就實現了編譯時期的依賴注入,那麼 EmployeeSerivce 就變成了:

package services

import models._

class EmployeeSerivce (db: DBService){
  ...
}
複製程式碼

可以看到, 這裡就省去了 @Inject() 註解。

同樣的,對於 EmployeeController:

package controllers

import play.api._
import play.api.mvc._
import models._
import services._

// 沒有了 @Inject() 註解
class EmployeeController (
  employeeSerivce: EmployeeSerivce,
  cc: ControllerComponents
) extends AbstractController(cc) {
  ...
}
複製程式碼

通過使用編譯時期的依賴注入,我們只需要在將所有的依賴例項化一次就夠了,並且使用這種方式,我們能夠在編譯時期就能發現程式的一些異常。同樣的,使用該方法也會有一些問題,就是我們需要寫許多樣板程式碼。另外本文的編譯時期的依賴注入完全是自己手動注入的,看上去也比較繁瑣,不是那麼直觀,如果要使用更優雅的方式,我們可以使用 macwire,這個我們在後面的文章中會詳細講解。

結語

本文簡單介紹了一下 Play 支援的兩種依賴注入的模式,文中提到的一些第三方依賴注入的框架我們會在後面的文章中詳細介紹。本文的例子請戳原始碼連結

Play! Framework 系列(三):依賴注入

相關文章