§相依性注入
相依性注入是一種廣泛使用的設計模式,有助於將元件的行為與相依性解析分開。元件會宣告其相依性,通常是建構函數參數,而相依性注入架構可協助您將這些元件連接在一起,讓您不必手動執行此操作。
Play 提供開箱即用的相依性注入支援,基於 JSR 330。Play 附帶的預設 JSR 330 實作是 Guice,但也可以插入其他 JSR 330 實作。若要啟用 Play 提供的 Guice 模組,請確定您在 build.sbt 中的函式庫相依性中包含 guice
,例如:
libraryDependencies += guice
Guice Wiki 是進一步了解 Guice 功能和一般 DI 設計模式的絕佳資源。
§動機
相依性注入可達成多項目標
1. 它讓您可以輕鬆地為同一元件繫結不同的實作。這對於測試特別有用,您可以在其中使用模擬相依性手動建立元件實例,或注入替代實作。
2. 它允許您避免全域靜態狀態。雖然靜態工廠可以達成第一個目標,您必須小心確保您的狀態正確設定。特別是 Play 的(現已棄用的)靜態 API 需要一個正在執行的應用程式,這使得測試較不靈活。而且一次有多個可用實例,使得可以並行執行測試。
Guice wiki 有些範例說明這一點的更多詳細資料。
§它的運作方式
Play 提供許多內建元件,並在模組中宣告它們,例如其 BuiltinModule。這些繫結描述建立 Application
實例所需的一切,包括預設值,由路由編譯器產生的路由器,其中控制器已注入到建構函式中。這些繫結接著可以轉換為在 Guice 和其他執行時期 DI 架構中運作。
Play 團隊維護 Guice 模組,它提供一個 GuiceApplicationLoader。它為 Guice 執行繫結轉換,使用這些繫結建立 Guice 注入器,並從注入器要求一個 Application
實例。
也有第三方載入器為其他架構執行這項工作,包括 Spring。
我們在下方更詳細地說明如何自訂預設繫結和應用程式載入器。
§宣告相依性
如果您有一個元件,例如控制器,而且它需要一些其他元件作為相依性,那麼可以使用 @Inject 註解來宣告這一點。@Inject
註解可以用在欄位或建構函式上。例如,要使用欄位注入
import javax.inject.*;
import play.libs.ws.*;
public class MyComponent {
@Inject WSClient ws;
// ...
}
請注意那些是實例欄位。注入靜態欄位通常沒有意義,因為它會破壞封裝。
使用建構函式注入
import javax.inject.*;
import play.libs.ws.*;
public class MyComponent {
private final WSClient ws;
@Inject
public MyComponent(WSClient ws) {
this.ws = ws;
}
// ...
}
欄位注入較短,但我們建議在應用程式中使用建構函式注入。這是最可測試的,因為在單元測試中,您需要傳遞所有建構函式參數來建立類別的執行個體,而編譯器會確保所有依賴項都在那裡。這也很容易了解發生了什麼事,因為沒有欄位的「神奇」設定。DI 架構只是自動化您可以手動編寫的相同建構函式呼叫。
Guice 還有其他幾種 注入類型,在某些情況下可能很有用。如果您正在遷移使用靜態項目的應用程式,您可能會發現其靜態注入支援很有用。
Guice 能夠自動建立任何類別,其建構函式上有 @Inject
,而不需要明確繫結它。此功能稱為 即時繫結,在 Guice 文件中更詳細地描述。如果您需要執行更複雜的操作,您可以宣告自訂繫結,如下所述。
§依賴注入控制器
Play 的路由編譯器會產生一個路由器類別,將您的控制器宣告為建構函式中的依賴項。這允許您的控制器注入到路由器中。
若要特別啟用注入路由產生器,請將下列內容新增到 build.sbt
中的建置設定
routesGenerator := InjectedRoutesGenerator
在控制器名稱前加上 @
符號具有特殊意義:控制器不會直接注入,而是會注入控制器的 Provider
。例如,這允許原型控制器,以及中斷循環依賴項的選項。
§元件生命週期
依賴注入系統管理注入元件的生命週期,視需要建立元件並將其注入其他元件。以下是元件生命週期運作方式
- 每次需要元件時都會建立新執行個體。如果元件使用多次,則預設會建立多個元件執行個體。如果你只想要一個元件執行個體,則需要將其標示為 單例。
- 執行個體會在需要時延遲建立。如果元件從未被其他元件使用,則根本不會建立。這通常是你想要的。對大多數元件來說,在需要之前建立它們沒有意義。不過,在某些情況下,你希望元件在應用程式啟動時立即啟動,甚至在其他元件未使用它們時也是如此。例如,你可能希望在應用程式啟動時傳送訊息到遠端系統或預熱快取。你可以使用 急切繫結 來強制建立元件。
- 執行個體不會自動清除,超出一般垃圾回收的範圍。當不再參照元件時,元件會被垃圾回收,但架構不會執行任何特殊動作來關閉元件,例如呼叫
close
方法。不過,Play 提供一種特殊類型的元件,稱為ApplicationLifecycle
,它讓你註冊元件,以便在應用程式停止時 關閉。
§單例
有時你可能有一個元件包含一些狀態,例如快取或與外部資源的連線,或者建立元件的成本很高。在這些情況下,只有該元件的一個執行個體非常重要。這可以使用 @Singleton 注解來達成
import javax.inject.*;
@Singleton
public class CurrentSharePrice {
private volatile int price;
public void set(int p) {
price = p;
}
public int get() {
return price;
}
}
§停止/清除
當 Play 關閉時,可能需要清除一些元件,例如停止執行緒池。Play 提供一個 ApplicationLifecycle 元件,可用於註冊掛鉤,以便在 Play 關閉時停止你的元件
import java.util.concurrent.CompletableFuture;
import javax.inject.*;
import play.inject.ApplicationLifecycle;
@Singleton
public class MessageQueueConnection {
private final MessageQueue connection;
@Inject
public MessageQueueConnection(ApplicationLifecycle lifecycle) {
connection = MessageQueue.connect();
lifecycle.addStopHook(
() -> {
connection.stop();
return CompletableFuture.completedFuture(null);
});
}
// ...
}
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(EnglishHello.class)
public interface Hello {
String sayHello(String name);
}
public class EnglishHello implements Hello {
public String sayHello(String name) {
return "Hello " + name;
}
}
§程式繫結
在一些更複雜的情況下,你可能想要提供更複雜的繫結,例如當你有多個介面實作時,這些實作會由 @Named 註解限定。在這些情況下,你可以實作自訂 Guice Module
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
public class Module extends AbstractModule {
protected void configure() {
bind(Hello.class).annotatedWith(Names.named("en")).to(EnglishHello.class);
bind(Hello.class).annotatedWith(Names.named("de")).to(GermanHello.class);
}
}
如果你呼叫此模組為 Module
並將其放在根目錄中,則它將自動向 Play 註冊。或者,如果你想要給它不同的名稱或將它放在不同的套件中,你可以將其完全限定的類別名稱附加到 application.conf
中的 play.modules.enabled
清單中,以向 Play 註冊它。
play.modules.enabled += "modules.HelloModule"
你也可以透過將名為 Module
的模組新增到已停用的模組中,來停用根目錄中模組的自動註冊
play.modules.disabled += "Module"
§可組態繫結
有時你可能想要在組態 Guice 繫結時讀取 Config
或使用 ClassLoader
。你可以透過將這些物件新增到模組的建構函式中來存取它們。
在以下範例中,每個語言的 Hello
繫結會從組態檔中讀取。這允許透過在 application.conf
檔中新增新的設定來新增新的 Hello
繫結。
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
import com.typesafe.config.Config;
import play.Environment;
public class Module extends AbstractModule {
private final Environment environment;
private final Config config;
public Module(Environment environment, Config config) {
this.environment = environment;
this.config = config;
}
protected void configure() {
// Expect configuration like:
// hello.en = "myapp.EnglishHello"
// hello.de = "myapp.GermanHello"
final Config helloConf = config.getConfig("hello");
// Iterate through all the languages and bind the
// class associated with that language. Use Play's
// ClassLoader to load the classes.
helloConf
.entrySet()
.forEach(
entry -> {
try {
String name = entry.getKey();
Class<? extends Hello> bindingClass =
environment
.classLoader()
.loadClass(entry.getValue().toString())
.asSubclass(Hello.class);
bind(Hello.class).annotatedWith(Names.named(name)).to(bindingClass);
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
}
});
}
}
注意:在多數情況下,如果你需要在建立元件時存取
Config
,你應該將Config
物件注入元件本身或元件的Provider
中。然後你可以在建立元件時讀取Config
。你通常不需要在為元件建立繫結時讀取Config
。
§即時繫結
在上述程式碼中,每次使用時都會建立新的 EnglishHello
和 GermanHello
物件。如果你只想建立這些物件一次,可能是因為建立它們很花費成本,那麼你應該使用 @Singleton
注解。如果你想建立它們一次,並在應用程式啟動時立即建立它們,而不是在需要時才建立,那麼你可以使用 Guice 的即時單例繫結。
import com.google.inject.AbstractModule;
import com.google.inject.name.Names;
// A Module is needed to register bindings
public class Module extends AbstractModule {
protected void configure() {
// Bind the `Hello` interface to the `EnglishHello` implementation as eager singleton.
bind(Hello.class).annotatedWith(Names.named("en")).to(EnglishHello.class).asEagerSingleton();
bind(Hello.class).annotatedWith(Names.named("de")).to(GermanHello.class).asEagerSingleton();
}
}
即時單例可以用於在應用程式啟動時啟動服務。它們通常與 關閉掛鉤 結合使用,以便服務可以在應用程式停止時清除其資源。
import javax.inject.*;
import play.inject.ApplicationLifecycle;
import play.Environment;
import java.util.concurrent.CompletableFuture;
// This creates an `ApplicationStart` object once at start-up.
@Singleton
public class ApplicationStart {
// Inject the application's Environment upon start-up and register hook(s) for shut-down.
@Inject
public ApplicationStart(ApplicationLifecycle lifecycle, Environment environment) {
// Shut-down hook
lifecycle.addStopHook(
() -> {
return CompletableFuture.completedFuture(null);
});
// ...
}
}
import com.google.inject.AbstractModule;
public class StartModule extends AbstractModule {
protected void configure() {
bind(ApplicationStart.class).asEagerSingleton();
}
}
§Play 函式庫
如果你正在為 Play 實作函式庫,那麼你可能希望它與 DI 架構無關,以便你的函式庫可以在應用程式中使用的任何 DI 架構中正常運作。因此,Play 提供了一個輕量級繫結 API,以 DI 架構無關的方式提供繫結。
若要提供繫結,請實作 模組 以傳回你想要提供的繫結順序。Module
特質也提供了一個用於建立繫結的 DSL
import com.typesafe.config.Config;
import java.util.Arrays;
import java.util.List;
import play.Environment;
import play.inject.Binding;
import play.inject.Module;
public class HelloModule extends Module {
@Override
public List<Binding<?>> bindings(Environment environment, Config config) {
return Arrays.asList(
bindClass(Hello.class).qualifiedWith("en").to(EnglishHello.class),
bindClass(Hello.class).qualifiedWith("de").to(GermanHello.class));
}
}
這個模組可以透過將它附加到 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"
§管理循環相依性
當您的元件之一依賴於另一個依賴於原始元件的元件(直接或間接)時,就會發生循環相依性。例如
public class Foo {
@Inject
public Foo(Bar bar) {
// ...
}
}
public class Bar {
@Inject
public Bar(Baz baz) {
// ...
}
}
public class Baz {
@Inject
public Baz(Foo foo) {
// ...
}
}
在此情況下,Foo
依賴於 Bar
,而 Bar
依賴於 Baz
,而 Baz
依賴於 Foo
。因此,您將無法實例化這些類別中的任何一個。您可以使用 Provider
來解決此問題
public class Foo {
@Inject
public Foo(Bar bar) {
// ...
}
}
public class Bar {
@Inject
public Bar(Baz baz) {
// ...
}
}
public class Baz {
@Inject
public Baz(Provider<Foo> fooProvider) {
// ...
}
}
請注意,如果您使用建構函式注入,當您有循環相依性時會更清楚,因為手動實例化元件是不可能的。
一般來說,循環相依性可以透過以更原子的方式拆分元件,或找出更明確的元件來相依,進而解決。一個常見的問題是相依於 Application
。當元件相依於 Application
時,表示它需要一個完整的應用程式才能執行工作;通常情況並非如此。相依性應建立在具有所需特定功能的更明確元件(例如 Environment
)上。最後,你可以透過注入 Provider<Application>
來解決問題。
§進階:擴充 GuiceApplicationLoader
Play 的執行時期相依性注入由 GuiceApplicationLoader
類別引導。此類別載入所有模組,將模組提供給 Guice,然後使用 Guice 建立應用程式。如果你想控制 Guice 如何初始化應用程式,則可以擴充 GuiceApplicationLoader
類別。
有幾個方法可以覆寫,但通常你會想要覆寫 builder
方法。此方法會讀取 ApplicationLoader.Context
並建立 GuiceApplicationBuilder
。下方你可以看到 builder
的標準實作,你可以隨意變更。你可以在關於 使用 Guice 進行測試 的部分中,找出如何使用 GuiceApplicationBuilder
。
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import play.ApplicationLoader;
import play.inject.guice.GuiceApplicationBuilder;
import play.inject.guice.GuiceApplicationLoader;
public class CustomApplicationLoader extends GuiceApplicationLoader {
@Override
public GuiceApplicationBuilder builder(ApplicationLoader.Context context) {
Config extra = ConfigFactory.parseString("a = 1");
return initialBuilder
.in(context.environment())
.loadConfig(extra.withFallback(context.initialConfig()))
.overrides(overrides(context));
}
}
當你覆寫 ApplicationLoader
時,你需要告訴 Play。將下列設定新增到你的 application.conf
play.application.loader = "modules.CustomApplicationLoader"
您不限於使用 Guice 進行依賴注入。透過覆寫 ApplicationLoader
,您可以控制應用程式的初始化方式。
§在不觸及子類別的情況下,將依賴項新增至類別
有時您可能想要將新的依賴項新增至某些可能具有許多子類別的基底類別。
為避免直接提供依賴項給每個子類別,您可以將其新增為可注入欄位。
此方法可能會降低類別的可測試性,因此請小心使用。
import com.google.inject.ImplementedBy;
@ImplementedBy(LiveCounter.class)
interface Counter {
public void inc(String label);
}
public class NoopCounter implements Counter {
public void inc(String label) {}
}
import javax.inject.Singleton;
@Singleton
public class LiveCounter implements Counter {
public void inc(String label) {
System.out.println("inc " + label);
}
}
import javax.inject.Inject;
import play.mvc.Controller;
import play.mvc.Result;
public class BaseController extends Controller {
// LiveCounter will be injected
@Inject protected volatile Counter counter = new NoopCounter();
public Result someBaseAction(String source) {
counter.inc(source);
return ok(source);
}
}
import javax.inject.Singleton;
import play.mvc.Result;
@Singleton
public class SubclassController extends BaseController {
public Result index() {
return someBaseAction("index");
}
}
下一步:編譯時間依賴注入
在此文件中發現錯誤?此頁面的原始程式碼可以在 這裡 找到。閱讀完 文件指南 後,請隨時貢獻一個 pull request。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。