環境構築は こちら。
ソースコードは GitHub にあげています。
https://github.com/opengl8080-javaee-samples/ejb
EJB とは
Enterprise Java Beans の略。
ビジネスロジックを簡潔に実装できるようにしてくれる仕組み・フレームワーク。
ビジネスロジックを持つクラスは POJO で作成でき、エンタープライズアプリケーションで必要になるトランザクション制御やリソース(JNDI, 他の EJB など)の取得、セキュリティ制御、 AOP などなどの機能はコンテナがほとんど自動で提供してくれる。
これにより、プログラマーはビジネスロジックの実装に集中できるようになる。
プロジェクト作成
コンテキストルートが ejb
になるように、 NetBeans で Web プロジェクトを作成する。
Hello World
package sample.javaee.ejb;
import javax.ejb.Stateless;
@Stateless
public class HelloEjb {
public void hello() {
System.out.println("Hello EJB!!");
}
}
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
にアクセスする。
情報: 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
定義
package sample.javaee.ejb;
import javax.ejb.Stateless;
@Stateless
public class StatelessSessionBean {
}
- ステートレスセッションBean を定義するには、
@Stateless
でクラスをアノテートする。 - ステートレスセッションBean には、 public または protected の引数なしのコンストラクタが存在しなければならない。
ライフサイクル
package sample.javaee.ejb;
import javax.ejb.Stateless;
@Stateless
public class StatelessSessionBean {
public void method() {
System.out.println("StatelessSessionBean hash=" + 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.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()
}
}
情報: 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
アノテーションでインジェクションしたインスタンスは、オリジナルのインスタンスがそのままインジェクションされているのではなく、プロキシがインジェクションされている。
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());
}
}
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
にアクセスする。
情報: 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
インスタンスに対して、異なるスレッドからメソッドを呼び出してみる。
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回だけアクセスする。
情報: 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
にアクセスする。
情報: 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 をシャットダウンする。
情報: StatelessSessionBean : pre destroy
-
@PreDestroy
でアノテートされたメソッドは、アクティブ状態のインスタンスが破棄されるときにコールバックされる。 - アノテートできるメソッドの条件は
@PostConstruct
のときと同じ。
ステートフルセッションBean
定義
package sample.javaee.ejb;
import javax.ejb.Stateful;
@Stateful
public class StatefulSessionBean {
}
- ステートフルセッションBean を定義するには、
@Stateful
でクラスをアノテートする。 - ステートフルセッションBean には、 public または protected で引数無しのコンストラクタが存在しなければならない。
ライフサイクル
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);
}
}
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()
}
}
情報: 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 クライアントに呼び出されると、アクティブ状態に戻される。
- パッシブ状態でさらに使用されない場合はパッシブ状態の情報も破棄される。
アクティブ化・インスタンス破棄のときに処理を実行する
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");
+ }
}
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
にアクセス。
情報: StatefulSessionBean : post construct
情報: clientHash=1139797100, count=1
http://localhost:8080/ejb/sfsb/remove
にアクセス。
情報: remove SatefulSessionBean
情報: StatefulSessionBean : pre destroy
- ステートレスセッションBean と同じで、
@PostConstruct
と@PreDestroy
でアノテートしたメソッドは、インスタンスがアクティブ状態になるときと、破棄されるときにコールバックされる。 -
@Remove
でアノテートされたメソッドを実行すると、インスタンスを破棄できる。- ステートレスセッションBean は GlassFish をシャットダウンしたときに
@PreDestroy
がコールバックされたが、ステートフルセッションBean の場合はコールバックされなかった。 - 代わりに、シャットダウン時は後述の
@PrePassivate
でアノテートされたメソッドがコールバックされた。
- ステートレスセッションBean は GlassFish をシャットダウンしたときに
パッシブ化・アクティブ化のときに処理を実行する
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
定義
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つだけ生成され使いまわされることが保証される。
- デフォルトでは、初めてアクセスがあったときにインスタンスが生成され、以後そのインスタンスが使いまわされる。
インスタンス生成時、破棄時に処理を実行する
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");
}
}
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
にアクセスする。
情報: SingletonBean : post construct
情報: SingletonBean : hash=206721482
GlassFish を停止する。
情報: SingletonBean : pre destroy
- 他のセッションBean と同様に、
@PostConstruct
と@PreDestroy
でコールバックを定義できる。
メソッドの同時実行制御
デフォルトは、コンテナが自動で同時実行の制御を行う
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);
}
}
}
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()
}
}
情報: 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
修飾子が自動で付与された状態になる。
-
特定のメソッドだけコンテナの同時実行制御を外す
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);
}
}
}
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()
}
}
情報: 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
でアノテートした場合、そのメソッドだけ設定が上書きされる。
- その状態でさらにメソッドを
タイムアウト時間を設定する
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);
}
}
}
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
に同時に複数回アクセスする。
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
とした場合、前のスレッドが処理を完了するまで待機し続ける。
同時実行制御をコンテナに管理させないようにする
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
を使い自力で実装する(そもそも制御が必要ないのであれば、何もしなくてもいい)。
サーバー起動時にインスタンスを生成させる
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 を起動する。
情報: StartupSingletonBean : post construct
-
@Startup
でシングルトンBean をアノテートすると、サーバー起動時にインスタンスを生成させることができる。 - 初期化処理に重い処理が存在する場合は、この方法が活用できる。
インスタンスの作成順序を指定する
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");
}
}
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");
}
}
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 を起動する。
情報: DependedSingletonBean1 : post construct
情報: DependedSingletonBean2 : post construct
情報: DependingSingletonBean : post construct
-
@DependsOn()
でアノテートすることで、そのシングルトンBean が生成される前に生成されなければならない他のシングルトンBean を指定できる。 - 他のシングルトンBean の指定は、
value
に Bean の名前を配列で渡す。
Bean の名前について
デフォルトでは、クラス名がそのまま Bean の名前になる。
任意の名前を指定したい場合は、 @Singleton
の name
属性で指定する。
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");
}
}
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");
}
}
メソッドを非同期処理にする
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());
}
}
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
にアクセスする。
情報: before thread=http-listener-1(1)
情報: after thread=http-listener-1(1)
情報: asyncMethod() thread=__ejb-thread-pool12
-
@Asynchronous
でセッションBean のメソッドをアノテートすると、そのメソッドは非同期で実行されるようになる。
戻り値を受け取れるようにする
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.");
}
}
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
にアクセスする。
情報: before thread=http-listener-1(4)
情報: after thread=http-listener-1(4)
情報: asyncMethod() thread=__ejb-thread-pool5
情報: result=asyncMethod result.
- 非同期メソッドから戻り値を受け取れるようにするには、
Future
を返すように実装する。 -
Future
はインターフェースなので、具体的な実装にはAsyncResult
を使用する。
EJB がスローした例外について
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");
}
}
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
にアクセスする。
重大: java.lang.Exception: test
at sample.javaee.ejb.ExceptionEjb.throwException(ExceptionEjb.java:9)
(以下略)
Exception
を継承した例外を EJB がスローした場合、その例外はそのままクライアントに送られる。
RuntimeException を継承したクラスは EJBException でラップされてクライアントに送られる
http://localhost:8080/ejb/exception/runtime
にアクセスする。
警告: 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 内にインターセプタ用のメソッドを定義する
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;
}
}
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
にアクセスする。
情報: before
情報: method()
情報: after
-
@AroundInvoke
でメソッドをアノテートすると、他のメソッドが実行されたときに呼び出される。 -
@AroundInvoke
でアノテートできるメソッドには、以下の条件がある。-
Object
型を返すこと。 -
InvocationContext
を引数に受け取る。 - チェック例外をスローする。
-
-
InvocationContext.proceed()
で、インターセプトしているメソッドを実行できる。
インターセプタを別クラスで定義する
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;
}
}
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()");
}
}
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
にアクセスする。
情報: before
情報: InterceptorBean.method()
情報: after
- セッションBean 内に定義したのと同じ要領で、
@AroundInvoke
でアノテートしたクラスを作成する。 -
@Interceptors
アノテーションで適用するインターセプタのクラスを指定する。-
@Interceptors
アノテーションは、クラスにも指定することができる。 - その場合は、全てのメソッドにインターセプタを適用できる。
- クラスを
@Interceptors
でアノテートしたものの、あるメソッドだけインターセプタを適用したくない場合は、@ExcludeClassInterceptors
でメソッドをアノテートする。
-
全ての EJB にインターセプタを適用させる
@Interceptors
を使った方法だと、「全ての EJB にインターセプタを適用したい」といったときに、わざわざ全てのクラスをアノテートしなくてはいけなくなる。
こういう場合は、デフォルトインターセプタというものを使用する。
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;
}
}
<?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
にアクセスする。
情報: DefaultInterceptor before
情報: before
情報: InterceptorBean.method()
情報: after
情報: DefaultInterceptor after
-
ejb-jar.xml
で定義することで、アノテーション無しでインターセプタを定義することができる。-
ejb-jar.xml
は、WEB-INF
の直下に配置する。
-
-
<ejb-name>
タグで、インターセプタを適用する EJB を指定する。- ワイルドカード(
*
)を指定できる。 - 例の場合は
*
だけなので、全ての EJB が対象になる。
- ワイルドカード(
-
<interceptor-binding>
タグは複数設定可能なので、下記の用に<ejb-name>
ごとに異なるインターセプタを適用させることもできる。
<?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>
複数のデフォルトインターセプタを適用する
<?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>
情報: DefaultInterceptor before
情報: DefaultInterceptor2 before
情報: before
情報: InterceptorBean.method()
情報: after
情報: DefaultInterceptor2 after
情報: DefaultInterceptor after
-
<interceptor-class>
タグを並べることで、複数のインターセプタを適用することができる。 - インターセプタの適用順序は、並べた順序に一致する。
デフォルトインターセプタを適用させないようにする
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()");
}
}
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
にアクセスする。
情報: ExcludeDefaultInterceptorEjb.method()
-
@ExcludeDefaultInterceptors
でメソッドをアノテートすると、デフォルトインターセプタが適用されなくなる。 - クラスをアノテートすれば、全てのメソッドがデフォルトインターセプタの対象外になる。
EJBのメソッドを定期的に実行する
アノテーションを使って宣言的に定義する
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 を起動する。
情報: 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分おき |
プログラムで動的にタイマーを登録する
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 を起動する。
情報: 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 |
特定のロールを持っている場合に限りメソッドの実行を許可する
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()");
}
}
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
ユーザでログインする。
情報: forUserMethod()
続いて、 http://localhost:8080/ebj/secure/admin
にアクセスする。
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
は、クラスに設定することも可能。- その場合は、全てのメソッドに設定が適用される。
誰でも実行できる/誰にも実行できないメソッドを定義する
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()");
+ }
}
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
にアクセスする。
情報: permitAll()
admin
ユーザでログインして、 http://localhost:8080/ejb/secure/deny-all
にアクセスする。
javax.ejb.EJBAccessException
at com.sun.ejb.containers.BaseContainer.mapLocal3xException(BaseContainer.java:2351)
(略)
-
@PermitAll
でアノテートすると、ロールに関係なくそのメソッドが使用できる。 -
@DenyAll
でアノテートすると、ロールに関係なくそのメソッドが使用できなくなる。 - 両方とも、クラスをアノテートすることで全メソッドに設定を適用させることができる。
- つまり、ホワイトリスト・ブラックリストのいずれかの方式でアクセス制御を設定するときに使える。
ロールを一時的に切り替える
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();
}
}
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
にアクセスする。
情報: 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'");
+ }
+ }
}
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
にアクセスする。
情報: admin has 'user_role'
情報: admin has 'admin_role'
-
SessionContext#isCallerInRole(String)
で、現在のユーザが指定したロールを持つかどうかをチェックできる。-
SessionContext
は@Resource
アノテーションでインジェクションする。
-
-
SessionContext#getCallerPrincipal()
で、認証済みのユーザの情報Principal
が取得できる。