Scala
GoogleGuice

Google Guiceについて、もうちょっと調べた

More than 3 years have passed since last update.

前回の記事で紹介したのは、Google GuiceのDI機能のうちの一つ、Linked Bindingsだけでした。この記事はその続きです。

サンプルはScalaで書きましたが、例によってScalaっぽいコードが出てくるわけではないですね……Guiceをどう使えばScalaとよりよく連携できるようになるかは、まだよく分かっていません1

さて、Guiceはもうちょっと色々な方法でDIができるので、それについても調べて紹介します。

以下では例示のために、前回も使用したServiceとその実装クラス2つを使います。

trait Service {

def doSomething(): Unit
}

class FakeService extends Service {
def doSomething() { println("FakeService") }
}

class RealService extends Service {
def doSomething() { println("RealService") }
}


用語

あるインタフェースに、特定の実装を結びつけることをバインディング(binding)と呼ぶようです。

バインディングを柔軟に制御することがDIフレームワークの目標です。


さまざまなバインディング方式


Binding Annotations

Linked Bindingでは、一つの型に一つの具体的な実装を配線させることができました。

Binding Annotationは、バインディングの配線条件をより細かくすることができる機能です。条件には@Namedアノテーションを利用します。例えば「型Aは実装Bを使うか、実装Cを使うかをアノテーションによって変える」といったことができます。


BindingAnnotaion.scala

class ServiceUser @Inject() (@Named("realService") service: Service) {

def use() { service.doSomething() }
}

class AnotherServiceUser @Inject() (@Named("fakeService") service: Service) {
def use() { service.doSomething() }
}

class ServiceModule extends AbstractModule {
override protected def configure() {
bind(classOf[Service]).annotatedWith(Names.named("realService")).to(classOf[RealService])
bind(classOf[Service]).annotatedWith(Names.named("fakeService")).to(classOf[FakeService])
}
}


また、カスタムのアノテーションを作ることもできます。

実行例はこんな感じになります。

def run() {

val injector = Guice.createInjector(new ServiceModule)
val user = injector.getInstance(classOf[ServiceUser])
val another = injector.getInstance(classOf[AnotherServiceUser])
user.use() // => "RealService"
another.use() // => "FakeService"
}


Instance Bindings

ある型をある型にバインディングするのではなく、ある型に具体的なインスタンスをバンディングする機能です。@Namedなどと同時に使用することもできます。

class ServiceUser @Inject()(service: Service) {

def use() {
println("received: " + service)
}
}

class ServiceModule extends AbstractModule {
override protected def configure() {
val service = new RealService
println("bound: " + service)
bind(classOf[Service]).toInstance(service)
}
}

例によってInjectorを準備してインスタンスを生成・実行してみると、確かに.toInstanceで指定したインスタンスがServiceUserに渡っていることが分かります。

def run() {

val injector = Guice.createInjector(new ServiceModule)
val user = injector.getInstance(classOf[ServiceUser])
user.use()
}


実行結果

bound: RealService@16f332ef

received: RealService@16f332ef

このバインディング方式は、複雑なオブジェクトの生成には向きません。

その場合には@Providesが推奨2されます。


ProvidesMethods

オブジェクトの生成をModule内で実装できるようにします。@Providesアノテーションを付与することで、そのメソッドがインスタンス生成に利用されるようになります。

class ServiceUser @Inject()(service: Service) {

def use() {
service.doSomething()
}
}

class ServiceModule extends AbstractModule {
override protected def configure() {}

@Provides
def provideRealService(): Service = new RealService
}

なお、プロバイダメソッドから例外を投げるのは推奨されないようです。


ProviderBindings

ProvidersMethodsがファクトリメソッドを作ってインスタンス生成を分離する機能だったのに対して、インスタンス生成を別クラスに分離するのがProviderBindingsです。より生成が複雑化した場合などに用いられるようです。

生成を行うクラスは、Provider[T]を実装するのがルールです。このクラスはTを返すメソッドget()を実装さえすれば機能します。

class RealServiceProvider extends Provider[Service] {

override def get(): Service = new RealService
}

生成の委譲は.toProviderを記述することで行えます。

class ServiceModule extends AbstractModule {

override protected def configure() {
bind(classOf[Service]).toProvider(classOf[RealServiceProvider])
}
}


Constructor Bindings

型を型ではなく、型を指定したコンストラクタにバインドすることもできます。

例えば、依存性を注入する先のクラスのコンストラクタが複数ある場合に利用できます3

また、AOPなどでメタプログラミングをする場合にも必要なケースがあるとのことです。


class ServiceUser @Inject()(service: Service) {
def use() {
service.doSomething()
}
}

class ServiceModule extends AbstractModule {
override protected def configure() {
bind(classOf[Service]).toConstructor(classOf[RealService].getConstructor())
}
}


Just-In-Time Bindings

ある型に対する実装が必要になったときでも、モジュールにバインドの記述がない場合、明示的バインディング(explicit bindings)ではなく暗黙のバインディング(Just-In-Time bindings / implicit bindings)が試みられます。

暗黙のバインディングには、明示的なコンストラクタと呼ばれるものと、@ImplementedByを使うもの、@ProvidedByを使うものがあります。


明示的コンストラクタ

class RealService (@Named("serviceName") name: String) extends Service {

override def doSomething(): Unit = println("service name is " + name)
}

class ServiceUser @Inject()(service: Service) {
def use() {
service.doSomething()
}
}

class ServiceModule extends AbstractModule {
override protected def configure(): Unit = {
bind(classOf[Service]).to(classOf[RealService])
}

@Provides
@Named("serviceName")
def provideServiceName: String = "ServiceName!!"
}

この例では、RealServiceについてのバインディングがServiceModuleに明示的に記述されているわけではないにもかかわらず、RealServiceインスタンスへnameが注入される形で全体のインスタンス化が行われます。

対象のクラスは、publicなコンストラクタを1つのみ持ち、かつコンストラクタ引数はたかだか1つ(0 or 1)でなければならないです。


@ImplementedBy

抽象に対するデフォルト実装をアノテーションで指定することができます。

@ImplementedBy(classOf[AnotherFakeService])

trait AnotherService {
def doSomething(): Unit
}

class AnotherFakeService extends AnotherService {
override def doSomething(): Unit = println("AnotherFakeService")
}

class ServiceModule extends AbstractModule {
override protected def configure() {}
}

上記コードを実行する実装を示します。

def run() {

val injector = Guice.createInjector(new AbstractModule {
override def configure(): Unit = {}
})
val service = injector.getInstance(classOf[AnotherService])
service.doSomething() // => "AnotherFakeService"
}

つまり、Modulebind(〜).to(...)で実装を指定するのと同様の効果をインタフェースへの指定で実現できます。ただし、bindで指定された明示的なバインディングがある場合には、そちらが優先されます。


@ProvidedBy

@ImplementedByが具体的なデフォルト実装を指定するものだったのに、対して、@ProvidedByはデフォルトのプロバイダを指定します。Modulebind(〜).toProvider(...)指定をするのに相当します。明示的なバインディングがある場合はそちらが優先されるのも同様です。

@ProvidedBy(classOf[AnotherRealServiceProvider])

trait AnotherService {
def doSomething(): Unit
}

class AnotherRealService extends AnotherService {
override def doSomething(): Unit = println("AnotherRealService")
}

class AnotherRealServiceProvider extends Provider[AnotherService] {
override def get(): AnotherService = new AnotherRealService
}


Untargeted Bindings

バインドする先を指定しないbindを記述することができます。Guiceにバインドがあることを伝えられるので、依存関係をeagerに解決することができるようになります。

もちろん、通常は別の方法(例えば@ImplementedBy)で依存関係が記述されている事が前提です。

bind(classOf[Service]) // .toがないのでuntargeted binding

bind(classOf[Service]).in(classOf[Singleton]) // スコープ指定(後述)をすることができる


Built-in Bindings

java.util.logging.Loggerなど、いくつかGuiceが組み込みで把握しているバインディングがあります。詳細についてはドキュメントを参照してください。

https://github.com/google/guice/wiki/BuiltInBindings


スコープ

オブジェクトの生存区間(lifetime)を表すために、Guiceではスコープが導入されています。スコープの指定に従って、Guiceは新しくインスタンスを生成するか、使い回しをするか決めます。

スコープを指定するアノテーションには@Singleton@SessionScoped@RequestScopedがあり、実装クラスにアノテーションすることでスコープを指定できます。それぞれがアプリケーション、セッション、リクエストを範囲とした生存区間を持ちます。

アノテーションの適用例です。

@Singleton

class RealService extends Service {
def doSomething() {
println("This is RealService")
}
}

bindの際に、.inによる指定も可能です。

bind(classOf[Service]).to(classOf[RealService]).in(classOf[Singleton])

ちなみに、スコープはカスタムする事も可能そうです。


Stageの切り替え

インスタンスの生成をeagerに行うか、lazyに行うかを、ステージと呼ばれる設定項目を切り替えることで変更できます。

ステージにはPRODUCTIONDEVELOPMENTがあります。開発時にはコンパイルしてから動作するまでの時間を短くするために、lazyなロードが向いています。一方でプロダクション動作の際にはeagerなロードが行われるようになっています。

ステージは、Guice.createInjector時に引数としてStageを渡すことで指定することができます。

Guice.createInjector(Stage.PRODUCTION, module)


EagerなSingleton

バインド時に.asEagerSingleton()を指定することで、ステージの設定にかかわらず、eagerにオブジェクトをロードさせることができます。

bind(classOf[Service]).to(classOf[RealService]).asEagerSingleton()


JSR-330との関係

Guiceが提供するアノテーションと、JSR-3304で定義されている@Injectなどのアノテーションには微妙に違いがあるため、importを間違えるとうまく動かなくなることがあります。

特に注意が必要なのは@InjectとProvider周りです。名前が同じで振る舞いが微妙に異なるからです。詳細はGuice公式のドキュメントを参照してください。

https://github.com/google/guice/wiki/JSR330


参考資料





  1. もっとも、少なくともJavaとScalaの相性が良いのと同程度には、GuiceとScalaの相性も良いです。 



  2. 公式ドキュメントいわく、"Avoid using .toInstance with objects that are complicated to create, since it can slow down application startup. You can use an @Provides method instead." 



  3. Guiceは依存性の解決時に、目標のクラスに複数コンストラクタがある場合、どのコンストラクタを使えばいのか判断できなくなります。単純な場合だと、@Providesなどで生成を分離することでも対応できます。 



  4. JSR-330はDIについてのJava標準です。