§相依性注入
相依性注入是一種廣泛使用的設計模式,有助於將元件的行為與相依性解析分開。Play 支援基於 JSR 330 的執行時期相依性注入(在此頁面中說明)以及 Scala 中的 編譯時期相依性注入。
執行時期相依性注入之所以如此稱呼,是因為相依性圖表會在執行時期建立、連接和驗證。如果找不到特定元件的相依性,您在執行應用程式之前不會收到錯誤訊息。
Play 支援 Guice,但也可以插入其他 JSR 330 實作。一般來說,Guice wiki 是進一步了解 Guice 功能和 DI 設計模式的絕佳資源。
預設情況下,Play 的 sbt 外掛程式不會提供任何特定的相依性注入架構。如果您想使用 Play 的 Guice 模組,請將其明確新增到您的函式庫相依性中,如下所示
libraryDependencies += guice
注意:Guice 是 Java 函式庫,本文件中的範例使用 Guice 內建的 Java API。如果您偏好 Scala DSL,您可能想使用 scala-guice 或 sse-guice 函式庫。
§動機
依賴注入實現了幾個目標
1. 它允許您輕鬆地為相同組件繫結不同的實作。這對於測試特別有用,在測試中,您可以使用模擬依賴手動實例化組件,或注入替代實作。
2. 它允許您避免全域靜態狀態。雖然靜態工廠可以達成第一個目標,但您必須小心確保您的狀態正確設定。特別是 Play 的(現已棄用)靜態 API 需要執行中的應用程式,這使得測試較不靈活。而且,一次有多個可用實例,使得可以並行執行測試。
Guice wiki 有些範例詳細說明這一點。
§它的運作方式
Play 提供許多內建組件,並在模組中宣告它們,例如其 BuiltinModule。這些繫結描述建立 Application
實例所需的一切,包括預設值,由路由編譯器產生的路由器,其中將控制器注入建構函式。然後,這些繫結可以轉換為在 Guice 和其他執行時期 DI 架構中運作。
Play 團隊維護 Guice 模組,它提供 GuiceApplicationLoader。這會對 Guice 執行繫結轉換,使用這些繫結建立 Guice 注入器,並從注入器要求 Application
實例。
也有第三方載入器為其他架構執行此操作,包括 Scaldi 和 Spring。
或者,Play 提供 BuiltInComponents 特質,允許您建立純粹的 Scala 實作,將您的應用程式 在編譯時期 串接在一起。
我們在下方更詳細地說明如何自訂預設繫結和應用程式載入器。
§宣告執行時期 DI 依賴
如果您有一個元件,例如控制器,而它需要其他元件作為相依性,那麼可以使用 @Inject 注解來宣告。@Inject
注解可以用在欄位或建構函式上。我們建議您將它用在建構函式上,例如
import javax.inject._
import play.api.libs.ws._
class MyComponent @Inject() (ws: WSClient) {
// ...
}
請注意,@Inject
注解必須在類別名稱之後,但建構函式參數之前,且必須加上括號。
此外,Guice 確實附帶了其他幾種 注入類型,但建構函式注入通常是 Scala 中最清楚、最簡潔、最可測試的,因此我們建議使用它。
Guice 能夠自動實例化建構函式上帶有 @Inject
的任何類別,而無需明確繫結它。此功能稱為 即時繫結,在 Guice 文件中更詳細地描述。如果您需要執行更複雜的操作,您可以宣告自訂繫結,如下所述。
§相依性注入控制器
Play 的路由編譯器會產生一個路由器類別,將您的控制器宣告為建構函式中的相依性。這讓您的控制器可以注入到路由器中。
在控制器名稱之前加上 @
符號具有特殊意義:控制器並非直接注入,而是會注入控制器的 Provider
。例如,這允許原型控制器,以及中斷循環相依性的選項。
§元件生命週期
依賴注入系統管理已注入元件的生命週期,依需要建立元件並將它們注入其他元件。以下是元件生命週期運作方式
- 每次需要元件時都會建立新執行個體。如果元件使用超過一次,則預設會建立多個元件執行個體。如果您只想要單一元件執行個體,則需要將其標記為 單例。
- 執行個體會在需要時延遲建立。如果元件從未被其他元件使用,則它根本不會被建立。這通常是您想要的。對於大多數元件,在需要之前建立它們沒有意義。然而,在某些情況下,您希望元件立即啟動,即使它們未被其他元件使用。例如,您可能希望在應用程式啟動時傳送訊息至遠端系統或預熱快取。您可以使用 急切繫結 強制立即建立元件。
- 執行個體不會自動清除,超出一般垃圾收集。元件會在不再被參照時進行垃圾收集,但架構不會執行任何特殊動作來關閉元件,例如呼叫
close
方法。然而,Play 提供一種特殊類型的元件,稱為ApplicationLifecycle
,它讓您可以註冊元件,以便在 應用程式停止時關閉。
§單例
有時您可能會有一個包含某些狀態的元件,例如快取、與外部資源的連線,或一個建立成本很高的元件。在這些情況下,重要的是只有一個該元件的執行個體。這可以使用 @Singleton 註解來達成
import javax.inject._
@Singleton
class CurrentSharePrice {
@volatile private var price = 0
def set(p: Int) = price = p
def get = price
}
§停止/清除
當 Play 關閉時,可能需要清除某些元件,例如,停止執行緒池。Play 提供一個 ApplicationLifecycle 元件,可以使用它來註冊掛勾,以便在 Play 關閉時停止您的元件
import javax.inject._
import scala.concurrent.Future
import play.api.inject.ApplicationLifecycle
@Singleton
class MessageQueueConnection @Inject() (lifecycle: ApplicationLifecycle) {
val connection = connectToMessageQueue()
lifecycle.addStopHook { () => Future.successful(connection.stop()) }
// ...
}
ApplicationLifecycle
會以與建立時相反的順序停止所有元件。這表示您依賴的任何元件仍可以在元件的停止掛勾中安全地使用。因為您依賴它們,所以它們必須在您的元件之前建立,因此它們不會在您的元件停止之前停止。
注意:確保所有註冊停止掛勾的元件都是單例非常重要。任何註冊停止掛勾的非單例元件都可能成為記憶體外洩的來源,因為每次建立元件時都會註冊新的停止掛勾。
您也可以使用 協調式關閉 來實作清理邏輯。Play 內部使用 Pekko 的協調式關閉,但使用者程式碼也可以使用。ApplicationLifecycle#stop
實作為協調式關閉任務。主要的差異是 ApplicationLifecycle#stop
會依序以可預測的順序執行所有停止掛勾,而協調式關閉會並行執行同一個階段中的所有任務,這可能會比較快,但不可預測。
§提供自訂繫結
為元件定義特質並讓其他類別依賴於該特質,而非元件的實作,這被視為良好的做法。這樣一來,您可以注入不同的實作,例如在測試應用程式時注入模擬實作。
在這種情況下,DI 系統需要知道哪個實作應該繫結到該特質。我們建議您宣告此內容的方式取決於您是作為 Play 的最終使用者撰寫 Play 應用程式,還是撰寫其他 Play 應用程式會使用的函式庫。
§Play 應用程式
我們建議 Play 應用程式使用應用程式所使用的 DI 架構所提供的任何機制。儘管 Play 提供繫結 API,但此 API 有點受限,而且無法讓您充分利用所使用架構的效能。
由於 Play 提供開箱即用的 Guice 支援,因此以下範例顯示如何提供 Guice 的繫結。
§繫結註解
將實作繫結到介面的最簡單方法是使用 Guice @ImplementedBy 註解。例如
import com.google.inject.ImplementedBy
@ImplementedBy(classOf[EnglishHello])
trait Hello {
def sayHello(name: String): String
}
class EnglishHello extends Hello {
def sayHello(name: String) = "Hello " + name
}
§程式化繫結
在一些更複雜的情況下,您可能想要提供更複雜的繫結,例如當您有多個特質實作時,這些實作是由 @Named 註解限定的。在這些情況下,您可以實作自訂 Guice 模組
import com.google.inject.name.Names
import com.google.inject.AbstractModule
class Module extends AbstractModule {
override def configure() = {
bind(classOf[Hello])
.annotatedWith(Names.named("en"))
.to(classOf[EnglishHello])
bind(classOf[Hello])
.annotatedWith(Names.named("de"))
.to(classOf[GermanHello])
}
}
如果您呼叫此模組為 Module
並將其放在根目錄中,它將自動向 Play 註冊。或者,如果您想要給它不同的名稱或將它放在不同的套件中,您可以透過將其完全限定類別名稱附加到 application.conf
中的 play.modules.enabled
清單來向 Play 註冊它
play.modules.enabled += "modules.HelloModule"
您也可以透過將其新增至已停用的模組來停用根目錄中名為 Module
的模組的自動註冊
play.modules.disabled += "Module"
§可設定繫結
有時您可能想要在設定 Guice 繫結時讀取 Play Configuration
或使用 ClassLoader
。您可以透過將它們新增至模組的建構函式來存取這些物件。
在以下範例中,每個語言的 Hello
繫結會從設定檔讀取。這允許透過在 application.conf
檔中新增新的設定來新增新的 Hello
繫結。
import com.google.inject.name.Names
import com.google.inject.AbstractModule
import play.api.Configuration
import play.api.Environment
class Module(environment: Environment, configuration: Configuration) extends AbstractModule {
override def configure() = {
// Expect configuration like:
// hello.en = "myapp.EnglishHello"
// hello.de = "myapp.GermanHello"
val helloConfiguration: Configuration =
configuration.getOptional[Configuration]("hello").getOrElse(Configuration.empty)
val languages: Set[String] = helloConfiguration.subKeys
// Iterate through all the languages and bind the
// class associated with that language. Use Play's
// ClassLoader to load the classes.
for (l <- languages) {
val bindingClassName: String = helloConfiguration.get[String](l)
val bindingClass: Class[_ <: Hello] =
environment.classLoader
.loadClass(bindingClassName)
.asSubclass(classOf[Hello])
bind(classOf[Hello])
.annotatedWith(Names.named(l))
.to(bindingClass)
}
}
}
注意:在多數情況下,如果您需要在建立元件時存取
Configuration
,您應該將Configuration
物件注入元件本身或元件的Provider
。然後您可以在建立元件時讀取Configuration
。您通常不需要在建立元件的繫結時讀取Configuration
。
§急切繫結
在以上的程式碼中,每次使用 EnglishHello
和 GermanHello
物件時,都會建立新的物件。如果您只想要建立這些物件一次,可能是因為建立它們的成本很高,那麼您應該使用 @Singleton
註解。如果您想要建立它們一次,而且在應用程式啟動時也急切地建立它們,而不是在需要時才建立,那麼您可以使用 Guice 的急切單例繫結。
import com.google.inject.name.Names
import com.google.inject.AbstractModule
// A Module is needed to register bindings
class Module extends AbstractModule {
override def configure() = {
// Bind the `Hello` interface to the `EnglishHello` implementation as eager singleton.
bind(classOf[Hello])
.annotatedWith(Names.named("en"))
.to(classOf[EnglishHello])
.asEagerSingleton()
bind(classOf[Hello])
.annotatedWith(Names.named("de"))
.to(classOf[GermanHello])
.asEagerSingleton()
}
}
當應用程式啟動時,可使用熱切單例來啟動服務。它們通常與關閉掛鉤結合使用,以便在應用程式停止時,服務可以清除其資源。
import javax.inject._
import scala.concurrent.Future
import play.api.inject.ApplicationLifecycle
// This creates an `ApplicationStart` object once at start-up and registers hook for shut-down.
@Singleton
class ApplicationStart @Inject() (lifecycle: ApplicationLifecycle) {
// Shut-down hook
lifecycle.addStopHook { () => Future.successful(()) }
// ...
}
import com.google.inject.AbstractModule
class StartModule extends AbstractModule {
override def configure() = {
bind(classOf[ApplicationStart]).asEagerSingleton()
}
}
§Play 函式庫
如果您正在為 Play 實作函式庫,則您可能希望它與 DI 架構無關,以便您的函式庫可以在應用程式中使用的任何 DI 架構中開箱即用。因此,Play 提供一個輕量級繫結 API,以 DI 架構無關的方式提供繫結。
若要提供繫結,請實作Module以傳回您要提供的繫結順序。Module
特質也提供了一個 DSL,用於建立繫結
import play.api.inject._
import play.api.Configuration
import play.api.Environment
class HelloModule extends Module {
def bindings(environment: Environment, configuration: Configuration): Seq[play.api.inject.Binding[_]] = Seq(
bind[Hello].qualifiedWith("en").to[EnglishHello],
bind[Hello].qualifiedWith("de").to[GermanHello]
)
}
此模組可以透過將其附加到reference.conf
中的play.modules.enabled
清單中,自動向 Play 註冊
play.modules.enabled += "com.example.HelloModule"
Module
bindings
方法採用 PlayEnvironment
和Configuration
。如果您想要動態配置繫結,則可以存取這些內容。- 模組繫結支援熱切繫結。若要宣告熱切繫結,請在
Binding
的結尾新增.eagerly
。
為了最大化跨架構相容性,請記住下列事項
- 並非所有 DI 架構都支援即時繫結。請確定您的函式庫提供的元件都已明確繫結。
- 請盡量簡化繫結金鑰 - 不同的執行時期 DI 架構對於金鑰是什麼以及它是否應該唯一,有非常不同的觀點。
§排除模組
如果您不希望載入某個模組,則可以透過將其附加到application.conf
中的play.modules.disabled
屬性來排除它
play.modules.disabled += "play.api.db.evolutions.EvolutionsModule"
§管理循環相依性
循環依賴會在其中一個元件依賴另一個依賴原始元件(直接或間接)的元件時發生。例如
import javax.inject.Inject
class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Foo)
在此情況下,Foo
依賴 Bar
,而 Bar
依賴 Baz
,而 Baz
依賴 Foo
。因此,您將無法實體化任何這些類別。您可以使用 Provider
解決這個問題
import javax.inject.Inject
import javax.inject.Provider
class Foo @Inject() (bar: Bar)
class Bar @Inject() (baz: Baz)
class Baz @Inject() (foo: Provider[Foo])
一般來說,可以透過以更原子的方式分解元件,或尋找更具體的元件來依賴,來解決循環依賴。常見的問題是依賴 Application
。當您的元件依賴 Application
時,表示它需要一個完整的應用程式才能執行其工作;通常情況並非如此。您的依賴關係應建立在具有您所需特定功能的更具體元件(例如 Environment
)上。最後,您可以透過注入 Provider[Application]
來解決問題。
§進階:擴充 GuiceApplicationLoader
Play 的執行時期依賴注入是由 GuiceApplicationLoader
類別引導的。此類別載入所有模組,將模組傳送至 Guice,然後使用 Guice 建立應用程式。如果您想要控制 Guice 如何初始化應用程式,則可以擴充 GuiceApplicationLoader
類別。
您可以覆寫多種方法,但通常會想要覆寫 builder
方法。此方法會讀取 ApplicationLoader.Context
並建立 GuiceApplicationBuilder
。您可以在下方看到 builder
的標準實作,您可以隨意變更。您可以在關於 使用 Guice 進行測試 的區段中找出如何使用 GuiceApplicationBuilder
。
import play.api.inject._
import play.api.inject.guice._
import play.api.ApplicationLoader
import play.api.Configuration
class CustomApplicationLoader extends GuiceApplicationLoader() {
override def builder(context: ApplicationLoader.Context): GuiceApplicationBuilder = {
val extra = Configuration("a" -> 1)
initialBuilder
.in(context.environment)
.loadConfig(context.initialConfiguration.withFallback(extra))
.overrides(overrides(context): _*)
}
}
當您覆寫 ApplicationLoader
時,您需要告知 Play。將下列設定新增至您的 application.conf
play.application.loader = "modules.CustomApplicationLoader"
您不限於使用 Guice 進行依賴注入。透過覆寫 ApplicationLoader
,您可以控制應用程式的初始化方式。在 下一節 中深入了解。
§新增依賴項至類別,而不觸及子類別
有時您可能想要新增新的依賴項至某些基礎類別,而該類別可能有很多子類別。
為避免直接提供依賴項給每個子類別,您可以將其新增為可注入欄位。
此方法可能會降低類別的可測試性,因此請小心使用。
import com.google.inject.ImplementedBy
import com.google.inject.Inject
import com.google.inject.Singleton
import play.api.mvc._
@ImplementedBy(classOf[LiveCounter])
trait Counter {
def inc(label: String): Unit
}
object NoopCounter extends Counter {
override def inc(label: String): Unit = ()
}
@Singleton
class LiveCounter extends Counter {
override def inc(label: String): Unit = println(s"inc $label")
}
class BaseController extends ControllerHelpers {
// LiveCounter will be injected
@Inject
@volatile protected var counter: Counter = NoopCounter
def someBaseAction(source: String): Result = {
counter.inc(source)
Ok(source)
}
}
@Singleton
class SubclassController @Inject() (action: DefaultActionBuilder) extends BaseController {
def index = action {
someBaseAction("index")
}
}
下一頁:編譯時間依賴注入
在此文件檔中發現錯誤?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件檔指南 後,請隨時提交拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇,與社群展開對話。