SessionBean?とは? †
- JavaEE の業務処理層
- トランザクション制御を行う
- Session Bean 以下の処理がすべて成功したら RDB に処理結果を反映 (Commit)
- どこかで処理を失敗したら、途中までの RDB 処理を差し戻す (Rollback)
- 通信経由で呼び出すこともできるがやらない
- 下位プロトコルに RMI-IIOP を使って、ネットワーク経由で Session Bean を呼び出すことができる。プログラムコードはローカル呼び出しと変わらず、設定をちょこっと変えるだけ。
- 速度的なメリットは全くない。ローカル呼び出しに比べて数千倍のオーダーで遅い
- ネットワーク経由で処理要求を出したいなら、JAX-RS (REST) を使うべき
サンプルプログラム †
- Glassfish JPA の続き
- OrderService?.java (顧客からの受注処理)
package com.mycompany.biz;
import com.mycompany.common.BusinessException;
import com.mycompany.entity.CustomerTable;
import com.mycompany.entity.ItemTable;
import com.mycompany.entity.OrderTable;
import java.util.Date;
import javax.ejb.EJB;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.commons.lang.time.DateUtils;
@Stateless
@LocalBean
public class OrderService {
@PersistenceContext(unitName = "warehousePU")
private EntityManager em;
@EJB
private FactoryService factory;
public OrderTable orderItem(final long customerId, final long itemId, final int amount) throws BusinessException {
CustomerTable customer = em.find(CustomerTable.class, customerId);
ItemTable item = em.find(ItemTable.class, itemId);
OrderTable order = new OrderTable();
order.setCustomerId(customer);
order.setItemId(item);
order.setAmount(amount);
if (item.getStock() > amount) {
// 在庫があった
item.setStock(item.getStock() - amount);
Date shipping = DateUtils.addDays(new Date(), 1);
order.setShipdate(shipping);
} else {
// 在庫がなかったので工場に発注
Date supply = factory.backorder(itemId, amount - item.getStock());
Date shipping = DateUtils.addDays(supply, 1);
order.setShipdate(shipping);
item.setStock(0L);
}
em.persist(order);
return order;
}
}
- Factory.java (工場への発注処理)
package com.mycompany.biz;
import com.mycompany.common.BusinessException;
import java.util.Date;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
@Stateless
@LocalBean
public class FactoryService {
public Date backorder(final long itemId, final long amount) throws BusinessException {
// TODO Implement Me!
return null;
}
}
- 見れば分かるとおり、普通の POJO (Plane Old Java Object)
- アノテーションで、トランザクションや依存ライブラリを設定する
@Local, @Remote, @Stateless, @Stateful, @Singleton †
@Local, @Remote †
- @Local : ローカル呼び出し
- @Remote : リモート呼び出し
- 同じ JavaEE サーバ内でも @Remote にすると、引数がシリアライズされる。基本的に @Remote は使わない
- 未指定時 @Local。あとで*1混乱するので、一律 @Local をつけておいた方が良い
@Stateless †
- 状態を保持しない Session Bean
- 普通はこれを使う
@Stateful †
- ユーザごとに状態を保持する Session Bean
- Web層の Cookie (jsessionid) でユーザを判別
- @SatefulTimeout?(value = 20, unit = TimeUnit?.MINUTES)
- クラスに対するアノテーション
- 20分間使われなかったら削除
- @StatefulTimeout? 未指定時は、Session Timeout で削除
- メモリ不足でディスクに退避される可能性がある
@Singleton †
- 全ユーザで一つになる Session Bean
- 何に使うんぢゃい? → WebSocket? のデザインパターンになるかも GlassFish v3.1 の新機能概要、寺田佳央、2010-12-21
- @Startup
- クラスに対するアノテーション
- コンテナ起動時に起動
- @DependsOn?("XEJB","YEJB")
- @Lock(LockType?.WRITE), @Lock(LockType?.READ)
- クラス・メソッドに対するアノテーション。両方指定した場合にはメソッドに設定した方が有効になる
- @Lock(LockType?.WRITE) : ロックする
- @Lock(LockType?.READ) : ロックしない
- @AccessTimeout?(value = 20, unit = TimeUnit?.SECONDS)
- @Lock(LockType?.WRITE) といっしょに使う
- 20秒待ってもロックが解除されなかった場合には ConcurrentAccessException? が発生する
- Singleton は Passivate されない → http://docs.oracle.com/javaee/6/tutorial/doc/giplj.html
インジェクション †
@PersistenceContext?(unitName = "warehousePU") †
@PersistenceContext(unitName = "warehousePU")
private EntityManager em;
@EJB †
@EJB
private FactoryService factory;
他の Session Bean をインジェクション
@Resource †
@Resource
private SessionContext context;
セッションコンテキストをインジェクション
Principal | SessionContext?#getCallerPrincipal?() | 呼び出しユーザを得る |
boolean | SessionContext?#getRollbackOnly?() | ロールバックされたか? |
TimerService? | SessionContext?#getTimerService?() | タイマサービスを得る |
UserTransaction? | SessionContext?#getUserTransaction?() | ユーザトランザクションを得る |
boolean | SessionContext?#isCallerInRole?(String roleName) | 呼び出しユーザは roleName か? |
Object | SessionContext?#lookup(String name) | EJB管理下のオブジェクトをlookupする |
void | SessionContext?#setRollbackOnly?() | ロールバックする |
boolean | SessionContext?#wasCancelCalled?() | 非同期で呼び出した Session Bean の実行が cancel されたか? |
非同期・タイマ †
@Asynchronous †
- クラス・メソッドに対するアノテーション
- 非同期実行になる
- 非同期 Session Bean
@Stateless
public class AsyncEJB {
@Resource
private SessionContext ctx;
@Asynchronous
public Future<Integer> doSomething () {
if (ctx.wasCancelCalled()) {
return new AsyncResult<Integer>(-1);
}
// do something
return new AsyncResult<Integer>(0);
}
}
- 非同期 Session Bean の使い方
@Stateless
public class ServiceEJB {
@EJB
private AsyncEJB asyncEjb;
public int xyzService () {
Future<Integer> futureStatus = asyncEjb.doSomething();
Integer status = status.get();
return status;
}
}
- 非同期実行のキャンセル
Future#cancel()
- キャンセルされたかどうか
SessionContext#wasCancelCalled()
- 突き放し実行 → 非同期実行メソッドの返値を void にして、呼び出し側で Future#get() しなければよい
タイマ †
@Stateless
public class TimerEJB {
@Schedule(dayOfMonth = "1", hour = "4")
public void task() {
...
}
}
- 毎月 1 日の 4 時に task() を実行
- @Schedules({ @Schedule(...), @Schedule(...) }) という書き方も出来る
- 時刻指定
second | [0,59] |
minute | [0,59] |
hour | [0,23] |
dayOfMonth? | [1,31],{"Sun","Mon","Tue","Wed","Thu","Fri","Sat"} |
month | [1,12],{"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"} |
year | [0,9999] |
timezone | Europe/Paris |
- 指定形式
単一 | year="2013" | 2012年 |
ワイルドカード | hour="*", minute="0,15,30,45" | 毎時 0,15,30,45 分 |
範囲 | hour="3-6" | 3時から6時 |
増分 | dayOfWeek?="Sun", hour="20/2" | 日曜日の 20 時 22 時 |
- プログラムからのタイマ作成
@Resource
SessionContext context;
context#getTimerService()
または、
@Resource
TimerService timerService;
で取得した timerService にタスクを投入する
ScheduleExpression schedule = new ScheduleExpression().dayOfMonth("1");
timerSchedule.createCalendarTimer(schedule, new TimerConfig(argObj, true));
// argObj is info like as "hello config", true is persistence flag
// new TimerConifig() is also ok.
指定された時刻に
@Timeout
public void task(Time timer) {
ArgClass argObj = (ArgClass) timer.getInfo();
...
}
が動く。
インターセプタ †
@Stateless
public class SomeEJB {
public Customer doSomething() {
...
}
public void doAnything() {
...
}
@AroundInvoke
private Object interceptor(InvocatonContext ic) throws Exception {
...
事前処理
...
ic.proceeed();
...
事後処理
...
}
}
- @AroundInvoke? アノテーションをつけたメソッドが、doSomething() や doAnything() に先だって呼ばれる
Object | ic#getTarget() | オブジェクト |
Method | ic#getMethod() | メソッド |
Object[] | ic#getParameters() | 引数 |
Timer | ic#getTimer() | タイマー |
Map<String,Object> | ic#getContextData?() | 複数のインターセプトを設定したとき、それらで共通に使えるMap |
Object | ic#proceed() | 本来呼び出されるメソッドの呼び出し。返値が void の場合は、返値は null |
void | ic#setParameters(Object[]) | 引数を差し替える |
- インターセプターを外部クラスに定義できる
@Stateless
public class SomeEJB {
@Interceptors(Interceptor.class)
public Customer doSomething() {
...
}
public void doAnything() {
...
}
}
public class Interceptor
@AroundInvoke
private Object interceptor(InvocatonContext ic) throws Exception {
...
事前処理
...
ic.proceeed();
...
事後処理
...
}
}
- クラスに @Interceptors(Interceptor.class) を指定したら、全メソッドに反映
- クラスに @Interceptors(Interceptor.class) を指定した場合、メソッドに @ExcludeInterceptors? で除外指定できる
- 全 EJB に適用する (ejb-jar.xml)
<assembly-descriptor>
<interceptor-binding>
<ejb-name>*</ejb-name>
<interceptor-class>com.foo.Interceptor</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
トランザクション †
- @TransactonAttribute?(TransactionAttributeType?.REQUIRED)
- クラス・メソッドに指定できる
- 両方に指定した場合には、近い方が有効
- 指定しない場合には REQUIRED になる (通常はこれで良い)
- トランザクション属性
CMT属性 | トランザクション外から呼び出された | トランザクション内から呼び出された |
REQUIRED | トランザクション開始 | 呼び出し元トランザクション内で動く |
REQUIRES_NEW | トランザクション開始 | 別のトランザクション開始 |
SUPPORTS | トランザクションなし | 呼び出し元トランザクション内で動く |
MANDATORY | javax.ejb.EJBTransactionRequiredException? | 呼び出し元トランザクション内で動く |
NOT_SUPPORTED | トランザクションなし | トランザクションなし |
NEVER | トランザクションなし | javax.ejb.EJBException |
- ロールバック
- SessionContext?#setRollbackOnly?()
@Stateless
public class SomeEJB {
@Resource
private SessionContext ctx;
public Customer doSomething() {
...
if (fail) {
ctx.setRollbackOnly();
}
}
}
- 例外を発生させる
発生させる例外 | @ApplicationException? | ロールバック |
Exception | なし | しない |
Exception | @ApplicationException?(rollback = true) | する |
Exception | @ApplicationException?(rollback = false) | しない |
RuntimeException? | なし | する |
RuntimeException? | @ApplicationException?(rollback = true) | する |
RuntimeException? | @ApplicationException?(rollback = false) | しない |
- 通常の例外ではロールバックしないことに注意。次のような例外クラスを作る。Web層でも使うので、common パッケージ に入れる
package com.mycompany.common;
import javax.ejb.ApplicationException;
@ApplicationException(rollback = true)
public class BusinessException extends Exception {
public BusinessException(String msg) {
super(msg);
}
}
- 非同期呼び出しのトランザクションってどうなるんだっけ? → トランザクションは伝播しない
- BisinessException? が EAR や EJB-JAR に同梱されていない場合、 @ApplicationException? が無視される (少なくとも Glassfish3/4 はそうなる)
ロール †
- Web層での認証結果をEJBでも使いたいときに使う → Glassfish 認証・認可(LDAP)
@Stateless
@RolesAllowed({"user","employee","admin"})
public class SecureEJB {
@Resource
private SessionContext ctx;
public void doSomething() {
// user, employee, admin ロールを持っているユーザのみ実行可能
...
}
@PermitAll;
public String doAnything() {
// 全ユーザ実行可能
...
if (ctx.isCallerInRole("admin") {
// admin に対する処理
...
} else if (ctx.isCallerInRole("customer")) {
// customer に対する処理
return "Hello " + ctx.getCallerPrincipal().getName();
}
return null;
}
@DenyAll;
public void doAnything() {
// どのユーザも実行できない
...
}
- セキュリティ関連のアノテーション
アノテーション | 設定可能箇所 | 概要 |
@RolesAllowed?({"role1","role2",...}) | Class | Method | 指定されたロールを持つユーザのみ実行可能 |
@PermitAll? | Class | Method | 誰でも実行可能 |
@DenyAll? | Class | Method | 誰も実行できない。Classに指定して、Methodで @RolesAllowed? とか |
@RunAs?("role") | Class | | 設定したクラスでは、仮に role ロールで動く |
@DeclareRoles? | Class | | 利用するロールを列挙する。web.xml の <security-role> で定義すべき → Glassfish 認証・認可(LDAP) |
- ユーザ情報の取得 @Resource SessionContext? ctx より取得可能
- ※ web.xml、glassfish-web.xml だけ設定すれば良いはずだけど ejb-jar.xml、glassfish-ejb-jar.xm にも同じ設定をする必要があるかも知れない。あとで検証
Java#Glassfish