JBoss SeamにはjBPMの利用を支援するコンポーネントが同梱されていますが、アプリケーション側で例外が発生したり、jBPM側(プロセス定義中の各種ハンドラなど)で例外が発生した際に、jBPMのデータがコミットされてしまう不具合があります。
この不具合のバグレポートが挙がっていますが、まだ解決されていないようです。
上記のバグレポートによればSeam 2.2.0.GAで発生するようですが、手元の2.1.2でも同様の問題が確認できました。
原因
この不具合の原因は2つあります。
- 例外発生後もjBPMのエンティティがHibernate Session上に残ってしまっている。
- JbpmContextのxxxForUpdateメソッドを使っており、Hibernate Session上だけでなく、JbpmContext.autoSaveProcessInstancesにもエンティティが残っている為、クローズ時に保存されてしまう。
SeamではHTTPリクエスト1件を複数のトランザクションで処理しますが、ビジネス層のトランザクション(Restore ViewフェーズからInvoke Applicationフェーズまで)後もjBPMのエンティティがHibernate Sessionに残っており、プレゼンテーション層のトランザクション(Render Responseフェーズ)でコミットされてしまいます。
また、xxxForUpdateメソッドを使用しているため、例外発生時にjBPMのHibernate Sessionをクリアしても、JbpmContextのautoSaveProcessInstancesにエンティティが残っており、jBPMデータが登録されてしまいます。
ちなみに、アプリケーション側のエンティティも永続コンテキストに残っているのに、なぜコミットされないのか疑問に思い調べたところ、Render Responseフェーズ直前でエンティティマネージャのFlushModeをMANUALに切り替えている為のようです(SeamPhaseListener#beforeRenderResponseの最後のあたり)。
回避策1. SeamのjBPM関連コンポーネントを使わない
SeamのjBPM関連コンポーネントは使わず、アプリケーションロジックから直接jBPM APIを叩くことで、この問題を回避…、というか無視できます。
jBPMの永続化には、Seam管理のHibernate Sessionを使用します。
Seam管理のHibernate Session - components.xml
...
<persistence:hibernate-session-factory name="hibernateSessionFactory"/>
<persistence:managed-hibernate-session name="hibernateSession"
auto-create="true"
session-factory-jndi-name="java:/hibernateSessionFactory"/>
...Hibernate SessionをJbpmContextインスタンスに注入することで、アプリケーションと同じトランザクション境界で処理できるようになります。
...
public void open() {
Session hibernateSession = (Session) Component.getInstance("hibernateSession");
JbpmContext jbpmContext = JbpmConfiguration.getInstance().createJbpmContext();
jbpmContext.setSession(hibernateSession);
return jbpmContext;
}
...回避策2. SeamのjBPM関連コンポーネントを置き換える
SeamのjBPM関連コンポーネントを使う場合は、上記の不具合を回避するよう修正したコンポーネントを、Seam同梱のコンポーネントと置き換えます。
public class MyManagedJbpmContext extends ManagedJbpmContext {
@Override
public void beforeCompletion() {
if (BusinessProcess.instance().hasActiveProcess()) {
//JbpmContextのxxxForUpdateメソッドを使わず、明示的にエンティティを保存する。
instance().save(ProcessInstance.instance());
}
super.beforeCompletion();
}
@Override
public void afterCompletion(int status) {
if (status == Status.STATUS_ROLLEDBACK) {
instance().getSession().clear();
}
super.afterCompletion(status);
}
}ManagedJbpmContextのほかに、JbpmContextのxxxForUpdateメソッドを叩いているコンポーネント(BusinessProcess、PooledTask、ProcessInstance、TaskInstance)をオーバーライドします。
例) BusinessProcess
public class MyBusinessProcess extends BusinessProcess {
@Override
public void createProcess(String processDefinitionName, boolean shouldSignalProcess) {
createProcess(processDefinitionName, null, shouldSignalProcess);
}
@Override
public void createProcess(String processDefinitionName, String businessKey) {
createProcess(processDefinitionName, businessKey, true);
}
protected void createProcess(String processDefinitionName, String businessKey, boolean shouldSignalProcess) {
//xxxForUpdateメソッドは使わない。
ProcessInstance process = ManagedJbpmContext.instance().newProcessInstance(processDefinitionName);
if (!Strings.isEmpty(businessKey)) process.setKey(businessKey);
afterCreateProcess(processDefinitionName, process, shouldSignalProcess);
}
private void afterCreateProcess(String processDefinitionName, ProcessInstance process, boolean shouldSignalProcess) {
/*
* オリジナルのBusinessProcessからコピーする。
*/
...
}
}最後に、Seam同梱のコンポーネントを修正したコンポーネントで置き換えます。
components.xml ... <component class="com.natswell.jbpm.MyManagedJbpmContext" name="org.jboss.seam.bpm.jbpmContext" scope="event"/> <component class="com.natswell.jbpm.MyBusinessProcess" name="org.jboss.seam.bpm.businessProcess" scope="conversation"/> ...