1. 程式人生 > >Play 2.6 依賴注入

Play 2.6 依賴注入

依賴注入

Play對實現了JSR 330的依賴注入提供了支援。Play自帶的預設的實現是Guice,其他實現也可以新增進來。為了啟用Guice模組,先要在依賴中新增資訊

libraryDependencies += guice

Guice wiki是學習Guice和依賴模式非常好的資源。

Motivation

Dependency injection achieves several goals:
1. It allows you to easily bind different implementations for the same component. This is useful especially for testing, where you can manually instantiate components using mock dependencies or inject an alternate implementation.
2. It allows you to avoid global static state. While static factories can achieve the first goal, you have to be careful to make sure your state is set up properly. In particular Play’s (now deprecated) static APIs require a running application, which makes testing less flexible. And having more than one instance available at a time makes it possible to run tests in parallel.

The Guice wiki has some good examples explaining this in more detail.

工作原理

Play提供了大量的內建元件並且在modules中進行了宣告,比如BuiltinModule。這些繫結描述了每一個建立一個Application例項所需的東西,包括,在預設情況下,根據routes生成一個router,並且將controller都注到構造中。這些繫結可以在後續中轉化為Guice或其他執行時DI框架的工作。

Play小組維護Guice模組,提供了一個 GuiceApplicationLoade。該模組為Guice轉化繫結,根據繫結建立Guice注入器,然後衝注入器中獲取一個Application例項。

也有第三方的載入器為其他的框架進行這些工作,包括Spring

下面將會展示如何定製化預設的繫結和應用載入器。

宣告依賴

如果你又一個元件,並且該元件需要其他元件作為依賴,然後可以使用@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的文件中獲取更多細節。如果你需要更加複雜的功能,下面將介紹如何進行訂計劃的繫結。

注入Controller

有兩種方式可以注入controller

注入routes generator

預設情況下,從2.5.0開始,Play會生成一個router類在構造器中宣告你的controller。這允許將你的controller注入到router中。

如果想特地的啟用routes generator,在build.sbt新增配置:

routesGenerator := InjectedRoutesGenerator

當使用routes generator時,在acrion前新增一個@字首會有特別的含義,這意味著一個controller的Provider會代替controller被注入到router中。這樣允許使用protype controller,可以作為打破迴圈依賴的選項。

靜態routes generator

你可以配置Play來使用遺留的靜態routes generator(在2.5.0之前),這會假設所以的action都是靜態方法,可以使用以下配置

routesGenerator := StaticRoutesGenerator

我們建議使用注入的routes generator。在已有工程沒辦法一次將controller非靜態化,靜態routes generator作為一種遷移手段而存在。

如果使用靜態routes generator,你可以通過新增一個@字首表明action有一個被注入的controller

GET        /some/path        @controllers.Application.index()

元件生命週期

依賴注入系統管理被注入元件的生命週期,在需要的時候建立然後注入到其他逐漸中。下面展示了元件的宣告週期如何工作:
- 在需要時每次都建立新的例項。如果一個元件使用超過一次,預設情況下,會有多個例項被建立。如果你指向要一個例項,可以標記為單例
- 例項懶載入。如果一個元件沒有被別的元件使用,那麼就不需要去建立例項,這也是你希望的方式。對於大多陣列件,在被使用前都不會建立。無論如何,你希望元件直接啟動。舉個例子,在服務啟動時你想給一個遠端服務傳送資訊或者預熱快取。可以通過eager binding來強制元件儘早建立。
- 例項不會自動清理,超出正常的垃圾回收之外。元件在他們不再被引用了之後才會回收,但是框架不會做任何操作來結束元件,像是呼叫一個close方法。不過元件提供一種特殊型別的元件,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 javax.inject.*;
import play.inject.ApplicationLifecycle;

import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;

@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會以建立的反序關閉所以的元件。這意味著你所依賴的任何元件都可以在停止hook中安全的使用,因為你依賴於這些元件,他們必須在你的元件之前建立,因此在你的元件關閉前它們都不會被停止。

Note: 註冊到鉤子中的元件必須是單例。任何非單例的元件都有可能導致記憶體洩漏,因為沒建立一個元件都會註冊一個新的鉤子

提供定製化的繫結

一個很好的實踐是為元件定義一個介面,然後提供一個實現類而不是直接去實現元件。提供介面可以讓你為元件注入不同的實現,比如你可以注入一個mock來測試你的程式。

在這種情況下,DI框架需要知道去繫結哪一個實現。我們的建議根據你是一名Play應用的使用者還是一個Play類庫的開發者有所區別。

Play應用開發

我們建議在Play中使用DI框架。雖然Play提供繫結API,但是作用非常有限。

Play對Guice提供了很好的支援,下面的例子都使用Guice

繫結註解
import com.google.inject.ImplementedBy;

@ImplementedBy(EnglishHello.class)
public interface Hello {

    String sayHello(String name);
}
程式繫結

在一些更加複雜的情況下,你也許需要提供更加負責的繫結,比如一個特質擁有多個實現,可以使用@Named進行限定,這種情況可以實現一個自定義的Guice Model

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);
    }
}

如果你將model起名為Module並放在根目錄下,會自動註冊Play。當然你也可以使用application.conf中進行配置

play.modules.enabled += "modules.HelloModule"

你也可以停用對根目錄下的Module自動注入

play.modules.disabled += "Module"
可配置繫結

再配置Guice時,也許你想讀取Config或者使用CLassLoader。你可以將這些新增到module的構造器中。

在下面的例子中,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);
            }
        });
    }
}
Note: 在大多數情況下,如果你需要在建立元件中獲取Config,你需要將Config物件注入到元件中,或者Provider裡。然後在建立元件時就可以讀取Config。在為元件建立繫結時一般不需要讀取Config
Eager bindings

在下面的例子中,EnglishHello和GermanHello在使用時每次都會建立新的物件。如果你只需要建立一次,可以使用@Singleton註解。如果你只想建立一次而且要儘可能早的建立,可以使用Guice’s eager singleton bindingbs

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();
    }
}

Eager singletons可以在應用啟動時開啟一些服務。他們通常與關閉鉤子一起使用,在應用結束時清理資源

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框架不可見,這樣你的類庫才能與各類框架一起使用。Play提供了一個輕量級的API來幫助你完成這個功能。

實現一個Module返回一個你需要繫結的序列。Module特質也提供了用於構建繫結的DSL

import play.api.*;
import play.api.inject.*;
import scala.collection.Seq;

public class HelloModule extends Module {
    @Override
    public Seq<Binding<?>> bindings(Environment environment, Configuration configuration) {
        return seq(
            bind(Hello.class).qualifiedWith("en").to(EnglishHello.class),
            bind(Hello.class).qualifiedWith("de").to(GermanHello.class)
        );
    }
}

可以在reference.conf(application.conf也可以)中新增如下內容,Play會自動進行註冊

play.modules.enabled += "com.example.HelloModule"
  • Module bindings方法獲取一個Play Environment和Configuration物件,如果你想動態的配置繫結,也可以去獲取這些引數。
  • 同樣支援eager bindings。可以通過.eagerly()方法來宣告。

為了儘可能大的增加相容性,記住以下幾條:
- 不是所有的框架都支援及時繫結。保證你類庫鎖提on個的元件有個顯示邊界。
- 試著去保持繫結key的簡單性,不同的框架對於什麼是key,key是否不同有著不同的視角

排除modules

如果有一個module你不想載入,你可以在application.conf中進行排除

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) {
    // ...
  }
}

在這個例子中,你不可能建立任何例項,可以通過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。當你的元件依賴Aplication時,表明需要一個完整的應用來支援這個元件,大多數情況下都是不必要的。你的元件需要依賴於一下更加具體的元件(比如Environment)提供你所需的特定功能。最後你可以注入一個Provider<Application>

高階應用,擴充套件GuiceApplicationLoader

Play執行時依賴注入由GuiceApplicationLoader這個類載入所有的module,將module傳遞給Guice,然後使用Guice建立應用。如果你想控制Guice初始化的過程,你可以擴充套件GuiceApplicationLoader

GuiceApplicationLoader有很多方法可以被覆蓋,通常來說你只需要覆蓋builder方法。這個方法讀取ApplicationLoader.Context然後建立一個GuiceApplicationBuilder。下面展示了一個標準的builder實現,你可以按你的需要來修改。你可以在測試Guice章節來看到具體的用法。

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。

play.application.loader = "modules.CustomApplicationLoader"

在Play中並不是只能使用Guice進行依賴注入,通過重寫ApplicationLoader你可以對初始化進行控制。