【Scala】Play! の Guice による Akka Actor の DI

2015.12.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

PlayFramework のバージョン 2.4 以降(以下 PlayFramework)には DI コンテナとして Guice が採用されています。 また、PlayFramework は Scala で書かれたフレームワークで内部的には Akka を利用しています。そのため、PlayFramework のアプリケーションからは Akka Actor を比較的簡単に使用でき、弊社のプロジェクトでもよく利用されています。
Guice による制御の反転を実践している PlayFramework のプロジェクトで Akka Actor を使用する際には、 Guice による Actor の生成 を行うのが良いでしょう。

というわけで、今回は Guice で Akka Actor の生成と Inject を行う方法についてご紹介します。

目次

Actorの準備

Actorを作成する

今回は次のようなアクターを作成しました。

app/SuffixActor.scala

import akka.actor.{ActorLogging, Actor}
import SuffixActor.Tell

class SuffixActor extends Actor with ActorLogging {

  val suffix = "なん?"

  override def receive: Receive = {
    case Tell(c) => log.info(c + suffix)
  }
}

object SuffixActor {
  case class Tell(content: String)
}

受け取った文字列の最後に「なん?」を付加する簡単なアクターです。
例えば SuffixActor.Tell("大丈夫") というメッセージを受け取ると 大丈夫なん? というログを出力します。

このアクターは現在のところハードコードされた「なん?」を文字列の末尾に付加しています。
こういった値がハードコードされているのはあまり良くないため、この値を設定ファイルから読み出すようにしたいと思っています。

設定ファイルに値を追加する

今回は Guice によって Inject される PlayFramework の Configuration から値を取得しようと思っています。 そのため、先に設定ファイルに値を追加しておきましょう。

conf/application.conf

// ...

suffixActor.suffix += "なん?" // 追加

これで設定ファイルから値を読みだす準備ができました。

Actorを修正する

先ほどのアクターを設定ファイルから値を読みだすように修正しましょう。

app/SuffixActor.scala

import javax.inject.Inject
import akka.actor.{Actor, ActorLogging}
import play.api.Configuration
import SuffixActor.Tell

class SuffixActor @Inject()(
  configuration: Configuration
) extends Actor with ActorLogging {

  lazy val suffix = configuration.getString("suffixActor.suffix").get

  override def receive: Receive = {
    case Tell(c) => log.info(c + suffix)
  }
}

object SuffixActor {
  case class Tell(content: String)
}

通常のクラスへの DI と同じように、 @InjectConfiguration のインスタンスを受け取ります。もちろん、そのためには SuffixActor 自身も Guice により生成される必要があります。

ActorのDI

AbstractModule の定義

今回の前提は「PlayFramework と Guice を利用しているアプリケーション」です。通常、このようなアプリケーションでは依存性解決のモジュールを定義します。というわけで今回は、Akka Actor の依存性解決モジュールとして ActorDependencyModule を定義したいと思います。

app/ActorDependencyModule.scala

import com.google.inject.AbstractModule
import play.api.libs.concurrent.AkkaGuiceSupport

class ActorDependencyModule extends AbstractModule with AkkaGuiceSupport {
  
  override def configure(): Unit = {
    bindActor[SuffixActor]("suffixactor")
  }
}

このように定義すれば @Named("suffixactor") という名前付き解決で SuffixActorActorRef を取得できるようになります。

また、依存性解決モジュールを作成しただけでは反映されないため、設定ファイルにモジュールへの参照を追加します。

conf/application.conf

// ...

suffixActor.suffix += "なん?"
play.modules.enabled += "ActorDependencyModule" // 追加

これで @Named("suffixactor") の名前付き解決で「 Configuration が注入された SuffixActorActorRef 」 を取得する準備が整いました。

実際に試してみる

初期化時に SuffixActor に対し「残業」というメッセージを送るだけのモジュールを作成しました。

app/SuffixActorBootstrap.scala

import javax.inject.{Named, Inject, Singleton}
import akka.actor.ActorRef

@Singleton
class SuffixActorBootstrap @Inject() (
  @Named("suffixactor") suffixActor: ActorRef
) {

  suffixActor ! SuffixActor.Tell("残業")
}

このクラスを Guice に EagerSingleton として生成させることで、アプリケーション起動時に一度だけ「残業」メッセージが送信されるようになります。

app/ActorDependencyModule.scala

import com.google.inject.AbstractModule
import play.api.libs.concurrent.AkkaGuiceSupport

class ActorDependencyModule extends AbstractModule with AkkaGuiceSupport {
  
  override def configure(): Unit = {
    bindActor[SuffixActor]("suffixactor")
    bind(classOf[SuffixActorBootstrap]).asEagerSingleton() // 追加
  }
}

実際に activator run などで実行し localhost:9000 にアクセスして初期化処理を走らせると 残業なん? というログが出力されます。
また試しに application.conf の設定値を変えて再起動してみると、ちゃんと反映されていることが確認できると思います。

注意すべきポイント

Guice による Akka Actor の依存性解決は非常に便利です。
しかし、一つ注意しなければならないポイントがあります。それは Guice により生成される Actor は system 直下に配置されることです。
独自に定義した Supervisor の子アクターとしてアクターを関連付けたい場合には Guice による生成を行うことができません。
この問題は、子アクターの生成は Supervisor 自身が行い、子アクターが必要とする依存モジュールは Supervisor に Inject し、 Supervisor 自身は Guice により生成させることで解決可能です。

註: play.api.libs.concurrent.InjectedActorSupport をミックスインすると injectChild(create: => Actor, name: String) が利用でき、 Guice のファクトリ自動生成と併せれば Guice による子アクターの生成が可能に思えます。しかしながら、この方法では Supervisor に管理される他の ActorRef を子アクターに Inject できません。これの解決方法としては、Supervisor がイベントバスになる、アクターが context.become を利用する前提で ActorRef 待ちのステートを持つなどが挙げられますが、前者は複雑さの観点、後者はフォールトトレランスの観点から無理筋と考えられます。

まとめ

  • Guice は Akka Actor を扱うことができます。
  • Supervisor に紐付く子アクターについては Guice に生成させず Supervisor 自身が生成し、子アクターの依存モジュールは Supervisor に DI しましょう。

PlayFramework と Akka Actor をフル活用し、耐障害性の高いアプリケーションを作りましょう! ではまた!

参考