はじめに (対象読者・この記事でわかること)

この記事は、Playframework 2.5(Java)でAkkaのsystem.scheduler()を使おうとして
ClassCastException: scala.concurrent.impl.ExecutionContextImpl cannot be cast to play.api.libs.streams.ExecutionContextProvider
とエラーに悩まされている開発者向けです。

Play 2.4 まで動いていた定期実行コードが、2.5 にアップデートした途端に動かなくなり、ググっても日本語記事が皆無だった…という経験をされた方に読んでいただきたい内容です。

記事を読み終えると、
・Play 2.5 での Dependency Injection の仕組み変化
・Akka Scheduler を安全に使うための正しい設定方法
・エラーの根本的な回避策
がしっかり身につきます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Java8 以降の言語機能(ラムダ式など) - Playframework のルーティング・コントローラの基本 - Akka Actor の基礎用語(ActorSystem, ActorRef など)

Play 2.5 で変わった DI 仕様と Akka の関係

Play 2.5 から Dependency Injection の実装が Guice 一本に統一され、従来の Play.currentPlay.application() などの静的アクセスが非推奨になりました。
それに伴い、Akka の ActorSystem も Guice 管理下に移行。
結果、次のような「古い」コードが動かなくなります。

Java
// Play 2.4 以前で動いていたコード ActorSystem system = Play.application().actorSystem(); system.scheduler().scheduleOnce( Duration.create(5, TimeUnit.SECONDS), () -> Logger.info("5秒後に実行"), system.dispatcher() );

2.5 以降、上記コードを実行すると
scala.concurrent.impl.ExecutionContextImplExecutionContextProvider へキャストしようとして失敗、
つまり ClassCastException が発生します。
これは、Guice が注入する ActorSystem が「Play 内部でラップされた特殊な実装」であり、
従来のように system.dispatcher() をそのまま scheduler に渡すと型互換が崩れるためです。

Play 2.5 以降で Akka Scheduler を使う正しい手順

以下、実プロジェクトで動作検証済みの設定・コードを共有します。

ステップ1:build.sbt の記述確認

Play 2.5 以降、Java 用 Akka scheduler を使うのに追加モジュールは不要です。
念のため、バージョンを明示しておくとトラブルが減ります。

Scala
// project/plugins.sbt addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.19") // build.sbt scalaVersion := "2.11.11" // or 2.12.x libraryDependencies ++= Seq( javaWs, javaJpa, // Akka 関連は Play が既に依存しているため追加不要 )

ステップ2:独自実行用の ExecutionContext を用意する

Guice 注入された ActorSystem の dispatcher をそのまま使わず、
明示的に ExecutionContext.fromExecutor を新規作成してやるのがポイントです。

Java
package tasks; import akka.actor.ActorSystem; import play.inject.ApplicationLifecycle; import scala.concurrent.ExecutionContext; import scala.concurrent.duration.Duration; import javax.inject.Inject; import javax.inject.Singleton; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @Singleton public class ScheduledTask { private final ActorSystem system; private final ExecutionContext customExecutionContext; @Inject public ScheduledTask(ActorSystem system, ApplicationLifecycle lifecycle) { this.system = system; // 独自 ExecutionContext を生成 this.customExecutionContext = system.dispatchers().lookup("akka.actor.default-dispatcher"); // アプリ終了時にキャンセルできるように返却 lifecycle.addStopHook(() -> { // 必要に応じて Cancellable を保持して cancel() 呼ぶ return CompletableFuture.completedFuture(null); }); start(); } private void start() { system.scheduler().schedule( Duration.create(0, TimeUnit.SECONDS), // 初回即実行 Duration.create(30, TimeUnit.SECONDS), // 30秒周期 this::run, customExecutionContext ); } private void run() { play.Logger.info("⏰ 30秒周期で実行中・・・"); } }

ステップ3:Module に登録してアプリケーション起動時に初期化

Java
import com.google.inject.AbstractModule; public class TaskModule extends AbstractModule { @Override protected void configure() { bind(ScheduledTask.class).asEagerSingleton(); } }

application.conf に追記

play.modules.enabled += "modules.TaskModule"

これで Play 起動と同時に scheduler が動き始めます。

ハマった点・エラー解決

  1. ClassCastException が出続ける
    system.dispatcher() を直接 scheduler に渡しているのが原因。
    独自 ExecutionContext を作って差し替えることで解消。

  2. 開発モード(run)では動くが、本番モード(start)で動かない
    ApplicationLifecycle の stopHook で scheduler をキャンセルしていないと、
    クラスローダーが解放されずにリークする。
    必ず Cancellable を保持して cancel() 呼ぶか、上記例のように
    ライフサイクルフックで安全に終了させる。

  3. テストコードで ActorSystem が見つからない
    WithApplication などでアプリケーションを起動してから
    app.injector().instanceOf(ActorSystem.class) で取得する。

解決策まとめ

Play 2.5 以降、Akka Scheduler を使う際は
「Guice 注入された ActorSystem は触らぬが華」ではなく、
「dispatcher をそのまま使わず、明示的に ExecutionContext を生成してやる」
という意識を持つだけで、ClassCastException は確実に回避できます。

まとめ

本記事では、Playframework 2.5 以降で Akka Scheduler が動かなくなる理由と、
それを回避するための正しい DI の設定方法を解説しました。

  • Play 2.5 で DI が Guice 一本化され、ActorSystem も管理対象に
  • 従来コードのまま system.dispatcher() を scheduler に渡すと ClassCastException
  • 独自 ExecutionContext を生成・指定することで安全に定期実行可能

この知見を活かせば、Play 2.5 でも 2.6、2.7、2.8 と移行する際にも
Akka まわりのトラブルを最小限に抑えながら、安定した定期処理を実装できます。

次回は「Play 2.8 + Akka Typed での scheduler 実装」について掘り下げていきます。

参考資料