Puzzle : EJB Timer #2

この記事は Java Puzzlers Advent Calendar の 18 日目です。昨日同様、EJB タイマーに関する問題を続けて出題します。

問題

次のような EJB タイマーを含む Web アプリケーションを、Java EE 7 Full Platform の環境にデプロイしました。想定される動作は以下のどれでしょうか?

@Stateful
public SomeTimeBean {
  @Schedule(minute = "10", persistent = false)
  public void execute() {
    System.out.println("Just a time!");
  }
}

(1) 毎時 10 分に "Just a time!" という文字列を出力する

(2) 毎日 0 時 10 分に "Just a time!" という文字列を出力する

(3) デプロイに失敗する

解答

(3) デプロイに失敗する

解説

昨日の問題は EJB タイマーを構築する @Schedule アノテーションのパラメータ誤りでした。本日の問題は、@Schedule アノテーションに誤りはありませんし、永続型の EJB タイマーもデプロイ可能な Java EE 7 Full Platform が前提です。

問題は別のところにあります。EJB タイマーの適用範囲は Stateless Bean もしくは Singleton Bean に限定されます。適用外の Stateful Bean に対して @Schedule アノテーションが付加された場合には、デプロイすることができません。これは EJB タイマーの仕様です。

この問題では、クラスに対して @Stateful アノテーションが付加されており、Stateful Bean であることが宣言されています。その上で @Schedule アノテーションを用いて EJB タイマーを宣言しているため、この EJB 自体がデプロイできないのです。

修正方法は、可能であれば @Stateful アノテーションを @Stateless アノテーションまたは @Singleton アノテーションに置き換えて、Stateless Bean または Singleton Bean とします。

  • EJB がフィールドを持たない場合には、@Stateless アノテーションに置き換えて Stateless Bean にすれば問題ありません。
  • EJB がフィールドを持つ場合で、かつ EJB のインスタンスが 1 つで足りるようであれば、@Singleton アノテーションに置き換えて Singleton Bean とすることにより問題を回避できます。
  • EJB がフィールドを持ち、かつ EJB のインスタンスが複数必要となる場合は、Stateful Bean として維持しなければならないため、残念ですが EJB タイマーを諦めることになります。

3 種類の Session Bean の利用頻度を考慮すると、大半が Stateless Bean になるため、問題になることは少ないとは思いますが、@Schedule アノテーションのにわか知識だけで EJB タイマーを使おうとすると以外とハマりやすい落とし穴です。Java EE の勉強会のスライドで EJB タイマーに触れたものは、Stateful Bean が適用外であることを明示していないものがそれなりにあるため (多分、当日のプレゼンでは口頭で説明しているはず...)、初学者はなおさら注意が必要です。

以上の修正を行った場合は、昨日の修正後と同じ挙動を示します。すなわち、

この場合、想定される動作は選択肢 (2)、すなわち、毎日 0 時 10 分に "Just a time!" という文字列を出力します。@Schedule のパラメータの既定値は、日付部である year、month、dayOfMonth、dayOfWeek についてはすべてワイルドカード "*" となっていますが、時刻部である hour、minute、second についてはすべて "0" となっています。

選択肢 (1) のように毎時 10 分に "Just a time!" という文字列を出力したい場合には、以下のように hour をワイルドカード指定する必要があります。

なお、余談になりますが、Stateless Bean であってもフィールドを持たせること自体は可能です。ただし、その内容は保証されません。これは 1 つのセッションに対して常に同じインスタンスが lookup される Stateful Bean と異なり、Session Bean はプーリングされたインスタンスの中から毎回 lookup し直すため、必ずしも同じインスタンスになるとは限らないためです (インスタンスが異なるとフィールド値も異なる可能性が高く、フィールドが値を保持する意味がなくなります)。文献によっては毎回異なるインスタンスが lookup されるとありますが、それも誤った記述です。正確には、どのインスタンスが lookup されるか保証されません。多くの場合では異なるインスタンスが lookup されますが、同一のインスタンスが lookup される可能性もあるのです。具体例を挙げると、サンプルプログラムなどで Stateless Bean のプールにインスタンスが 1 つしか存在しない場合などは、毎回同じインスタンスを lookup することになります。また、実運用であっても、直前に lookup したインスタンスを再び lookup する可能性もゼロではありません。

これは昨日の問題にも共通して言えることですが、Java EE サーバーの Deployer が発するエラーメッセージは不親切なものが多く、ログに出力されたエラーメッセージだけでは何が間違っているのか分からないことが多々あります。かといって Java EE の文献は公式ドキュメントを含めてザルであることが多いため、最終的には「自分だけを信じろ、他人を信じるな」という結論に達します。今後も共に精進しましょう!