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"/> ...