2009年12月6日日曜日

コンテナ管理トランザクションであそぶ - (1/10) -

はじめに

EJBのコンテナ管理トランザクション(ContainerManagedTransactions-CMT)とは
どんなものなのか試してみようというのが今回のテーマです。
ステートレスのローカルオブジェクトのセッション Bean で検証をします。
主に@TransactionAttributeアノテーションによってTransactionAttributeTypeが
REQUIREDとREQUIRES_NEWに指定されたメソッドがコミットされる様子と
ロールバックされる様子を観察したいと思います。
インターセプターを使ってトランザクション内に例外を発生させてロールバックするようにします。
確認のためにセッションIDをアクセスログとして保存し表示する検証用Webアプリを作成します。
検証用Webアプリはシンプルにしたいのでフレームワークを使わずServletとJSPで作成します。

以下の内容を検証します。
  1. REQUIREDに指定されたメソッド同士は同じトランザクションとして処理されるのか?
    トランザクションに参加するいずれかのメソッドで例外が発生した場合、
    その トランザクションに参加するすべてのメソッドでの処理がロールバックされるか検証します。
  2. REQUIRES_NEWに指定されたメソッドは別トランザクションを開始して処理されるのか?
    別トランザクションのメソッドで例外が発生した場合でも REQUIRES_NEWに指定された
    メソッドでの処理がコミットされていれば、その処理はロールバックされないことを検証します。
    (別トランザクションのメソッドでの処理はロールバックされる)

コンテナ管理トランザクションであそぶ - (2/10) -

準備する

検証環境
アプリケーションサーバー GlassFish v2.1
データベースサーバー Apache Derby
OS Windows XP
ビルドツール Maven 2
※mvnコマンドが実行できるように設定しておいてください。

適当な場所に作業フォルダー(C:\mywork)を作り、フォルダとファイルを配置します。
一括ビルドプロジェクト(build)、EARファイルプロジェクト(build-ear)、
EJBプロジェクト(ejb-module)、Webアプリプロジェクト(webtest-client)を作成します。

  • C:\mywork
    • build
      • pom.xml
    • build-ear
      • pom.xml
    • ejb-module
      • src
        • main
          • java
            • com
              • mydomain
                • ejb
                  • Action.java
                  • ActionImpl.java
                  • Message.java
                  • MessageUtil.java
                  • Rollback.java
                  • SubAction.java
                  • SubActionImpl.java
          • resources
            • META-INF
              • ejb-jar.xml
              • persistence.xml
      • pom.xml

    • webtest-client
      • src
        • main
          • java
            • com
              • mydomain
                • ejb
                  • Client.java
                  • PageData.java
          • webapp
            • WEB-INF
              • web.xml
            • index.jsp
      • pom.xml

コンテナ管理トランザクションであそぶ - (3/10) -

DataSourceの設定をする

asadmin create-jdbc-connection-poolコマンドを使ってコネクションプールを作成します。

コマンドプロンプトを開き以下のように入力します。
asadmin create-jdbc-connection-pool ^
  --datasourceclassname org.apache.derby.jdbc.ClientDataSource ^
  --restype javax.sql.DataSource ^
  --property user=sa:password=sa:databaseName=example ^
  MyJDBCPool
コネクションプールのプロパティに接続文字列を追加します。
asadmin set ^
  domain.resources.jdbc-connection-pool.MyJDBCPool.property.url=jdbc:derby://localhost:1527/example
コネクションプールのプロパティに接続文字列属性を追加します。
asadmin set ^
  domain.resources.jdbc-connection-pool.MyJDBCPool.property.connectionAttributes=;create=true
コネクションの接続を確認します。
asadmin ping-connection-pool MyJDBCPool


asadmin create-jdbc-resourceコマンドを使ってコネクションプールをJNDIに登録します。

コマンドプロンプトを開き以下のように入力します。
asadmin create-jdbc-resource ^
  --connectionpoolid MyJDBCPool ^
  jdbc/MyJDBCResource
GlassFish管理コマンド

asadmin コマンドを使用するためには 『GLASSFISHインストールフォルダ/bin』 フォルダを
PATH変数に追加しておかないといけません。
コントロールパネルのシステムの詳細設定タブから環境変数を編集します。
それからGlassFishサーバーとJavaDBを起動します。
WebのGlassFish管理コンソールからコネクションプールをJDBCリソースに登録する場合は
『jdbc/MyJDBCResource』 でJNDIから参照できるようにします。
Persistence Unitを定義します。

persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
  xmlns="http://java.sun.com/xml/ns/persistence" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
                      http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
  <persistence-unit name="example-pu">
  <provider>oracle.toplink.essentials.PersistenceProvider</provider>
    <jta-data-source>jdbc/MyJDBCResource</jta-data-source>
    <properties>
      <property name="toplink.ddl-generation" value="drop-and-create-tables" />
    </properties>
  </persistence-unit>
</persistence>
Persistence UnitをJNDIのグローバルネームスペース 『example-pu』 に登録します。

コンテナ管理トランザクションであそぶ - (4/10) -

ビジネスBeanを作成する

ステートレス Session Beanを作成します。
ビジネスインターフェイスとセッションBeanを2組作成します。

Action.java
package com.mydomain.ejb;

import java.util.List;
import javax.ejb.*;

@Local
public interface Action
{
    public void regist(String message, int rollbak);
    
    public void insert(Message message);
    
    public void decorateInsert(Message message);

    public List<Message> getMessages();
    
    public void delete();
}
ActionImpl.java
package com.mydomain.ejb;

import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
import static com.mydomain.ejb.MessageUtil.*;

@Stateless
public class ActionImpl implements Action
{
    @PersistenceContext(unitName="example-pu")
    private EntityManager em;
    
    @EJB
    private SubAction subAction;

    public void regist(String text, int rollback)          
    {       
        this.decorateInsert(createMessage(text, 2, rollback));
        this.insert(createMessage(text, 3, rollback));
        this.subAction.decorateInsert(createMessage(text, 5, rollback));
        this.subAction.insert(createMessage(text, 6, rollback));
        try
        {
            this.subAction.requiresNewInsert(createMessage(text, 8, rollback));
        }
        catch(Exception exp){}

        Message chkMessage = createMessage(text, 1, rollback);
        doRollback(chkMessage);
    }

    public void insert(Message message)          
    {
        setLogMessage(message, this.getClass(), "insert");
        this.em.persist(message);
        
        Message decMessage = createMessage(message);
        setNextLogMessage(decMessage);
        this.decorateInsert(decMessage);
        
        doRollback(message);
    }
    
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void decorateInsert(Message message)            
    {
        setLogMessage(message, this.getClass(), "decorateInsert");
        message.setText(message.getText() + "*");
        this.em.persist(message);

        doRollback(message);
    }

    public List<Message> getMessages()
    {
        String sql = "SELECT m FROM message AS m ORDER BY m.no";
        return this.em.createQuery(sql).getResultList();
    }

    public void delete()
    {
        this.em.createQuery("DELETE FROM message").executeUpdate();
    }
}
regist()メソッド
サーブレットから呼出されてセッションIDをアクセスログとして登録するメインの処理を開始します。
処理の最後にMessageUtil.doRollback()メソッドを呼出して例外発生の判定します。
insert()メソッド
セッションIDを登録します。
処理の最後にMessageUtil.doRollback()メソッドを呼出して例外発生の判定します。
decorateInsert()メソッド
セッションIDに『*』 アスタリスクを付けて登録します。
REQUIRES_NEW を指定して別トランザクションとして処理されるようにします。
処理の最後にMessageUtil.doRollback()メソッドを呼出して例外発生の判定します。


SubAction.java
package com.mydomain.ejb;

import javax.ejb.Local;

@Local
public interface SubAction
{
    public void insert(Message message);

    public void decorateInsert(Message message);
    
    public void requiresNewInsert(Message message);
}
SubActionImpl.java
package com.mydomain.ejb;

import javax.ejb.*;
import javax.persistence.*;
import static com.mydomain.ejb.MessageUtil.*;

@Stateless(name="subActionImpl")                 
public class SubActionImpl implements SubAction
{
    @PersistenceContext(unitName="example-pu")
    private EntityManager em;

    public void insert(Message message)          
    {
        setLogMessage(message, this.getClass(), "insert");
        this.em.persist(message);

        Message decMessage = createMessage(message);
        setNextLogMessage(decMessage);
        this.decorateInsert(decMessage);

        doRollback(message);
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void decorateInsert(Message message)            
    {
        setLogMessage(message, this.getClass(), "decorateInsert");
        message.setText(message.getText() + "*");
        this.em.persist(message);

        doRollback(message);
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void requiresNewInsert(Message message)         
    {
        String sessionId = message.getText();
        message.setText(sessionId + "+");
        this.em.persist(message);

        Message decMessage = createMessage(message);
        setNextLogMessage(decMessage);
        decMessage.setText(sessionId);
        this.insert(decMessage);
    }
}
このクラスのEJB名(EJBコンテナ内での名前)をsubActionImpl に設定します。
insert()メソッド
セッションIDを登録します。
処理の最後にMessageUtil.doRollback()メソッドを呼出して例外発生の判定します。
decorateInsert()メソッド
セッションIDに『*』 アスタリスクを付けて登録します。
REQUIRES_NEW を指定して別トランザクションとして処理されるようにします。
処理の最後にMessageUtil.doRollback()メソッドを呼出して例外発生の判定します。
requiresNewInsert()メソッド
セッションIDに『+』 プラスを付けて登録します。
REQUIRES_NEW を指定して別トランザクションとして処理されるようにします。
処理の最後にインターセプターによって例外発生の判定をします。

コンテナ管理トランザクションであそぶ - (5/10) -

インターセプターを作成する

インターセプターを作成します。

Rollback.java
package com.mydomain.ejb;

import javax.interceptor.*;
import static com.mydomain.ejb.MessageUtil.*;

public class Rollback
{
    @AroundInvoke
    public Object action(InvocationContext ctx) throws Exception
    {
        Object[] params = ctx.getParameters();
        Message message = (Message)params[0];
        setLogMessage(message, ctx.getTarget().getClass(), ctx.getMethod().getName());
        
        Object retObj = ctx.proceed();                     
        
        doRollback(message);
        
        return retObj;
    }
}
同じクラスの同じメソッドであっても呼出される条件によってインターセプトが適用されない場合もあったので
SubActionImpl クラスの requiresNewInsert() メソッドのみにインターセプトを設定しています。
ejb-jar.xml
<ejb-jar version="3.0"
  xmlns="http://java.sun.com/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
                      http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd">
  <interceptors>
    <interceptor>
      <interceptor-class>com.mydomain.ejb.Rollback</interceptor-class>         
    </interceptor>
  </interceptors>
  <assembly-descriptor>
    <interceptor-binding>
      <ejb-name>subActionImpl</ejb-name>         
      <interceptor-class>com.mydomain.ejb.Rollback</interceptor-class>
      <method>
        <method-name>requiresNewInsert</method-name>
        <method-params>
          <method-param>com.mydomain.ejb.Message</method-param>
        </method-params>
      </method>
    </interceptor-binding>
  </assembly-descriptor>
</ejb-jar>
インターセプトされないメソッドの呼出し

EJBインスタンスを通して直接呼出される場合はインターセプトが適用されるようですが
メソッドの処理の中で呼出される場合にはインターセプトが 適用されないみたいでした。
今回の検証用Webアプリの例でいうとActionImplクラスのinsert()メソッドとdecorateInsert()メソッドは
regist()メソッドから呼出されるためインターセプトは適用されないようです。
(regist()メソッドはサーブレットから直接呼出されるのでインターセプトが適用できるようです。)
またSubActionImplクラスのinsert()メソッドとdecorateInsert()メソッドは
ActionImplクラスのregist()メソッドから直接呼出される場合にはインターセプトが適用されるようですが、
requiresNewInsert()メソッドの処理の中から呼出されたinsert()メソッド、
insert()メソッドの処理の中から呼出されたdecorateInsert()メソッドには インターセプトが適用されないようでした。

コンテナ管理トランザクションであそぶ - (6/10) -

エンティティBeanを作成する

エンティティBeanを作成します。

Message.java
package com.mydomain.ejb;

import java.io.Serializable;
import javax.persistence.*;

@Entity(name="message")
public class Message implements Serializable
{
    private static final long serialVersionUID = 1L;

    public Message(){}

    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE)
    private long id;
    public long getId(){ return this.id; }
    public void setId(long id){ this.id = id; }

    private String text;
    public String getText(){ return this.text; }
    public void setText(String text){ this.text = text; }

    @Column(name="number")
    private int no;
    public int getNo(){ return this.no; }
    public void setNo(int no){ this.no = no; }

    @Column(name="rollback_point")
    private int rollback;
    public int getRollback(){ return this.rollback; }
    public void setRollback(int rollback){ this.rollback = rollback; }

    private String biko;
    public String getBiko(){ return this.biko; }
    public void setBiko(String biko){ this.biko = biko; }
}


エンティティBeanの生成と例外の生成をするユーティリティクラスを作成します。

MessageUtil.java
package com.mydomain.ejb;

public class MessageUtil
{    
    public static Message createMessage(Message message)
    {
        return createMessage(
            message.getText(), message.getNo(), message.getRollback(), message.getBiko());
    }

    public static Message createMessage(String text, int no, int rollback)
    {
        return createMessage(text, no, rollback, "");
    }
    
    public static Message createMessage(String text, int no, int rollback, String biko)
    {
        Message message = new Message();
        message.setText(text);
        message.setNo(no);
        message.setRollback(rollback);
        message.setBiko(biko);
        return message;
    }

    public static void setLogMessage(Message message, Class cls, String method)
    {
        String biko = cls.getSimpleName() + ":" + method + "( )";
        message.setBiko(message.getBiko() + biko);
    }
    
    public static void setNextLogMessage(Message message)
    {
        message.setBiko(message.getBiko() + "=>");
        message.setNo(message.getNo()+1);
    }
    
    public static boolean doRollback(Message message)      
    {
        if(message.getRollback() == message.getNo())
        {
            Object obj = null;
            obj.toString();
        }
        return false;
    }
}
doRollback()メソッド
例外発生の判定をします。
例外はjava.lang.NullPointerExceptionをthrowします。

コンテナ管理トランザクションであそぶ - (7/10) -

検証用Webアプリを作成する

サーブレットとviewページを作成します。

Client.java
package com.mydomain.ejb;

import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import javax.ejb.EJB;
import javax.servlet.*;
import javax.servlet.http.*;

public class Client extends HttpServlet
{
    @EJB
    private Action action;
    
    protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException
    {
        HttpSession session = req.getSession();
        try
        {
            this.action.delete();
            String strRollback = req.getParameterMap().containsKey("rollback")
                ? req.getParameter("rollback").toString() : "0";
            int rollback = Pattern.matches("[^0-9]", strRollback)
                ? 0 : Integer.parseInt(strRollback);
            try
            {
                this.action.regist(session.getId(), rollback);
            }
            catch(Exception exp){}
            req.setAttribute("messages", this.action.getMessages());
            this.setMethodList(req, rollback);
        }
        catch(Exception ex)
        {
            session.invalidate();
            ex.printStackTrace();
            System.out.println("webclient servlet test failed");
            throw new ServletException(ex);
        }
    }
    
    protected void setMethodList(HttpServletRequest req, int rollback)
    {
        PageData pageData = new PageData();
        List<Map<String,String>> methodList = pageData.getMethodList();
        req.setAttribute("methodList", methodList);

        List<String> rollbacks = new ArrayList<String>();
        for(int ct=0; ct<methodList.size(); ct++)
        {
            rollbacks.add((rollback == ct) ? "checked" : "");
        }
        req.setAttribute("rollbacks", rollbacks);
    }
}
PageData.java
package com.mydomain.ejb;

import java.io.Serializable;
import java.util.*;

public class PageData implements Serializable
{
    private static final long serialVersionUID = 1L;

    public PageData(){}
    
    public List<Map<String,String>> getMethodList()
    {
        List<Map<String,String>> methods = new ArrayList<Map<String,String>>();
        String spc = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
        String pls = spc + spc + spc + "+";

        this.addMethodRow(methods, "", "");
        this.addMethodRow(methods, "+->regist( )", pls);
        this.addMethodRow(methods, spc + "+->decorateInsert( )", pls);
        this.addMethodRow(methods, spc + "+->insert( )", pls);
        this.addMethodRow(methods, spc + "&nbsp;&nbsp;&nbsp;+->decorateInsert( )", pls);
        this.addMethodRow(methods, spc + "+--------------------------", "--->decorateInsert( )");
        this.addMethodRow(methods, spc + "+--------------------------", "--->insert( )");
        this.addMethodRow(methods, "", spc + "&nbsp;&nbsp;&nbsp;+->decorateInsert( )");
        this.addMethodRow(methods, spc + "+--------------------------", "--->requiresNewInsert( )");
        this.addMethodRow(methods, "", spc + "&nbsp;&nbsp;&nbsp;+->insert( )");
        this.addMethodRow(methods, "", spc + spc + "+->decorateInsert( )");
        
        return methods;
    }

    protected void addMethodRow(List<Map<String,String>> methods, String act, String sub)
    {
        Map<String,String> row = new HashMap<String,String>();
        row.put("act", act);
        row.put("sub", sub);
        row.put("rollback", Integer.toString(methods.size()));
        methods.add(row);
    }
}
index.jsp
<%@ page language="java" pageEncoding="UTF-8" %>
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.util.*,com.mydomain.ejb.*" %>
<jsp:include page="/servlet" />
<%
List<String> rollbacks = (List<String>)request.getAttribute("rollbacks");
List<Map<String,String>> methodList 
  = (List<Map<String,String>>)request.getAttribute("methodList"); 
%>
<html>
  <head>
    <title>Access Log</title>
    <style type="text/css">
      table.list, table  {font-size: 9pt;}
      table.list th {background:#FFCC66;}
      table.list td {background:#FFFFCC}
    </style>
  </head>
  <body>
    <form method="post">
      <table>
        <tr>
          <td>
            <table style="border: solid 1px;">
              <tr>
                <th>No.</th>
                <th></th>
                <th align="left" style="text-decoration: underline;">ActionImpl</th>
                <th style="text-decoration: underline;">SubActionImpl</th>
              </tr>
              <% for(int ct=1; ct<rollbacks.size(); ct++){ %>
              <tr>
                <td><%= ct %></td>
                <td align="right">
                  Rollback
                  <input type="radio" name="rollback" value="<%= ct %>" <%= rollbacks.get(ct) %>>
                </td>
                <td><%= methodList.get(ct).get("act") %></td>
                <td><%= methodList.get(ct).get("sub") %></td>
              </tr>
              <% } %>
              <tr>
                <td></td>
                <td align="right">
                  Commit
                  <input type="radio" name="rollback" value="0"  <%= rollbacks.get(0) %>>
                </td>
                <td colspan="3"></td>
              </tr>
            </table>
            <input type="submit" value="access">
          </td>
        </tr>
        <tr>
          <td>
            <table class="list">
              <tr>
                <th>No.</th>
                <th>Session ID<br /></th>
                <th>Class:Method</th>
              </tr>
              <% for(Message row : (List<Message>)request.getAttribute("messages")){ %>
              <tr>
                <td><%= row.getNo() %></td>
                <td><%= row.getText() %></td>
                <td><%= row.getBiko() %></td>
              </tr>
              <% } %>
            </table>
          </td>
        </tr>
      </table>
    </form>
  </body>
</html>


web.xmlファイルを作成します。

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 
  xmlns="http://java.sun.com/xml/ns/javaee" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
                      http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>  
  <servlet>
    <servlet-name>Servlet</servlet-name>
    <servlet-class>com.mydomain.ejb.Client</servlet-class>
    <load-on-startup>0</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Servlet</servlet-name>
    <url-pattern>/servlet</url-pattern>
  </servlet-mapping>
</web-app>
サーブレットで@EJBアノテーションを使用するためServlet2.5のスキーマを宣言します。

コンテナ管理トランザクションであそぶ - (8/10) -

ビルドする

ejb-jarファイル、warファイル、earファイル作成ビルドファイルを作成します。

ejb-module/pom.xml
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mydomain.ejb</groupId>
  <artifactId>ejb-module</artifactId>
  <packaging>ejb</packaging>
  <version>1.0</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-ejb-plugin</artifactId>
        <configuration>
          <ejbVersion>3.0</ejbVersion>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>javaee-api</artifactId>
      <version>5.0-2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>openejb-core</artifactId>
      <version>3.1.2</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>
webtest-client/pom.xml
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mydomain.ejb</groupId>
  <artifactId>webtest-client</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>javaee-api</artifactId>
      <version>5.0-2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>openejb-core</artifactId>
      <version>3.1.2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.mydomain.ejb</groupId>
      <artifactId>ejb-module</artifactId>
      <version>1.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</project>
build-ear/pom.xml
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mydomain.ejb</groupId>
  <artifactId>enterprise-bean-example</artifactId>
  <packaging>ear</packaging>
  <version>1.0</version>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-ear-plugin</artifactId>
        <version>2.4</version>
        <configuration>
          <modules>
            <ejbModule>
              <groupId>com.mydomain.ejb</groupId>
              <artifactId>ejb-module</artifactId>
            </ejbModule>
            <webModule>
              <groupId>com.mydomain.ejb</groupId>
              <artifactId>webtest-client</artifactId>
              <contextRoot>webtest-client</contextRoot>
            </webModule>
          </modules>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>com.mydomain.ejb</groupId>
      <artifactId>ejb-module</artifactId>
      <version>1.0</version>
      <type>ejb</type>
    </dependency>
    <dependency>
      <groupId>com.mydomain.ejb</groupId>
      <artifactId>webtest-client</artifactId>
      <version>1.0</version>
      <type>war</type>
    </dependency>
  </dependencies>
</project>


一括ビルドpomファイルを作成します。

build/pom.xml
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mydomain.ejb</groupId>
  <artifactId>build</artifactId>
  <packaging>pom</packaging>
  <version>1.0</version>
  <modules>
      <module>../ejb-module/pom.xml</module>
      <module>../webtest-client/pom.xml</module>
      <module>../build-ear/pom.xml</module>
   </modules>
</project>


コマンドプロンプトを開き一括ビルドpomファイルのあるフォルダに移動して一括ビルドpomファイルを実行します。

cd C:\mywork\build
mvn clean install
生成されたenterprise-bean-example-1.0.earを 『GLASSFISHインストールフォルダ/domains/domain1/autodeploy』フォルダーにコピーします。

コンテナ管理トランザクションであそぶ - (9/10) -

検証する

Webブラウザから検証ページ(『http://localhost:8080/webtest-client/』)にアクセスして動作を確認します。
検証用Webアプリの説明

  • ラジオボタンでロールバックするポイント、またはコミットを選択して 『access』 ボタンを押します。
    (メソッド処理の最後に例外が発生してロールバックします。)
  • セッションIDとセッションIDが登録されたクラスとメソッドの呼出しツリーが一覧に表示されます。
検証する
  1. ラジオボタンでNo.3の Rollback を選択して 『access』 ボタンを押します。

    No.2の処理(ActionImplクラスのdecorateInsert()メソッド)は
    TransactionAttributeType.REQUIRES_NEWが指定されているので
    No.3の処理(ActionImplクラスのinsert()メソッド)で例外が発生しても
    No.2の処理は登録されるはずなのですが登録されていません。
    これはNo.2の処理がActionImplクラスのregist()メソッドから呼出されているため
    regist()メソッドが呼出された段階でActionImplクラスのEntityManagerにトランザクションが設定されるようです。
    この時のトランザクションはregist()メソッドで指定されたTransactionAttributeType.REQUIRED(デフォルト)になり
    regist()メソッドの処理が終了するまでの間、このクラスのメソッドはTransactionAttributeTypeの指定に関わらず
    同一のトランザクションとして処理されるようです。

  2. ラジオボタンでNo.6の Rollback を選択して 『access』 ボタンを押します。

    No.6の処理(SubActionImplクラスのinsert()メソッド)はActionImplクラスのregist()メソッドで設定された
    トランザクションの参加することになるようで、No.6の処理で例外が発生すると
    ActionImplクラスのメソッドでの処理 (No.2~4)も一緒にロールバックされるようです。
    No.5の処理(SubActionImplクラスのdecorateInsert()メソッド)は
    TransactionAttributeType.REQUIRES_NEWが指定されているので、
    このメソッドが呼出された段階で SubActionImplクラスのEntityManagerに
    別トランザクションが設定されメソッド処理の終了時にコミットされているようです。
    そのためNo.5の処理はロールバックされず登録されています。

  3. ラジオボタンでNo.8の Rollback を選択して 『access』 ボタンを押します。

    No.8の処理(SubActionImplクラスのrequiresNewInsert()メソッド)で発生した例外は
    ActionImplクラスのregist()メソッドで catch しているので
    他のメソッドの処理は ロールバックされず登録されるようです。

  4. ラジオボタンでNo.1の Rollback を選択して 『access』 ボタンを押します。

    No.7の処理(SubActionImplクラスのdecorateInser()メソッド)は
    TransactionAttributeType.REQUIRES_NEWが 指定されているのですが
    No.6の処理(SubActionImplクラスのinsert()メソッド)から呼出されているため
    No.6の処理で設定されたトランザクション(ActionImplクラスのregist()メソッドで設定されたトランザクション)に
    参加することになり、No.1の処理で例外が発生するとActionImplクラスのメソッドでの処理 (No.2~4)と
    SunActionImplクラスのNo.6、No.7の処理はロールバックされるようです。
    No.5の処理(SubActionImplクラスのdecorateInsert()メソッド)は
    別トランザクションで処理されているので ロールバックせずに登録されるようです。
    No.8の処理(SubActionImplクラスのrequiresNewInsert()メソッド)は
    TransactionAttributeType.REQUIRES_NEWが 指定されたメソッドなので
    別トランザクションで処理されることからNo.8の処理、そしてこの処理から呼出される
    No.6、No.7の処理は ロールバックせずに登録されるようです。

  5. ラジオボタンで Commit を選択して 『access』 ボタンを押します。

    すべての登録処理が実行されます。

結果考察
  • Session Beanのフィールドに置かれたEntityManagerは Session Beanのインスタンスを通して
    最初に呼出されたメソッドの TransactionAttributeTypeに従ってトランザクションを開始するようだ。
    そしてそのメソッドの処理が終了するまでそのクラスのメソッドはTransactionAttributeTypeの指定に関わらず
    同一のトランザクションとして処理されるようだ。
  • 同じクラスの同じメソッドであっても呼出される条件によって参加するトランザクションが変わるようだ。
  • 1つのEntityManagerのインスタンスは1つのトランザクションを管理するようだ。

コンテナ管理トランザクションであそぶ - (10/10) -

おわりに

今更ながらコンテナ管理トランザクションを試してみてとても良い感じでした。
もっとはやく試していたらよかったとも思いました。
begin-commit-rollback をコーディングしなくてよくなるだけなのですが
たったこれだけのことでも一旦コーディングしなくてもよくなると
なんとかコーディングしないで済むようにソースコードを工夫しだしたりします。
ただ本格的に使うにはもうちょっと調査が必要かな?とも...
ステートレスセッションBeanとステートフルセッションBeanでは動作が少し違うような気もします。
ローカルオブジェクトとリモートオブジェクトでも違ったりするのかな?
もしかすると実行環境によっても動作に差があるかも???
このあたりは今後のテーマです。
今回は例外の生成にJavaEEのインターセプターを使っていたのですが
これがなんだかいまいちで...(あたしの理解不足でしょうか?)
AspectJ なんかを使うともうちょっと良い感じになるのではと思います。
EJBをJNDIからlookupしてViewページに渡すところも Seam なんかを使っても面白いのはないでしょうか。
参考
http://homepage3.nifty.com/rio_i/lab/ejb3/index.html
http://itpro.nikkeibp.co.jp/article/COLUMN/20060615/241006/
Sun GlassFish Enterprise Server v2.1.1 Administration Guide.pdf
ejb-3_0-fr-spec-ejbcore.pdf