Java
glassfish
JavaEE
EJB

JavaEE使い方メモ(EJB)

More than 1 year has passed since last update.

環境構築は こちら

ソースコードは GitHub にあげています。
https://github.com/opengl8080-javaee-samples/ejb

EJB とは

Enterprise Java Beans の略。

ビジネスロジックを簡潔に実装できるようにしてくれる仕組み・フレームワーク。

ビジネスロジックを持つクラスは POJO で作成でき、エンタープライズアプリケーションで必要になるトランザクション制御やリソース(JNDI, 他の EJB など)の取得、セキュリティ制御、 AOP などなどの機能はコンテナがほとんど自動で提供してくれる。
これにより、プログラマーはビジネスロジックの実装に集中できるようになる。

プロジェクト作成

コンテキストルートが ejb になるように、 NetBeans で Web プロジェクトを作成する。

Hello World

HelloEjb.java
package sample.javaee.ejb;

import javax.ejb.Stateless;

@Stateless
public class HelloEjb {

    public void hello() {
        System.out.println("Hello EJB!!");
    }
}
HelloEjbServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.HelloEjb;

@WebServlet("/hello")
public class HelloEjbServlet extends HttpServlet {

    @EJB
    private HelloEjb ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.ejb.hello();
    }
}

Web ブラウザで http://localhost:8080/ejb/hello にアクセスする。

GlassFishコンソール出力
情報:   Hello EJB!!
  • @Stateless でクラスをアノテートすると、そのクラスはセッションBeanとしてコンテナで管理されるようになる。
  • @EJB アノテーションを使うことで、 Servlet のフィールドなどにセッションBeanのインスタンスをインジェクションすることができる。

EJB の種類

EJB には、「セッションBean」と「メッセージ駆動Bean」の2種類が存在する。

セッションBean は基本となる EJB で、トランザクション制御などコンテナが提供する様々な機能を利用できる。
メッセージ駆動Bean は、 JMS でメッセージを受け取ったときの処理を実装することができる Bean。

ここでは、セッションBean の方の使い方をメモする。

エンティティBean

昔の EJB には「エンティティBean」という Bean が存在した。
これは永続化に関する仕組みを提供していたが、現在は JPA がその役割を担っていて、 EJB からは削除されている。

セッションBean の種類

セッションBean には、さらに3つの種類が存在する。

  • ステートレスセッションBean
  • ステートフルセッションBean
  • シングルトンBean

ステートレスセッションBean

定義

StatelessSessionBean.java
package sample.javaee.ejb;

import javax.ejb.Stateless;

@Stateless
public class StatelessSessionBean {

}
  • ステートレスセッションBean を定義するには、 @Stateless でクラスをアノテートする。
  • ステートレスセッションBean には、 public または protected の引数なしのコンストラクタが存在しなければならない。

ライフサイクル

StatelessSessionBean.java
package sample.javaee.ejb;

import javax.ejb.Stateless;

@Stateless
public class StatelessSessionBean {

    public void method() {
        System.out.println("StatelessSessionBean hash=" + this.hashCode());
    }
}
StatelessSessionBeanServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.StatelessSessionBean;

@WebServlet("/slsb")
public class StatelessSessionBeanServlet extends HttpServlet {

    @EJB
    private StatelessSessionBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.ejb.method();
    }
}

Groovy で以下のコードを実行(curl は cygwin でインストール)。

(1..10).each {
    Thread.start {
        "curl http://localhost:8080/ejb/slsb".execute()
    }
}
GlassFishコンソール出力
情報:   StatelessSessionBean hash=475599768
情報:   StatelessSessionBean hash=1797462320
情報:   StatelessSessionBean hash=1276339794
情報:   StatelessSessionBean hash=1276339794
情報:   StatelessSessionBean hash=1797462320
情報:   StatelessSessionBean hash=1276339794
情報:   StatelessSessionBean hash=475599768
情報:   StatelessSessionBean hash=1176949248
情報:   StatelessSessionBean hash=677901407
情報:   StatelessSessionBean hash=677901407
  • ステートレスセッションBean のインスタンスは、以下の要領でコンテナにより管理・運用される。
    • ステートレスセッションBean のインスタンスは、コンテナにより作成される。
    • インスタンスはコンテナが管理するメモリ内にプールされる(この状態を アクティブ状態 と呼ぶ)。
    • アプリケーションがインスタンスを要求すると、アクティブ状態のインスタンスの中から空いているインスタンスが取り出されて渡される。
    • 利用が終わったインスタンスは、再びプールに戻される。
  • アクティブ状態のインスタンスのうち、どのインスタンスが渡されるかはプログラムからは指定できない。
  • つまり、ステートレスセッションBean は状態を保持できない(インスタンス変数に値を保存することはできるが、どのインスタンスが渡されるか分からないので制御できない)。

インジェクションされている EJB はプロキシ

前述の実装と動作には、奇妙な点がある。

それは、 Servlet のインスタンスは1つだけなので、そのインスタンス変数である ejb も1つだけのはずなのに、実際はリクエストのたびに異なるインスタンスが利用されている、という点。

実は @EJB アノテーションでインジェクションしたインスタンスは、オリジナルのインスタンスがそのままインジェクションされているのではなく、プロキシがインジェクションされている。

StatelessSessionBean.java
package sample.javaee.ejb;

import javax.ejb.Stateless;

@Stateless
public class StatelessSessionBean {

    public void method(int servletHash, int servletEjbHash) {
        System.out.printf("servlet.hash=%d, servlet.ejb.hash=%d, slsb.hash=%d", servletHash, servletEjbHash, this.hashCode());
    }
}
StatelessSessionBeanServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.StatelessSessionBean;

@WebServlet("/slsb")
public class StatelessSessionBeanServlet extends HttpServlet {

    @EJB
    private StatelessSessionBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("ejb.class=" + this.ejb.getClass());
        this.ejb.method(this.hashCode(), this.ejb.hashCode());
    }
}

http://localhost:8080/ejb/slsb にアクセスする。

GlassFishコンソール出力
情報:   ejb.class=class sample.javaee.ejb.__EJB31_Generated__StatelessSessionBean__Intf____Bean__
情報:   ejb.class=class sample.javaee.ejb.__EJB31_Generated__StatelessSessionBean__Intf____Bean__
情報:   servlet.hash=558071930, servlet.ejb.hash=820309742, slsb.hash=1662278739
情報:   servlet.hash=558071930, servlet.ejb.hash=820309742, slsb.hash=28283218

Servlet にインジェクションされているインスタンスは StatelessSessionBean のインスタンスではなく、 EJB コンテナが動的に作成したプロキシクラスのインスタンスになっている。

プロキシのメソッドが実行されると、プロキシがプールされている本物のインスタンスを取得し、本来の処理を呼び出している。

試しに、同じ ejb インスタンスに対して、異なるスレッドからメソッドを呼び出してみる。

StatelessSessionBeanServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.StatelessSessionBean;

@WebServlet("/slsb")
public class StatelessSessionBeanServlet extends HttpServlet {

    @EJB
    private StatelessSessionBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        int servletHash = this.hashCode();
        int ejbHash = this.ejb.hashCode();

        for (int i=0; i<10; i++) {
            new Thread() {
                public void run() {
                    ejb.method(servletHash, ejbHash);
                }
            }.start();
        }
    }
}

http://localhost:8080/ejb/slsb に1回だけアクセスする。

GlassFishコンソール出力
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=218922525
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=391093029
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=340780847
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=802165962
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=340780847
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=802165962
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=1438623512
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=925820447
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=901322463
情報:   servlet.hash=466257751, servlet.ejb.hash=1592413802, slsb.hash=1483487844

Servlet にアクセスがあったときに EJB のインスタンスが取得されているのではなく、プロキシのメソッドが呼び出されたときに取得されている。

インスタンスがアクティブ化するときに処理を実行する

package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.ejb.Stateless;

@Stateless
public class StatelessSessionBean {

    @PostConstruct
    public void postConstruct() {
        System.out.println("StatelessSessionBean : post construct");
    }

    // 省略...
}

http://localhost:8080/ejb/slsb にアクセスする。

GlassFishコンソール出力
情報:   StatelessSessionBean : post construct
  • @PostConstruct でアノテートされたメソッドは、最初にアクティブ化されるときにコールバックされる。
  • @PostConstruct でアノテートするメソッドは以下の条件を満たしている必要がある。
    • インスタンスメソッドである。
    • 引数が存在しない。
    • チェック例外をスローしない。
    • 戻り値は void 型。
  • 可視性は private でも良い。

インスタンスが破棄されるときに処理を実行する

package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Stateless;

@Stateless
public class StatelessSessionBean {

    // 省略...

    @PreDestroy
    public void preDestroy() {
        System.out.println("StatelessSessionBean : pre destroy");
    }
}

GlassFish をシャットダウンする。

GlassFishコンソール出力
情報:   StatelessSessionBean : pre destroy
  • @PreDestroy でアノテートされたメソッドは、アクティブ状態のインスタンスが破棄されるときにコールバックされる。
  • アノテートできるメソッドの条件は @PostConstruct のときと同じ。

ステートフルセッションBean

定義

package sample.javaee.ejb;

import javax.ejb.Stateful;

@Stateful
public class StatefulSessionBean {

}
  • ステートフルセッションBean を定義するには、 @Stateful でクラスをアノテートする。
  • ステートフルセッションBean には、 public または protected で引数無しのコンストラクタが存在しなければならない。

ライフサイクル

StatefullSessionBean.java
package sample.javaee.ejb;

import java.io.Serializable;
import javax.ejb.Stateful;

@Stateful
public class StatefulSessionBean implements Serializable{

    private int count;

    public void countUp(int clientHash) {
        System.out.printf("clientHash=%d, count=%d", clientHash, ++this.count);
    }
}
EjbClient.java
package sample.javaee.ejb;

import javax.ejb.EJB;
import javax.ejb.Stateless;

@Stateless
public class EjbClient {

    @EJB
    private StatefulSessionBean sfsb;

    public void method() {
        this.sfsb.countUp(this.hashCode());
    }
}
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.EjbClient;

@WebServlet("/sfsb")
public class StatefulSessionBeanServlet extends HttpServlet {

    @EJB
    private EjbClient client;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.client.method();
    }
}

Groovy で以下のコードを実行(curl は cygwin でインストール)。

(1..10).each {
    Thread.start {
        "curl http://localhost:8080/ejb/sfsb".execute()
    }
}
GlassFishコンソール出力
情報:   clientHash=491437360, count=1
情報:   clientHash=491437360, count=2
情報:   clientHash=491437360, count=3
情報:   clientHash=1061708930, count=1
情報:   clientHash=1061708930, count=2
情報:   clientHash=491437360, count=4
情報:   clientHash=1061708930, count=3
情報:   clientHash=1061708930, count=4
情報:   clientHash=491437360, count=5
情報:   clientHash=491437360, count=6
  • ステートフルセッションBean は、EJB クライアントごとに 常に同じインスタンスが割り当てられる。
    • 厳密には、パッシブ状態からアクティブ化されるとインスタンスは変わる可能性がある。
  • EJB クライアントとは、 EJB を利用する他のコンポーネントのこと(他のセッションBean や Servlet など)。
    • HttpSession ごとではない(勘違いしてた...)。
  • ステートフルセッションBean のインスタンスは、以下の要領でコンテナに管理・運用される。
    • ステートフルセッションBean のインスタンスは、 EJB コンテナによって生成される。
    • 生成されたインスタンスは EJB クライアントごとに割り当てられ、メモリ上に保存される。この状態をアクティブ状態と呼ぶ。
    • インスタンスがしばらくの間使用されないでいると、メモリ消費を抑えるためインスタンスはメモリ以外のどこかに退避される。この状態をパッシブ状態と呼ぶ。
    • パッシブ状態のインスタンスが再び EJB クライアントに呼び出されると、アクティブ状態に戻される。
    • パッシブ状態でさらに使用されない場合はパッシブ状態の情報も破棄される。

アクティブ化・インスタンス破棄のときに処理を実行する

StatefulSessionBean.java
package sample.javaee.ejb;

+ import javax.annotation.PostConstruct;
+ import javax.annotation.PreDestroy;
+ import javax.ejb.Remove;
import javax.ejb.Stateful;

@Stateful
public class StatefulSessionBean {

    private int count;

    public void countUp(int clientHash) {
        System.out.printf("clientHash=%d, count=%d", clientHash, ++this.count);
    }

+   @PostConstruct
+   public void postConstruct() {
+       System.out.println("StatefulSessionBean : post construct");
+   }
+   
+   @PreDestroy
+   public void preDestroy() {
+       System.out.println("StatefulSessionBean : pre destroy");
+   }
+   
+   @Remove
+   public void remove() {
+       System.out.println("remove SatefulSessionBean");
+   }
}
EjbClient.java
package sample.javaee.ejb;

import javax.ejb.EJB;
import javax.ejb.Stateless;

@Stateless
public class EjbClient {

    @EJB
    private StatefulSessionBean sfsb;

    public void method() {
        this.sfsb.countUp(this.hashCode());
    }

+   public void remove() {
+       this.sfsb.remove();
+   }
}
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.EjbClient;

- @WebServlet("/sfsb")
+ @WebServlet("/sfsb/*")
public class StatefulSessionBeanServlet extends HttpServlet {

    @EJB
    private EjbClient client;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
+       String path = req.getRequestURI();
+       
+       if (path.endsWith("/sfsb")) {
            this.client.method();
+       } else if (path.endsWith("/sfsb/remove")) {
+           this.client.remove();
+       }
    }
}

http://localhost:8080/ejb/sfsb にアクセス。

GlassFishコンソール出力
情報:   StatefulSessionBean : post construct
情報:   clientHash=1139797100, count=1

http://localhost:8080/ejb/sfsb/remove にアクセス。

GlassFishコンソール出力
情報:   remove SatefulSessionBean
情報:   StatefulSessionBean : pre destroy
  • ステートレスセッションBean と同じで、 @PostConstruct@PreDestroy でアノテートしたメソッドは、インスタンスがアクティブ状態になるときと、破棄されるときにコールバックされる。
  • @Remove でアノテートされたメソッドを実行すると、インスタンスを破棄できる。
    • ステートレスセッションBean は GlassFish をシャットダウンしたときに @PreDestroy がコールバックされたが、ステートフルセッションBean の場合はコールバックされなかった。
    • 代わりに、シャットダウン時は後述の @PrePassivate でアノテートされたメソッドがコールバックされた。

パッシブ化・アクティブ化のときに処理を実行する

package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
+ import javax.ejb.PostActivate;
+ import javax.ejb.PrePassivate;
import javax.ejb.Remove;
import javax.ejb.Stateful;

@Stateful
public class StatefulSessionBean {

    private int count;

    public void countUp(int clientHash) {
        System.out.printf("clientHash=%d, count=%d", clientHash, ++this.count);
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("StatefulSessionBean : post construct");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("StatefulSessionBean : pre destroy");
    }

    @Remove
    public void remove() {
        System.out.println("remove SatefulSessionBean");
    }

+   @PrePassivate
+   public void prePassivate() {
+       System.out.println("pre passivate");
+   }
+   
+   @PostActivate
+   public void postActivate() {
+       System.out.println("post activate");
+   }
}
  • @PrePassiavte でアノテートされたメソッドは、インスタンスがパッシブ化する直前にコールバックされる。
  • @PostActivate でアノテートされたメソッドは、インスタンスがパッシブ状態からアクティブ状態に切り換わるときにコールバックされる。

パッシブ化できるインスタンスフィールド

ステートフルセッションBean をパッシブ状態にするとき、 EJB コンテナはインスタンスをメモリ以外の場所に退避する。
(ただし、具体的にどこにどうやって退避するかはコンテナの実装に任されている)

このとき、ステートフルセッションBean のインスタンスフィールドは、以下のいずれかの型(値)である必要がある。

  • プリミティブ型
  • java.io.Serializable を実装したクラス
  • javax.ejb.SessionContext
  • javax.jta.UserTransaction
  • javax.naming.Context
  • javax.persistence.EntityManager
  • javax.persistence.EntityManagerFactory
  • javax.sql.DataSource
  • 他の EJB
  • null 値

これら以外の型(例えば、 Serializable を実装していないクラス)が存在する場合は、パッシブ化するときに警告メッセージが表示される。

パッシブ化できないフィールドが存在する場合に表示される警告メッセージ
警告:   [NRU-sample.javaee.ejb.servlet.test.Sfsb]: passivateEJB(), Exception caught ->
警告:   Error during passivation of [Sfsb <==> Sfsb; id: 90c01700a81f-ffffffffaadb2422-0]
警告:   sfsb passivation error. Key: [90c01700a81f-ffffffffaadb2422-0]

この警告が表示されたステートフルセッションBean のインスタンスをアクティブ化させようとすると、 NoSuchEJBException がスローされる。

シリアライズできない型をフィールドに持つ場合は、 @PrePassivate のときにフィールドに null をセットするようにして、 @PostActivate のときに改めてインスタンスを作成するように実装すればいい。

※パッシブ化時にエラーが発生する様子を確認する手順については、 GitHub に上げたサンプルの README.md を参照。

シングルトンBean

定義

SingletonBean.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;

@Singleton
public class SingletonBean {

}
  • シングルトンBean を定義するには、 @Singleton でクラスをアノテートする。
  • シングルトンBean には、 public または protected の引数なしのコンストラクタが存在しなければならない。

ライフサイクル

  • 名前の通り、インスタンスはコンテナにより1つだけ生成され使いまわされることが保証される。
  • デフォルトでは、初めてアクセスがあったときにインスタンスが生成され、以後そのインスタンスが使いまわされる。

インスタンス生成時、破棄時に処理を実行する

SingletonEjb.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.Singleton;

@Singleton
public class SingletonBean {

    public void method() {
        System.out.println("SingletonBean : hash=" + this.hashCode());
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("SingletonBean : post construct");
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println("SingletonBean : pre destroy");
    }
}
SingletonBeanServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.SingletonBean;

@WebServlet("/singleton")
public class SingletonBeanServlet extends HttpServlet {

    @EJB
    private SingletonBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.ejb.method();
    }
}

http://localhost:8080/ejb/singleton にアクセスする。

GlassFishコンソール出力
情報:   SingletonBean : post construct
情報:   SingletonBean : hash=206721482

GlassFish を停止する。

GlassFishコンソール出力
情報:   SingletonBean : pre destroy
  • 他のセッションBean と同様に、 @PostConstruct@PreDestroy でコールバックを定義できる。

メソッドの同時実行制御

デフォルトは、コンテナが自動で同時実行の制御を行う

ConcurrencyControlSingletonBean.java
package sample.javaee.ejb;

import javax.ejb.Singleton;

@Singleton
public class ConcurrencyControlSingletonBean {

    public void defaultControl() {
        this.process("defaultControl");
    }

    private void process(String method) {
        this.log(method, "before");
        this.sleep();
        this.log(method, "after");
    }

    private void log(String method, String tag) {
        System.out.printf("%s() %s [Thread : %s]", method, tag, Thread.currentThread().getName());
    }

    private void sleep() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
ConcurrencyControlServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.ConcurrencyControlSingletonBean;

@WebServlet("/singleton/concurrency/*")
public class ConcurrencyControlServlet extends HttpServlet {

    @EJB
    private ConcurrencyControlSingletonBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.ejb.defaultControl();
    }
}

Groovy で以下のコードを実行。

(1..10).each {
    Thread.start {
        "curl http://localhost:8080/ejb/singleton/concurrency".execute()
    }
}
GlassFishコンソール出力
情報:   defaultControl() before [Thread : http-listener-1(4)]
情報:   defaultControl() after [Thread : http-listener-1(4)]
情報:   defaultControl() before [Thread : http-listener-1(2)]
情報:   defaultControl() after [Thread : http-listener-1(2)]
情報:   defaultControl() before [Thread : http-listener-1(5)]
情報:   defaultControl() after [Thread : http-listener-1(5)]
情報:   defaultControl() before [Thread : http-listener-1(3)]
情報:   defaultControl() after [Thread : http-listener-1(3)]
情報:   defaultControl() before [Thread : http-listener-1(1)]
情報:   defaultControl() after [Thread : http-listener-1(1)]
情報:   defaultControl() before [Thread : http-listener-1(4)]
情報:   defaultControl() after [Thread : http-listener-1(4)]
情報:   defaultControl() before [Thread : http-listener-1(2)]
情報:   defaultControl() after [Thread : http-listener-1(2)]
情報:   defaultControl() before [Thread : http-listener-1(5)]
情報:   defaultControl() after [Thread : http-listener-1(5)]
情報:   defaultControl() before [Thread : http-listener-1(3)]
情報:   defaultControl() after [Thread : http-listener-1(3)]
情報:   defaultControl() before [Thread : http-listener-1(1)]
情報:   defaultControl() after [Thread : http-listener-1(1)]
  • 複数のスレッドが defaultControl() メソッドを実行しているが、1つのスレッドがメソッドを実行しているときに、他のスレッドが割り込むようなことは発生しない。
  • シングルトンBean のメソッドは、コンテナによって自動で同期実行の制御が行われる。
    • synchronized 修飾子が自動で付与された状態になる。

特定のメソッドだけコンテナの同時実行制御を外す

ConcurrencyControlSingletonBean.java
package sample.javaee.ejb;

+ import javax.ejb.Lock;
+ import javax.ejb.LockType;
import javax.ejb.Singleton;

@Singleton
public class ConcurrencyControlSingletonBean {

    public void defaultControl() {
        this.process("defaultControl");
    }

+   @Lock(LockType.READ)
+   public void readControl() {
+       this.process("readControl");
+   }

    private void process(String method) {
        this.log(method, "before");
        this.sleep();
        this.log(method, "after");
    }

    private void log(String method, String tag) {
        System.out.printf("%s() %s [Thread : %s]", method, tag, Thread.currentThread().getName());
    }

    private void sleep() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
ConcurrencyControlServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.ConcurrencyControlSingletonBean;

@WebServlet("/singleton/concurrency/*")
public class ConcurrencyControlServlet extends HttpServlet {

    @EJB
    private ConcurrencyControlSingletonBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
+       String path = req.getRequestURI();
+        
+       if (path.endsWith("/read")) {
+           this.ejb.readControl();
+       } else {
            this.ejb.defaultControl();
+       }
    }
}

Groovy で以下のコードを実行。

(1..10).each {
    Thread.start {
        "curl http://localhost:8080/ejb/singleton/concurrency/read".execute()
    }
}
GlassFishコンソール出力
情報:   readControl() before [Thread : http-listener-1(2)]
情報:   readControl() before [Thread : http-listener-1(3)]
情報:   readControl() before [Thread : http-listener-1(4)]
情報:   readControl() before [Thread : http-listener-1(5)]
情報:   readControl() before [Thread : http-listener-1(1)]
情報:   readControl() after [Thread : http-listener-1(3)]
情報:   readControl() after [Thread : http-listener-1(4)]
情報:   readControl() after [Thread : http-listener-1(2)]
情報:   readControl() before [Thread : http-listener-1(2)]
情報:   readControl() before [Thread : http-listener-1(3)]
情報:   readControl() before [Thread : http-listener-1(4)]
情報:   readControl() after [Thread : http-listener-1(5)]
情報:   readControl() after [Thread : http-listener-1(1)]
情報:   readControl() before [Thread : http-listener-1(5)]
情報:   readControl() before [Thread : http-listener-1(1)]
情報:   readControl() after [Thread : http-listener-1(2)]
情報:   readControl() after [Thread : http-listener-1(3)]
情報:   readControl() after [Thread : http-listener-1(4)]
情報:   readControl() after [Thread : http-listener-1(5)]
情報:   readControl() after [Thread : http-listener-1(1)]
  • @Lock でメソッドをアノテートし LockType.READ を値として渡すと、そのメソッドは複数のスレッドから同時に実行できるようになる。
  • LockType.WRITE を指定すると、同時実行制御が行われるようになる(デフォルトはこちら)。
  • @Lock はクラスにもアノテートすることができる。その場合は、全てのメソッドに設定が反映される。
    • その状態でさらにメソッドを @Lock でアノテートした場合、そのメソッドだけ設定が上書きされる。

タイムアウト時間を設定する

ConcurrencyControlSingletonBean.java
package sample.javaee.ejb;

+ import javax.ejb.AccessTimeout;
import javax.ejb.Lock;
import javax.ejb.LockType;
import javax.ejb.Singleton;

@Singleton
public class ConcurrencyControlSingletonBean {

    public void defaultControl() {
        this.process("defaultControl");
    }

    @Lock(LockType.READ)
    public void readControl() {
        this.process("readControl");
    }

+   @AccessTimeout(400)
+   public void timeout() {
+       this.process("timeout");
+   }

    private void process(String method) {
        this.log(method, "before");
        this.sleep();
        this.log(method, "after");
    }

    private void log(String method, String tag) {
        System.out.printf("%s() %s [Thread : %s]", method, tag, Thread.currentThread().getName());
    }

    private void sleep() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
ConcurrencyControlServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.ConcurrencyControlSingletonBean;

@WebServlet("/singleton/concurrency/*")
public class ConcurrencyControlServlet extends HttpServlet {

    @EJB
    private ConcurrencyControlSingletonBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String path = req.getRequestURI();

        if (path.endsWith("/read")) {
            this.ejb.readControl();
+       } else if (path.endsWith("/timeout")) {
+           this.ejb.timeout();
        } else {
            this.ejb.defaultControl();
        }
    }
}

Web ブラウザで F5 連打などを使って http://localhost:8080/ejb/singleton/concurrency/timeout に同時に複数回アクセスする。

GlassFishコンソール出力
javax.ejb.ConcurrentAccessTimeoutException: Couldn't acquire a lock within 400 MILLISECONDS
    at com.sun.ejb.containers.CMCSingletonContainer._getContext(CMCSingletonContainer.java:151)
(以下略)
  • @AccessTimeout でメソッドをアノテートすると、メソッドを待機できる時間を指定できる。
  • value に待機時間を指定する(ミリ秒)。
    • value の単位は unit で指定することができる。
    • (例)@AccessTimeout(value=1, unit=TimeUnit.MINUTES)
  • value=0 とした場合、待機時間無しで例外がスローされる。つまり、メソッドの同時実行は一切許可されない状態になる。
  • value=-1 とした場合、前のスレッドが処理を完了するまで待機し続ける。

同時実行制御をコンテナに管理させないようにする

ManualConcurrencyControlSingletonBean.java
package sample.javaee.ejb;

import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Singleton;

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
public class ManualConcurrencyControlSingletonBean {

    synchronized public void method() {
        // ...
    }
}
  • 同時実行の制御をコンテナに任せたくない場合は、クラスを @ConcurrencyManagement でアノテートし、値に ConcurrencyManagementType.BEAN を指定する。
  • その場合、必要であれば同時実行の制御は synchronized を使い自力で実装する(そもそも制御が必要ないのであれば、何もしなくてもいい)。

サーバー起動時にインスタンスを生成させる

StartupSingletonBean.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;

@Singleton
@Startup
public class StartupSingletonBean {

    @PostConstruct
    public void postConstruct() {
        System.out.println("StartupSingletonBean : post construct");
    }
}

GlassFish を起動する。

GlassFishコンソール出力
情報:   StartupSingletonBean : post construct
  • @Startup でシングルトンBean をアノテートすると、サーバー起動時にインスタンスを生成させることができる。
  • 初期化処理に重い処理が存在する場合は、この方法が活用できる。

インスタンスの作成順序を指定する

DependedSingletonBean1.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.ejb.Singleton;

@Singleton
public class DependedSingletonBean1 {

    @PostConstruct
    public void postConstruct() {
        System.out.println("DependedSingletonBean1 : post construct");
    }
}
DependedSingletonBean2.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.ejb.Singleton;

@Singleton
public class DependedSingletonBean2 {

    @PostConstruct
    public void postConstruct() {
        System.out.println("DependedSingletonBean2 : post construct");
    }
}
DependingSingletonBean.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.ejb.DependsOn;
import javax.ejb.Singleton;
import javax.ejb.Startup;

@DependsOn({"DependedSingletonBean1", "DependedSingletonBean2"})
@Singleton
@Startup
public class DependingSingletonBean {

    @PostConstruct
    public void postConstruct() {
        System.out.println("DependingSingletonBean : post construct");
    }
}

GlassFish を起動する。

GlassFishコンソール出力
情報:   DependedSingletonBean1 : post construct
情報:   DependedSingletonBean2 : post construct
情報:   DependingSingletonBean : post construct
  • @DependsOn() でアノテートすることで、そのシングルトンBean が生成される前に生成されなければならない他のシングルトンBean を指定できる。
  • 他のシングルトンBean の指定は、 value に Bean の名前を配列で渡す。

Bean の名前について

デフォルトでは、クラス名がそのまま Bean の名前になる。
任意の名前を指定したい場合は、 @Singletonname 属性で指定する。

DependedSingletonBean2.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.ejb.Singleton;

- @Singleton
+ @Singleton(name="singleton2")
public class DependedSingletonBean2 {

    @PostConstruct
    public void postConstruct() {
        System.out.println("DependedSingletonBean2 : post construct");
    }
}
DependingSingletonBean.java
package sample.javaee.ejb;

import javax.annotation.PostConstruct;
import javax.ejb.DependsOn;
import javax.ejb.Singleton;
import javax.ejb.Startup;

- @DependsOn({"DependedSingletonBean1", "DependedSingletonBean2"})
+ @DependsOn({"DependedSingletonBean1", "singleton2"})
@Singleton
@Startup
public class DependingSingletonBean {

    @PostConstruct
    public void postConstruct() {
        System.out.println("DependingSingletonBean : post construct");
    }
}

メソッドを非同期処理にする

AsyncMethodBean.java
package sample.javaee.ejb;

import javax.ejb.Asynchronous;
import javax.ejb.Stateless;

@Stateless
public class AsyncMethodBean {

    @Asynchronous
    public void asyncMethod() {
        System.out.println("asyncMethod() thread=" + Thread.currentThread().getName());
    }
}
AsyncMethodServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.AsyncMethodBean;

@WebServlet("/async-method")
public class AsyncMethodServlet extends HttpServlet {

    @EJB
    private AsyncMethodBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("before thread=" + Thread.currentThread().getName());
        this.ejb.asyncMethod();
        System.out.println("after thread=" + Thread.currentThread().getName());
    }
}

http://localhost:8080/ejb/async-method にアクセスする。

GlassFishコンソール出力
情報:   before thread=http-listener-1(1)
情報:   after thread=http-listener-1(1)
情報:   asyncMethod() thread=__ejb-thread-pool12
  • @Asynchronous でセッションBean のメソッドをアノテートすると、そのメソッドは非同期で実行されるようになる。

戻り値を受け取れるようにする

AsyncMethodBean.java
package sample.javaee.ejb;

import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;

@Stateless
public class AsyncMethodBean {

    @Asynchronous
    public Future<String> asyncMethod() {
        System.out.println("asyncMethod() thread=" + Thread.currentThread().getName());
        return new AsyncResult<>("asyncMethod result.");
    }
}
AsyncMethodServlet.java
package sample.javaee.ejb.servlet;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.AsyncMethodBean;

@WebServlet("/async-method")
public class AsyncMethodServlet extends HttpServlet {

    @EJB
    private AsyncMethodBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("before thread=" + Thread.currentThread().getName());
        Future<String> result = this.ejb.asyncMethod();
        System.out.println("after thread=" + Thread.currentThread().getName());

        try {
            System.out.println("result=" + result.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

http://locahlost:8080/ejb/async-method にアクセスする。

GlassFishコンソール出力
情報:   before thread=http-listener-1(4)
情報:   after thread=http-listener-1(4)
情報:   asyncMethod() thread=__ejb-thread-pool5
情報:   result=asyncMethod result.
  • 非同期メソッドから戻り値を受け取れるようにするには、 Future を返すように実装する。
  • Future はインターフェースなので、具体的な実装には AsyncResult を使用する。

EJB がスローした例外について

ExceptionEjb.java
package sample.javaee.ejb;

import javax.ejb.Stateless;

@Stateless
public class ExceptionEjb {

    public void throwException() throws Exception {
        throw new Exception("test");
    }

    public void throwRuntimeException() {
        throw new RuntimeException("test");
    }
}
ExceptionServlet.java
package sample.javaee.ejb.servlet;

import java.io.IOException;
import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.ExceptionEjb;

@WebServlet("/exception/*")
public class ExceptionServlet extends HttpServlet {

    @EJB
    private ExceptionEjb ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String path = req.getRequestURI();

        try {
            if (path.endsWith("/runtime")) {
                this.ejb.throwRuntimeException();
            } else {
                this.ejb.throwException();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Exception を継承したクラスはアプリケーション例外としてそのままクライアントに送られる

http://localhost:8080/ejb/exception にアクセスする。

GlassFishコンソール出力
重大:   java.lang.Exception: test
    at sample.javaee.ejb.ExceptionEjb.throwException(ExceptionEjb.java:9)
(以下略)

Exception を継承した例外を EJB がスローした場合、その例外はそのままクライアントに送られる。

RuntimeException を継承したクラスは EJBException でラップされてクライアントに送られる

http://localhost:8080/ejb/exception/runtime にアクセスする。

GlassFishコンソール出力
警告:   A system exception occurred during an invocation on EJB ExceptionEjb, method: public void sample.javaee.ejb.ExceptionEjb.throwRuntimeException()
警告:   javax.ejb.EJBException
    at com.sun.ejb.containers.EJBContainerTransactionManager.processSystemException(EJBContainerTransactionManager.java:748)
(中略)
Caused by: java.lang.RuntimeException: test
    at sample.javaee.ejb.ExceptionEjb.throwRuntimeException(ExceptionEjb.java:13)
(後略)

RuntimeException を継承した例外を EJB がスローした場合、その例外は EJBException にラップされてクライアントに送られる。

インターセプタ

セッションBean 内にインターセプタ用のメソッドを定義する

BasicInterceptorBean.java
package sample.javaee.ejb;

import javax.ejb.Stateless;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

@Stateless
public class BasicInterceptorBean {

    public void method() {
        System.out.println("method()");
    }

    @AroundInvoke
    private Object intercept(InvocationContext context) throws Exception {
        System.out.println("before");
        Object result = context.proceed();
        System.out.println("after");
        return result;
    }
}
BasicInterceptorServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.BasicInterceptorBean;

@WebServlet("/basic-interceptor")
public class BasicInterceptorServlet extends HttpServlet {

    @EJB
    private BasicInterceptorBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.ejb.method();
    }
}

http://localhost:8080/ejb/basic-interceptor にアクセスする。

GlassFishコンソール出力
情報:   before
情報:   method()
情報:   after
  • @AroundInvoke でメソッドをアノテートすると、他のメソッドが実行されたときに呼び出される。
  • @AroundInvoke でアノテートできるメソッドには、以下の条件がある。
    • Object 型を返すこと。
    • InvocationContext を引数に受け取る。
    • チェック例外をスローする。
  • InvocationContext.proceed() で、インターセプトしているメソッドを実行できる。

インターセプタを別クラスで定義する

MyInterceptor.java
package sample.javaee.ejb.interceptor;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

public class MyInterceptor {

    @AroundInvoke
    public Object intercept(InvocationContext context) throws Exception {
        System.out.println("before");
        Object result = context.proceed();
        System.out.println("after");
        return result;
    }
}
InterceptorBean.java
package sample.javaee.ejb;

import javax.ejb.Stateless;
import javax.interceptor.Interceptors;
import sample.javaee.ejb.interceptor.MyInterceptor;

@Stateless
public class InterceptorBean {

    @Interceptors(MyInterceptor.class)
    public void method() {
        System.out.println("InterceptorBean.method()");
    }
}
InterceptorServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.InterceptorBean;

@WebServlet("/interceptor")
public class InterceptorServlet extends HttpServlet {

    @EJB
    private InterceptorBean ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.ejb.method();
    }
}

http://localhost:8080/ejb/interceptor にアクセスする。

GlassFishコンソール出力
情報:   before
情報:   InterceptorBean.method()
情報:   after
  • セッションBean 内に定義したのと同じ要領で、 @AroundInvoke でアノテートしたクラスを作成する。
  • @Interceptors アノテーションで適用するインターセプタのクラスを指定する。
    • @Interceptors アノテーションは、クラスにも指定することができる。
    • その場合は、全てのメソッドにインターセプタを適用できる。
    • クラスを @Interceptors でアノテートしたものの、あるメソッドだけインターセプタを適用したくない場合は、 @ExcludeClassInterceptors でメソッドをアノテートする。

全ての EJB にインターセプタを適用させる

@Interceptors を使った方法だと、「全ての EJB にインターセプタを適用したい」といったときに、わざわざ全てのクラスをアノテートしなくてはいけなくなる。

こういう場合は、デフォルトインターセプタというものを使用する。

DefaultInterceptor.java
package sample.javaee.ejb.interceptor;

import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;

public class DefaultInterceptor {

    @AroundInvoke
    public Object intercept(InvocationContext context) throws Exception {
        System.out.println("DefaultInterceptor before");
        Object result = context.proceed();
        System.out.println("DefaultInterceptor after");
        return result;
    }
}
ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        version="3.2"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/ejb-jar_3_2.xsd">

  <interceptors>
    <interceptor>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor</interceptor-class>
    </interceptor>
  </interceptors>

  <assembly-descriptor>
    <interceptor-binding>
      <ejb-name>*</ejb-name>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor</interceptor-class>
    </interceptor-binding>
  </assembly-descriptor>
</ejb-jar>

http://localhost:8080/ejb/interceptor にアクセスする。

GlassFishコンソール出力
情報:   DefaultInterceptor before
情報:   before
情報:   InterceptorBean.method()
情報:   after
情報:   DefaultInterceptor after
  • ejb-jar.xml で定義することで、アノテーション無しでインターセプタを定義することができる。
    • ejb-jar.xml は、 WEB-INF の直下に配置する。
  • <ejb-name> タグで、インターセプタを適用する EJB を指定する。
    • ワイルドカード(*)を指定できる。
    • 例の場合は * だけなので、全ての EJB が対象になる。
  • <interceptor-binding> タグは複数設定可能なので、下記の用に <ejb-name> ごとに異なるインターセプタを適用させることもできる。
ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        version="3.2"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/ejb-jar_3_2.xsd">

  <interceptors>
    <interceptor>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor</interceptor-class>
    </interceptor>
    <interceptor>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor2</interceptor-class>
    </interceptor>
    <interceptor>
      <interceptor-class>sample.javaee.ejb.interceptor.HelloInterceptor</interceptor-class>
    </interceptor>
  </interceptors>

  <assembly-descriptor>
    <interceptor-binding>
      <ejb-name>*</ejb-name>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor</interceptor-class>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor2</interceptor-class>
    </interceptor-binding>

    <interceptor-binding>
      <ejb-name>HelloEjb</ejb-name>
      <interceptor-class>sample.javaee.ejb.interceptor.HelloInterceptor</interceptor-class>
    </interceptor-binding>
  </assembly-descriptor>
</ejb-jar>

複数のデフォルトインターセプタを適用する

ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        version="3.2"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/ejb-jar_3_2.xsd">

  <interceptors>
    <interceptor>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor</interceptor-class>
    </interceptor>
+   <interceptor>
+     <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor2</interceptor-class>
+   </interceptor>
  </interceptors>

  <assembly-descriptor>
    <interceptor-binding>
      <ejb-name>*</ejb-name>
      <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor</interceptor-class>
+     <interceptor-class>sample.javaee.ejb.interceptor.DefaultInterceptor2</interceptor-class>
    </interceptor-binding>
  </assembly-descriptor>
</ejb-jar>
GlassFishコンソール出力
情報:   DefaultInterceptor before
情報:   DefaultInterceptor2 before
情報:   before
情報:   InterceptorBean.method()
情報:   after
情報:   DefaultInterceptor2 after
情報:   DefaultInterceptor after
  • <interceptor-class> タグを並べることで、複数のインターセプタを適用することができる。
  • インターセプタの適用順序は、並べた順序に一致する。

デフォルトインターセプタを適用させないようにする

ExcludeDefaultInterceptorEjb.java
package sample.javaee.ejb;

import javax.ejb.Stateless;
import javax.interceptor.ExcludeDefaultInterceptors;

@Stateless
public class ExcludeDefaultInterceptorEjb {

    @ExcludeDefaultInterceptors
    public void method() {
        System.out.println("ExcludeDefaultInterceptorEjb.method()");
    }
}
ExcludeDefaultInterceptorServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.ExcludeDefaultInterceptorEjb;

@WebServlet("/interceptor/exclude")
public class ExcludeDefaultInterceptorServlet extends HttpServlet {

    @EJB
    private ExcludeDefaultInterceptorEjb ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        this.ejb.method();
    }
}

http://localhost:8080/ejb/interceptor/exclude にアクセスする。

GlassFishコンソール出力
情報:   ExcludeDefaultInterceptorEjb.method()
  • @ExcludeDefaultInterceptors でメソッドをアノテートすると、デフォルトインターセプタが適用されなくなる。
  • クラスをアノテートすれば、全てのメソッドがデフォルトインターセプタの対象外になる。

EJBのメソッドを定期的に実行する

アノテーションを使って宣言的に定義する

ScheduledEjb.java
package sample.javaee.ejb;

import java.text.SimpleDateFormat;
import java.util.Date;
import javax.ejb.Schedule;
import javax.ejb.Stateless;

@Stateless
public class ScheduledEjb {

    @Schedule(second = "*/10", minute = "*", hour = "*", persistent = false)
    public void process() {
        System.out.printf("%s : ScheduledEjb.process()", this.now());
    }

    private String now() {
        SimpleDateFormat format = new SimpleDateFormat("mm:ss");
        return format.format(new Date());
    }
}

GlassFish を起動する。

GlassFishコンソール出力
情報:   29:40 : ScheduledEjb.process()
情報:   29:50 : ScheduledEjb.process()
情報:   30:00 : ScheduledEjb.process()
情報:   30:10 : ScheduledEjb.process()
情報:   30:20 : ScheduledEjb.process()
情報:   30:30 : ScheduledEjb.process()
  • 10 秒ごとに process() メソッドが実行されている。
  • @Schedule でセッションBean のメソッドをアノテートすることで、メソッドを定期的に実行できるようになる。

※persistent=false について
書籍やネット上でサンプルコードを見ると、 persistent はデフォルトの true が使用されている。
しかし、自分が試した限りでは、 GlassFish 4.1 では persistent=false にしないと動作しなかった。

試しに Wildfly 8.2.0 で試してみたら persistent=true でも動いたので、何か設定が漏れていたのか、あるいは GlassFish のバグか。。。

ちなみに、 persistent の設定はスケジュールの進行状況をサーバー停止時も記録しておくかどうかを指定するためのもので、 true が指定されている場合は記録される(デフォルトはこの設定)。
進行状況が記録されている場合、サーバーを再起動したときにもしサーバー停止中にスケジュールで指定された時刻を過ぎていると、起動時に即座に処理が実行されるようになっている。

参考

スケジュールの指定方法

基本

属性ごとに指定できる基本的な値は以下。

属性 意味 指定可能な値 デフォルト値
second [0 - 59] 0
minute [0 - 59] 0
hour [0 - 23] 0
dayOfMonth 日・曜日 [1 - 31], [1st 2nd 3rd 4th 5th - Last], [Sun Mon Tue Wed Thu Fri Sat], [-n(月末日から n 日前)]
month [1 - 12], [Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec] *
dayOfWeek 曜日 [0 - 7], [Sun Mon Tue Wed Thu Fri Sat] *
year [yyyy] *
timezone タイムゾーン

さらに以下の要領で複数の値を指定できたりする。

複数の値を指定する

@Schedule(second="1,2,3,4")
  • カンマ , 区切りで複数の値を指定することができる。

ワイルドカードで指定する

@Schedule(second="*")
  • ワイルドカード * で、該当する全ての値にマッチするように指定できる。

範囲で指定する

@Schedule(second="1-20")
  • ハイフン - 区切りで、範囲指定ができる。

間隔を指定する

@Schedule(second="*/10")
  • スラッシュ / で区切ると、スラッシュの右側で指定した時間で区切った間隔でスケジュールを指定できる。
  • 上記例の場合、 10 秒毎にスケジューリングされる。
  • スラッシュの左側に * 以外を指定すると、開始時間として扱われる。
    • second="30/10" とした場合、 30 秒を開始時間として、10 秒間隔でスケジューリングされる(30, 40, 50 秒で処理が実行される)。

スケジュールの設定例

設定 動作タイミング
dayOfWeek="Wed" 毎週水曜日の午前0時
dayOfWeek="Mon-Fri", hour="7" 月~金曜日の午前7時
second="*/10", minute="*", hour="*" 10秒おき
dayOfMonth="Last Mon" 毎月の最終月曜日の午前0時
dayOfMonth="-5" 毎月の最終日5日前の午前0時
dayOfMonth="2nd Mon" 毎月の第二月曜日の午前0時
minute="*/30", hour="*/6" 6時間おきに30分ごと
minute="30/5", hour="*" 毎時30分から5分おき

プログラムで動的にタイマーを登録する

TimerEjb.java
package sample.javaee.ejb;

import java.text.SimpleDateFormat;
import java.util.Date;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.ScheduleExpression;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.Timeout;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;

@Singleton
@Startup
public class TimerEjb {

    @Resource
    private TimerService timer;

    @PostConstruct
    public void registerTimer() {
        ScheduleExpression schedule = new ScheduleExpression().hour("*").minute("*").second("*/10");
        this.timer.createCalendarTimer(schedule, new TimerConfig(null, false));
    }

    @Timeout
    private void timeout() {
        System.out.printf("%s : TimerEjb.timeout()", this.now());
    }

    private String now() {
        SimpleDateFormat format = new SimpleDateFormat("mm:ss");
        return format.format(new Date());
    }
}

GlassFish を起動する。

GlassFishコンソール出力
情報:   09:00 : TimerEjb.timeout()
情報:   09:10 : TimerEjb.timeout()
情報:   09:20 : TimerEjb.timeout()
情報:   09:30 : TimerEjb.timeout()
情報:   09:40 : TimerEjb.timeout()
情報:   09:50 : TimerEjb.timeout()
  • TimerService を使って、タイマーを動的に登録することができる。
    • TimerService のインスタンスは、 @Resource アノテーションを使ってセッションBean にインジェクションする。
  • ScheduleExpression を使って、 @Schedule アノテーションで指定していたスケジュールを定義する。
  • TimerService#createCalendarTimer(ScheduleExpression) で、スケジュールを登録する。
    • TimerConfig() は、 persistent に false を設定するために指定している。
  • スケジューリングされた時間になると、 @Timeout でアノテートしたメソッドがコールバックされる。

セキュリティ

認証機能を追加する

/secure/* 以下の URL には BASIC 認証が必要になるよう設定する。

BASIC 認証の設定方法については こちら を参照のこと。

なお、ユーザとロールは以下の様に設定する。

ユーザ名 ロール
user user_role
admin admin_role

特定のロールを持っている場合に限りメソッドの実行を許可する

SecureEjb.java
package sample.javaee.ejb;

import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;

@Stateless
public class SecureEjb {

    @RolesAllowed({"user_role", "admin_role"})
    public void forUserMethod() {
        System.out.println("forUserMethod()");
    }

    @RolesAllowed("admin_role")
    public void forAdminMethod() {
        System.out.println("forAdminMethod()");
    }
}
SecureServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.SecureEjb;

@WebServlet("/secure/*")
public class SecureServlet extends HttpServlet {

    @EJB
    private SecureEjb ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String path = req.getRequestURI();

        if (path.endsWith("/user")) {
            this.ejb.forUserMethod();
        } else if (path.endsWith("/admin")) {
            this.ejb.forAdminMethod();
        }
    }
}

Web ブラウザで http://localhost:8080/ejb/secure/user にアクセスする。
すると、 BASIC 認証のログインダイアログが表示されるので、 user ユーザでログインする。

GlassFishコンソール出力
情報:   forUserMethod()

続いて、 http://localhost:8080/ebj/secure/admin にアクセスする。

GlassFishコンソール出力
javax.ejb.EJBAccessException
    at com.sun.ejb.containers.BaseContainer.mapLocal3xException(BaseContainer.java:2351)
(中略)
    at java.lang.Thread.run(Thread.java:745)
Caused by: javax.ejb.AccessLocalException: Client not authorized for this invocation
    at com.sun.ejb.containers.BaseContainer.preInvoke(BaseContainer.java:1960)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:210)
    ... 34 more
  • @RolesAllowed でセッションBean のメソッドをアノテートすると、 value で指定したロールを持ったユーザのみが、そのメソッドを実行できるようになる。
  • ロールは配列で複数指定することができる。
  • @RolesAllowd は、クラスに設定することも可能。
    • その場合は、全てのメソッドに設定が適用される。

誰でも実行できる/誰にも実行できないメソッドを定義する

SecureEjb.java
package sample.javaee.ejb;

+ import javax.annotation.security.DenyAll;
+ import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;

@Stateless
public class SecureEjb {

    @RolesAllowed({"user_role", "admin_role"})
    public void forUserMethod() {
        System.out.println("forUserMethod()");
    }

    @RolesAllowed("admin_role")
    public void forAdminMethod() {
        System.out.println("forAdminMethod()");
    }

+   @PermitAll
+   public void permitAll() {
+       System.out.println("permitAll()");
+   }
+   
+   @DenyAll
+   public void denyAll() {
+       System.out.println("denyAll()");
+   }
}
SecureServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.SecureEjb;

@WebServlet("/secure/*")
public class SecureServlet extends HttpServlet {

    @EJB
    private SecureEjb ejb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String path = req.getRequestURI();

        if (path.endsWith("/user")) {
            this.ejb.forUserMethod();
        } else if (path.endsWith("/admin")) {
            this.ejb.forAdminMethod();
+       } else if (path.endsWith("/permit-all")) {
+           this.ejb.permitAll();
+       } else if (path.endsWith("/deny-all")) {
+           this.ejb.denyAll();
        }
    }
}

user ユーザでログインして、 http://localhost:8080/ejb/secure/permit-all にアクセスする。

GlassFishコンソール出力
情報:   permitAll()

admin ユーザでログインして、 http://localhost:8080/ejb/secure/deny-all にアクセスする。

GlassFishコンソール出力
javax.ejb.EJBAccessException
    at com.sun.ejb.containers.BaseContainer.mapLocal3xException(BaseContainer.java:2351)
(略)
  • @PermitAll でアノテートすると、ロールに関係なくそのメソッドが使用できる。
  • @DenyAll でアノテートすると、ロールに関係なくそのメソッドが使用できなくなる。
  • 両方とも、クラスをアノテートすることで全メソッドに設定を適用させることができる。
  • つまり、ホワイトリスト・ブラックリストのいずれかの方式でアクセス制御を設定するときに使える。

ロールを一時的に切り替える

RunAsAdminEjb.java
package sample.javaee.ejb;

import javax.annotation.security.RunAs;
import javax.ejb.EJB;
import javax.ejb.Stateless;

@Stateless
@RunAs("admin_role")
public class RunAsAdminEjb {

    @EJB
    private SecureEjb secureEjb;

    public void asAdmin() {
        this.secureEjb.forAdminMethod();
    }
}
SecureServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+ import sample.javaee.ejb.RunAsAdminEjb;
import sample.javaee.ejb.SecureEjb;

@WebServlet("/secure/*")
public class SecureServlet extends HttpServlet {

    @EJB
    private SecureEjb ejb;

    @EJB
    private RunAsAdminEjb runAsAdminEjb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String path = req.getRequestURI();

        if (path.endsWith("/user")) {
            this.ejb.forUserMethod();
        } else if (path.endsWith("/admin")) {
            this.ejb.forAdminMethod();
        } else if (path.endsWith("/permit-all")) {
            this.ejb.permitAll();
        } else if (path.endsWith("/deny-all")) {
            this.ejb.denyAll();
+       } else if (path.endsWith("/run-as")) {
+           this.runAsAdminEjb.asAdmin();
        }
    }
}

user ユーザでログインして、 http://localhost:8080/ejb/secure/run-as にアクセスする。

GlassFishコンソール出力
情報:   forAdminMethod()
  • @RunAs でアノテートすると、アノテートされたメソッド(クラス)を実行している間だけ、ロールを value で指定したものに切り替えることができる。

プログラムでロールの有無を確認する

package sample.javaee.ejb;

+ import java.security.Principal;
+ import javax.annotation.Resource;
+ import javax.ejb.SessionContext;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Stateless;

@Stateless
public class SecureEjb {

+   @Resource
+   private SessionContext context;

    @RolesAllowed({"user_role", "admin_role"})
    public void forUserMethod() {
        System.out.println("forUserMethod()");
    }

    @RolesAllowed("admin_role")
    public void forAdminMethod() {
        System.out.println("forAdminMethod()");
    }

    @PermitAll
    public void permitAll() {
        System.out.println("permitAll()");
    }

    @DenyAll
    public void denyAll() {
        System.out.println("denyAll()");
    }

+   public void checkRole() {
+       String name = this.context.getCallerPrincipal().getName();
+       
+       if (this.context.isCallerInRole("user_role")) {
+           System.out.println(name + " has 'user_role'");
+       }
+       
+       if (this.context.isCallerInRole("admin_role")) {
+           System.out.println(name + " has 'admin_role'");
+       }
+   }
}
SecureServlet.java
package sample.javaee.ejb.servlet;

import javax.ejb.EJB;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import sample.javaee.ejb.RunAsAdminEjb;
import sample.javaee.ejb.SecureEjb;

@WebServlet("/secure/*")
public class SecureServlet extends HttpServlet {

    @EJB
    private SecureEjb ejb;

    @EJB
    private RunAsAdminEjb runAsAdminEjb;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String path = req.getRequestURI();

        if (path.endsWith("/user")) {
            this.ejb.forUserMethod();
        } else if (path.endsWith("/admin")) {
            this.ejb.forAdminMethod();
        } else if (path.endsWith("/permit-all")) {
            this.ejb.permitAll();
        } else if (path.endsWith("/deny-all")) {
            this.ejb.denyAll();
        } else if (path.endsWith("/run-as")) {
            this.runAsAdminEjb.asAdmin();
+       } else if (path.endsWith("/check-role")) {
+           this.ejb.checkRole();
        }
    }
}

admin ユーザでログインして、 http://localhost:8080/ejb/secure/check-role にアクセスする。

GlassFishコンソール出力
情報:   admin has 'user_role'
情報:   admin has 'admin_role'
  • SessionContext#isCallerInRole(String) で、現在のユーザが指定したロールを持つかどうかをチェックできる。
    • SessionContext@Resource アノテーションでインジェクションする。
  • SessionContext#getCallerPrincipal() で、認証済みのユーザの情報 Principal が取得できる。

参考