EJBコンテナが分散コンポーネントモデルから軽量なDIコンテナに変化してきた歴史を振り返る

十年一昔といいますが、文字通り一昔前の書籍ではJ2EEEJBコンポーネントはプロセスが分散化されたリモート呼び出しにより処理を行う分散コンポーネントとして説明されています。そして、残念ながら現状Java EE関連の日本語の書籍はこうした古い時代に書かれたものがほとんどとなっています。それゆえ、

  • 開発効率がきわめて悪い
  • 実行性能が悪い*1
  • 仕様がきわめて複雑で理解が大変

といった悪いイメージが定着してしまっているのではないかと思います。しかしながら、最新バージョンのJava EE6では、Spring、GuiceSeamなどのOSSの軽量コンテナのアイデアを取り込むことにより、以前とは比較にならないくらい開発効率が改善されているという事実があります。
ここでは、Hello WorldEJBの書き方を以前の古いバージョンから順次振り返りながら比較してみることで、EJBのプログラミングモデルがどのように変化してきたのかについて考えてみることにしたいと思います。古い仕様のEJBを使ったシステムをメンテナンスする必要のない幸運な人も、以前と比較して現在どのくらい便利になったのかを知ることは興味深いことだと思います。

EJB1.xの時代は純粋に分散コンポーネントの仕様だった(1998年〜2001年)

今では全く信じられないことですが、もともとEJBの仕様が登場してきた当時はEJBはCORBAやWebサービスと同じように別プロセスから呼び出される分散コンポーネントを作成するための仕様としてスタートしました。分散コンポーネントですから、POJOのような普通のクラスと違ってプログラマーが気軽に作成するようなものとは当然考えられておらず、一つのサブシステムのようにある程度複雑な責務をカプセル化した粒度の大きな単位のコンポーネントとして作成するものと想定されていたのです。それゆえ、

  • 事前にインターフェースを明確に定義しておく
  • ソースコードに手を加えずにxmlファイルで独立して設定を変えられるように柔軟性を持たせておく
  • ある程度呼び出しや作成が面倒でも目をつぶる
  • インスタンスプールなどサーバーのリソースを多く消費する

といった思想で仕様が設計されたのだと思います。この頃からコンポーネントの目的に応じてステーレスセッションBean、ステートフルセッションBean、エンティティBeanといった種類がありましたが、ここではEJBとしてもっとも単純なステートレスセッションBeanを定義することを考えます。
まず、EJB1.1では以下の2種類のインターフェースを定義する必要があります。

  • リモートホームインターフェース(EJBを生成するためのファクトリとして使うためのインターフェースを定義)
  • リモートコンポーネントインターフェース(クライアントから呼び出し可能な業務処理のインターフェースを定義)

まず、ホームインターフェースは規約に従って以下のように定義します。これは決まりきった形なのですが、EJBHomeを継承し、create()メソッドの戻り値型はリモートインターフェースとなっており、チェック例外であるCreateExceptionとRemoteExceptionを必ずthrowsすると宣言する必要があることに注意してください。

package hello.ejb;

import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;

public interface HelloHome extends EJBHome {
    Hello create() throws CreateException, RemoteException;
}

一方、リモートインタフェースの方は以下のようになります。EJBObjectを継承し、ビジネスメソッドに対応するメソッドを定義しますが、ここでもRemoteExceptionの送出を宣言することが必須です。また、パラメーターや戻り値はRMI互換となるため、シリアライズ可能型など制約があります。

package hello.ejb;

import java.rmi.RemoteException;
import javax.ejb.EJBObject;

public interface Hello extends EJBObject { 
    String sayHello(String name) throws RemoteException;
}

次に、EJBの実装クラスは以下のように作成します。

package hello.ejb;

import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class HelloBean implements SessionBean { // 間違えやすいのですが、Hello自体はimplementsしない。
    
    private SessionContext context;

    private String message;
    
    public void setSessionContext(SessionContext aContext) {
        context = aContext;
    }

    public void ejbActivate() {}

    public void ejbPassivate() {}

    public void ejbRemove() {}

    // EJBのインスタンスが生成されるときに一度だけ呼び出される初期化処理
    public void ejbCreate() {
        // ejb-jar.xmlで定義された環境エントリからメッセージ文字列を取得する。
        Context ic = null;
        try {
            ic = new InitialContext();
            message = (String)ic.lookup("java:comp/env/message");

        } catch (NamingException ex) {
            throw new RuntimeException(ex);
        } finally {
            if (ic != null) {
                try {
                    ic.close();
                } catch (NamingException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }
 

   // リモートコンポーネントインターフェースで宣言したビジネスメソッドを定義する

    public String sayHello(String name) {
        return message + " " + name;
    }

}

以上のEJBの実装クラスでポイントは以下の通りです。

  • ビジネスメソッドはリモートコンポーネントインターフェースのメソッドに対応しているが、直接Helloインターフェースを実装してはならない。
  • SessionBeanインターフェースを実装しなくてはならない。(このためejbXX()メソッドなど多くのコールバックメソッドを空でもよいので実装する必要がある。)
  • コンストラクタではなくてejbEreate()で初期化処理を行う。(ここでは例としてメッセージ文字列を標準のxmlファイルからルックアップしてEJBのフィールドに格納する例を示しています。)

そして、さらに以上の3つのクラスやインターフェースに対応してejb-jar.xmlというデプロイメント記述子を以下のように作成する必要があります。

<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar version="2.1" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd">
    <display-name>EJB2App-ejb</display-name>
    <enterprise-beans>
        <session>
            <display-name>HelloBeanSB</display-name>
            <ejb-name>HelloBean</ejb-name>
            <home>hello.ejb.HelloHome</home>
            <remote>hello.ejb.Hello</remote>
            <ejb-class>hello.ejb.HelloBean</ejb-class>
            <session-type>Stateless</session-type>
            <transaction-type>Container</transaction-type>
            <env-entry>
                <env-entry-name>message</env-entry-name>
                <env-entry-type>java.lang.String</env-entry-type>
                <env-entry-value>こんにちは</env-entry-value>
            </env-entry>
        </session>
    </enterprise-beans>
    <assembly-descriptor>
        <container-transaction>
            <method>
                <ejb-name>HelloBean</ejb-name>
                <method-name>*</method-name>
            </method>
            <trans-attribute>Required</trans-attribute>
        </container-transaction>
    </assembly-descriptor>
</ejb-jar>

以上で、ようやくHello EJBの作成と定義が終わりデプロイできる状態となりました。次に、このEJBをWeb層のサーブレットから呼び出すには以下の手順に従う必要があります。

  • HelloHomeをJNDIからルックアップ
  • HelloHomeに対してcreate()メソッドを呼び出すことでHelloインタフェースのEJBオブジェクト(リモートProxy)を生成
  • 生成したHelloに対してビジネスメソッドを呼び出す

まず、HomeインターフェースのルックアップはJNDIのAPIを利用して行いますが、これだけで相当面倒なコーディングが必要なため、ベストプラクティスとして
J2EEパターンではService Locatorパターンを使うことが推奨されています。例えば、以下のようなルックアップ専用のクラスを作成します。

package hello.web;

import hello.ejb.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;

public class HelloServiceLocator {

    public static HelloHome lookupHelloHome() {
        Context context = null;
        try {
            context = new InitialContext();
            // 実際にはHomeの参照をキャッシュした方がベター
            return (HelloHome) PortableRemoteObject.narrow(
                    context.lookup("java:global/EJB2App/EJB2App-ejb/HelloBean!hello.ejb.HelloHome"), HelloHome.class);
        } catch (NamingException ex) {
            throw new RuntimeException(ex);
        } finally {
            if (context != null) {
                try {
                    context.close();
                } catch (NamingException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }

なお、実は以上の実装には可搬性の上でも性能の上でも問題があります。なぜなら、アプリケーションサーバーに固有のJNDI名を使って直接ルックアップしているからです。JavaEE6でこうしたJNDI名の標準化が行われるまではJNDI名の文字列としてサーバーごとにそれぞれ別々の形式を使わなくてはならなかったのです。そこで、ちょっと面倒なのですが、当時として正しくEJBを参照するにはweb.xmlにて以下のようにリソース参照を定義するのが推奨される方法です。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
...
    <ejb-ref>
        <ejb-ref-name>ejb/hello</ejb-ref-name>
        <ejb-ref-type>Session</ejb-ref-type>
        <home>hello.ejb.HelloHome</home>
        <remote>hello.ejb.Hello</remote>
        <ejb-link>HelloBean</ejb-link>
    </ejb-ref>
</web-app>

そうすると、ServiceLocatorでルックアップしている部分は、以下のように記述できます。

    return (HelloHome) PortableRemoteObject.narrow(
        context.lookup("java:comp/env/ejb/hello"), HelloHome.class);

WebアプリケーションやEJBなどJ2EEコンポーネントごとに環境エントリーと呼ばれるローカルなJNDIツリーが存在し、web.xmlEJB参照として定義したEJB参照名がこのローカルなJNDIツリーにバインドされます。環境エントリーはjava:comp/env/というコンテキストにバインドされます。そうすることで、アプリケーション開発者はソースコードを修正することなくJNDIの参照先を変えられるので便利であるとされました。ちなみに、このようにEJB参照を使ってアクセスすることで上記ではというタグの指定を使うことで、同一earファイル中であればJNDIを経由せずに直接EJBを参照する最適化が可能になっています。
次に、EJBObjectを生成して呼び出すのですが、呼び出しもチェック例外の処理を行うと大変なので、J2EEパターンではBusiness Delegateと呼ばれるコンポーネントカプセル化すべきとされました。以下はBusiness Delegateの実装例です。

package hello.web;

import hello.ejb.Hello;
import hello.ejb.HelloHome;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.RemoveException;

public class HelloDelegate {

    private HelloHome helloHome;

    public HelloDelegate(HelloHome helloHome) {
        this.helloHome = helloHome;
    }

    public String sayHello(String name) {
        Hello hello = null;
        try {
            hello = helloHome.create(); // EJBオブジェクトを生成
            return hello.sayHello(name); // ビジネスメソッドの呼び出し。

        } catch (CreateException ex) {
            throw new RuntimeException(ex);
        } catch (RemoteException ex) {
            throw new RuntimeException(ex);
        } finally {
            if (hello != null) {
                try {
                    hello.remove();
                } catch (RemoteException ex) {
                    throw new RuntimeException(ex);
                } catch (RemoveException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }
}

そして、これらをコンポーネントを使ってサーブレットから以下のように起動します。

package hello.web;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {

    protected void sayHelloRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            HelloDelegate delegate = new HelloDelegate(HelloServiceLocator.lookupHelloHome());
            String message = delegate.sayHello("Test");

            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet HelloServlet</title>");  
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>" + message + "</h1>");
            out.println("</body>");
            out.println("</html>");
        } finally {            
            out.close();
        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        sayHelloRequest(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        sayHelloRequest(request, response);
    }
}

いかがでしょうか。詳細の理解はとにかく、実際、昔のJ2EEがあきれる程複雑だったという話のネタとして理解していただければ十分です。実際にここでやっていることはHello Worldの文字列を生成するEJBを呼びだすだけなのですが、これだけのコードや設定ファイルの記述が必要だったのです。しかも、これでももっとも仕様が単純なステートレスセッションBeanしか使っていませんでした。しかし、実際の開発ではエンティティBeanなどを使用するとさらに複雑になりますし、性能面などを考えるとリモート呼び出しのオーバーヘッドを避けるために、ファサードDTOなどのオブジェクトを作成し、値をまとめて転送したりする工夫が必要となり、さらにいろいろなクラスを作成することになります。

EJB2.xでは驚くべきことにさらに仕様が複雑化(2002年〜2006年)

J2EE1.4で標準化されたEJB2.1までは、基本的にこの複雑なプログラミングモデルに変化はありませんが、性能の問題を回避するために新たにローカルインターフェースという概念が登場しました。ローカルインターフェースを使うことでRMIなどの制約やオーバーヘッドがなくなるので多少は単純になるのですが、基本的な複雑さは変わらず、また、リモートとローカルを適切に使い分けるなど仕様はさらに巨大化しました。たとえば、ローカルホームをルックアップする処理は以下のように記述できます。リモートの場合との違いはルックアップの結果をPortableRemoteObjectを使ってナローイングする代わりに単純にキャストしている点です。

    public static HelloLocalHome lookupHelloLocalHome() {
        Context context = null;
        try {
            context = new InitialContext();
            return (HelloLocalHome)context.lookup("java:comp/env/ejb/helloLocal"); // 単純なキャストでOK

        } catch (NamingException ex) {
            throw new RuntimeException(ex);
        } finally {
            if (context != null) {
                try {
                    context.close();
                } catch (NamingException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }

一方、コンポーネントインターフェースの呼び出しは以下のように記述できます。ローカルインターフェースのためRemoteExceptionの処理は不要になっています。

package hello.web;

import hello.ejb.HelloLocal;
import hello.ejb.HelloLocalHome;
import javax.ejb.CreateException;
import javax.ejb.RemoveException;

public class HelloLocalDelegate {

    private HelloLocalHome helloLocalHome;

    public HelloLocalDelegate(HelloLocalHome helloLocalHome) {
        this.helloLocalHome = helloLocalHome;
    }

    public String sayHello(String name) {
        HelloLocal hello = null;
        try {
            hello = helloLocalHome.create();
            return hello.sayHello(name);

        } catch (CreateException ex) {
            throw new RuntimeException(ex);
        } finally {
            if (hello != null) {
                try {
                    hello.remove();
                } catch (RemoveException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }
}

なお、実際にEJB2.1の仕様で最高に複雑なのは今のJPAに相当するエンティティBeanの部分なのですが、ここではあまりにも複雑なため説明しません。興味のある方は昔のEJBの解説書や仕様書を調べてみてください。

EJB3.0におけるプログラミングモデルの劇的な単純化(2006年〜2009年)

こうした面倒な開発に対処するためにSpringやSeasar2のような軽量コンテナが発明された経緯がありますが、標準仕様としても、Java EE5とともに登場したEJB3の仕様でPojoやDIなど軽量コンテナの利点を採用することで開発の簡易化が図られました。実際、今回のHello EJBEJB3.0で作成するには以下のような手順で済みます。
まず、普通にHello EJBの機能を示すビジネスインターフェースをごく普通にJavaのインターフェースとして作成します。先ほどのEJB2.1までの例と違い、EJBLocalなどのインターフェースを継承したり、Homeインターフェースを定義したりする必要はありません。

package hello.ejb;

public interface Hello {    
    String sayHello(String name);
}

そしてEJBの実装クラスでは以下のように普通にインターフェースを実装すれば済みます。普通のPojoとの違いは@Statelessというアノテーションがついている点だけですね。

package hello.ejb;

import javax.annotation.Resource;
import javax.ejb.Stateless;

@Stateless
public class HelloBean implements Hello { //ビジネスインターフェースを普通に実装

    @Resource
    private String message;
        
    // ビジネスメソッド
    public String sayHello(String name) {
        return message + " " + name;
    }
}

以前のバージョンではJNDIを使って環境エントリーからメッセージ文字列をルックアップしていたのですが、@Resourceアノテーションにより、自動的にインジェクションできます。ただし、この場合以下のようなejb-jar.xmlの定義が必要になります。

<?xml version="1.0" encoding="UTF-8"?>

<ejb-jar xmlns = "http://java.sun.com/xml/ns/javaee" 
         version = "3.0" 
         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">
    <display-name>EJB3App-ejb</display-name>
    <enterprise-beans>
        <session>
            <display-name>HelloBeanSB</display-name>
            <ejb-name>HelloBean</ejb-name>
            <ejb-class>hello.ejb.HelloBean</ejb-class>
            <env-entry>
                <env-entry-name>hello.ejb.HelloBean/message</env-entry-name>
                <env-entry-type>java.lang.String</env-entry-type>
                <env-entry-value>こんにちは</env-entry-value>
            </env-entry>
        </session>
    </enterprise-beans>
</ejb-jar>

サーブレットからの呼び出しも、従来の方式が全く嘘のように簡単に以下のように記述できます。ビジネスインターフェースを経由して普通に呼び出せますので、もはや、Service LocatorやBusiness Delegateなどのパターンを適用する必要はありません。

package hello.web;

import hello.ejb.Hello;
import java.io.IOException;
import java.io.PrintWriter;
import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {

    @EJB
    private Hello hello; // EJBをDIによりインジェクション
    
    protected void sayHelloRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        String message = hello.sayHello("Test");
  
        try {
            out.println("<html>");
            out.println("<head>");
            out.println("<title>Servlet HelloServlet</title>");  
            out.println("</head>");
            out.println("<body>");
            out.println("<h1>" + message + "</h1>");
            out.println("</body>");
            out.println("</html>");
        } finally {            
            out.close();
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        sayHelloRequest(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        sayHelloRequest(request, response);
    }
}

EJB3.1でようやく軽量コンテナの生産性に追いついた(追い越した?)(2010年〜)

Java EE5で登場したEJB3.0により、PojoとDIを使ってEJBを開発できるようになり、このように表面的にはSpringなどと同様に開発できるようになったように見えます。しかし、実際には

  • 問題1 EJB開発には重たいアプリケーションサーバーが必要
  • 問題2 インメモリーの埋め込みサーバーの利用が標準化されておらず、単体試験の自動化が困難
  • 問題3 EJBを利用する場合にはEARファイルへのパッケージ化が必要
  • 問題4 EJBは実は軽量ではなく、インスタンスプールなどサーバーリソースを消費する
  • 問題5 以前のコンポーネントモデルを踏襲しており、JNDIの名前空間なども統合されていない

最初の二つの問題はJava EEサーバーが重くてテスト不能というイメージはもう過去の話かもしれない - 達人プログラマーを目指してでも説明したように、現在ではGlassfishなどの軽量のサーバーが登場したことで既に解決しています。ここでは、残りの問題がJavaEE 6のEJB3.1でどのように解決されているのか見ていきます。

EJB3.1はwarファイル中に格納できる

EJB3.1仕様のEJBはwarモジュール中のWEB-INF/classesディレクトリーに格納したり、WEB-INF/libフォルダ中のjarファイルに含めることができます。したがって、EJBを利用するという理由のためだけで独立したejb-jarファイルとearファイルの作成を行う必要はなくなります。以前Java EEのearファイル形式はMavenの天敵 - 達人プログラマーを目指してで書いたようにearファイルのビルドは面倒なところがあり、またクラスローダーの階層も複雑だったため、これだけでも大幅な簡易化と開発生産性の向上が期待できます。
なお、warファイルにEJBを格納した場合はJAX-RPCやEJB2.1までのエンティティBeanといったレガシー機能は利用できませんが、リモート呼び出しなどを含めてその他の機能は利用することができます。

本物のPojoをDI管理可能にする管理Beanの概念の登場

EJBは見かけ上はPojoの形で開発できますが、実際には従来通りの重量コンポーネントであることには変わりがありません。実際ステートレスセッションBeanであれば、EJBごとにサーバーにインスタンスのプールが構築されるなど、それなりのリソースを消費します。しかし、実際にはEJBコンテナの機能は不要だけれども、DIなどのサービスを利用したいことが多くあります。ここも従来JavaEE 5までは軽量コンテナに対して不便な点だったのですが、JavaEE 6では管理Beanという概念が定義されています。特に、@javax.annotation.ManagedBeanをPojoに付けることで

  • @Resourceや@EJBを使ったDI
  • @PostConstruct、@PreDestoryなどのライフサイクルコールバック
  • インターセプタの適用によるAOP

などの基本的なDIコンテナのサービスを受けることができます。仕様では従来のEJBは管理Beanの一種であるとして定義されているため、図示すると以下のような関係となります。

なお、大混乱に陥っているJavaEE 6のアノテーションに関する使い分けについて - 達人プログラマーを目指してでも書いたように、非常に勘違いしやすいのですが、faces-config.xmlか@javax.faces.bean.ManagedBeanで定義されるJSFの管理Beanの概念は、@javax.annotation.ManagedBeanで定義される管理Beanとは別の概念のため混同しないように注意が必要です。@javax.annotation.ManagedBeanで定義されたBeanをJSFの管理Beanとして利用することはできません。
実際、以下のような形で管理Beanを定義すると

package hello.web;

import javax.annotation.ManagedBean;

@ManagedBean("sample")
public class SampleBean {
    
    public String sample() {
        return "test";
    }
}

サーブレットや他のEJB(あるいは一般に管理Bean)に対してインジェクションすることが可能です。

@WebServlet(name = "HelloServlet", urlPatterns = {"/hello"})
public class HelloServlet extends HttpServlet {

    @Resource
    SampleBean sample;

... 
}
CDIが有効化されたモジュールでは結局ほとんどすべてのBeanが管理Beanに

今まで説明してきたJava EE6の管理Beanですが、実際には以下のような大きな制約を持っています。

  • ライフサイクルが単純なprototype Bean的なスコープしか持たない
  • モジュールごとにJNDIの名前空間が分断されてしまう(EL式などで扱いにくい)
  • JSFの管理Beanとして利用できない

まず、最初の問題ですが、@javax.annotation.ManagedBeanで定義されたBeanはちょうどSpringのprototypeスコープのBeanと同様にJNDIからルックアップしたり、インジェクションされる度に別々のインスタンスが生成されます。シングルトン的に共有したり、セッションなどの特定のスコープで保持することができません。
2番目の問題はwarのみを使うプロジェクトでは問題とならないのですが、earを使うような場合に問題となります。例えば、warファイル中のサーブレットからejb-jarに格納された管理Beanをインジェクションするためには、わざわざ以下のようにルックアップ先を指定する必要があるのです。

    @Resource(lookup="java:app/EJBjar名/sample")
    SampleBean sample;

そして、一番大きな問題はその名前にも関わらずJSFの管理Beanとして利用できないことです。
もともと、JBoss Seamはその名前の通り、EJBJSFの管理BeanなどあらゆるPojoをBeanとして扱い、全レイヤーを継ぎ目なくアクセスするという思想があります。Seam作者であるGavin King氏が中心となって策定されたCDI(JSR-299)の仕様では、この思想にしたがって一部例外を除き結局ほとんどすべてのPojoを管理Beanとして統一的に扱うことができるようになっています。さらに、セッションや会話などのスコープも定義することができるようになっています。
JavaEE 6では各モジュール中にbeans.xmlファイルが含まれているとCDIの機能が有効になることになっていますが、この場合は結局以下のような感じになるということですね。

CDIを有効にした場合、EJBもその他のPojoも以下のように統一的にアクセスできるようになるだけでなく、さまざまなスコープ中に格納してEL式から参照することも簡単にできるようになります。なお、この場合にJSR-330で別途規定されているDIのアノテーションを利用することになります。*2

@WebServlet(name = "HelloServlet", urlPatterns = {"/hello"})
public class HelloServlet extends HttpServlet {

    // EJBもPojoも同様に統一した形でインジェクションできる。
    @Inject
    private Hello hello;
    
    @Inject
    private SampleBean sample;
...
}

この場合、EJBは従来のようにコンポーネントと考えるのではなく、トランザクションやプールなどの特殊なアスペクトのかかった管理Beanに過ぎないと考えた方がしっくりきます。そして、従来のようなJNDI名を使った環境エントリのようなJava EEの複雑な仕掛けを一切忘れることが可能になります。
そうすると、従来のEJBコンポーネントモデルがまったく蔑ろにされているようにも思われるのですが、

  • 従来のシステムとの互換性を考える必要がある。
  • 要件によりモノリシックなWebアプリケーションではなく、疎結合コンポーネントモデルが適切

などの場合には、CDIのオプションをはずして、従来通りのコンポーネントモデルで開発することができます。このあたりは、アーキテクトの判断で個別に適切に使い分ける必要があると思います。*3

まとめ

以上、この10年間のJ2EEおよびEJBのプログラミングモデルの進化について簡単に振り返りました。こうした進化が一昔といわれる僅か10年の間に急激に起こったため、ちょっとでも技術から離れてしまっているとこのような劇的な変化があったということに気づきもしないかもしれません。特に、プログラミングをしない上流のSEやコンサルの人は、J2EE関連のシステムに関わっていてもこうした事実をまったく知らないという人もいるのではないでしょうか。そして、いまだに当時の古いJ2EEの仕様に基づいて作成された社内標準フレームワーク上でアプリケーションを作成しなくてはならないというチームも多くあるのではないでしょうか。
一口にEJBと言っても、ここで紹介したようにまったくプログラミングの方法や考え方がバージョンによって異なります。特にJava EE6で利用できるようになったCDIは従来のコンポーネント中心のモデルと比較してパラダイムシフトといえるくらいの大きな変化と言えます。
Java EEは重くて使えないと信じている人も一度最新のCDIを試してみるとその生産性の高さを実感できると思います。
なお、(私もすべて完読できていないのですが)参考書を紹介させていただきます。
- The Java EE 6 Tutorial

Java EE 6 Tutorial, The: Basic Concepts (Java Series)

Java EE 6 Tutorial, The: Basic Concepts (Java Series)

  • 作者: Eric Evans, Ian Gollapudi, Devika Haase, Kim Srivathsa, Chinmayee Jendrock
  • 出版社/メーカー: Prentice Hall
  • 発売日: 2010/08/24
  • メディア: ペーパーバック
  • クリック: 12回
  • この商品を含むブログ (1件) を見る
以下はCDIを管理Beanとする前提で説明が書かれた古くからあるJSFの参考書の改訂版です。
Core JavaServer Faces (Core Series)

Core JavaServer Faces (Core Series)

EJB3.1の本も出版されています。
Enterprise JavaBeans 3.1: Developing Enterprise Java Components

Enterprise JavaBeans 3.1: Developing Enterprise Java Components

以下はGlassfishを使ったJava EE6の全般的な解説がされた良書ですが、残念ながらCDIに関する記述が欠けています。
Beginning Java EE 6 with GlassFish 3 (Expert's Voice in Java Technology)

Beginning Java EE 6 with GlassFish 3 (Expert's Voice in Java Technology)

また、CDIに関してはJSR-299の仕様書の他、JBoss Weldのマニュアルも参考になります。
http://seamframework.org/Weld/Documentation
以下のサイトも参考になりますね。
Seam3の概要 | Think IT(シンクイット)
あと、Twitter上で#javaeejpというハッシュタブ上でJavaEEに関する議論をしたいと思っていますので、ご興味のある方は是非参加してください。最新バージョンにかかわらず、ここで紹介した古いバージョンの改善方法や移行方法などを含めて議論できればと思います。

*1:多くの場合リモート呼び出しやエンティティBeanが原因

*2:もともとはCDIでは全然別のアノテーションが使われる予定だったが、最終的にJSR-330のアノテーションを利用することで統一された経緯がある。Java EE 6 に調和する依存性注入

*3:それにしても、これだけの仕様の変化に追随して、開発時に適切に判断する必要があるアプリケーションアーキテクトとは実に大変で割に合わない仕事だとは思いますね。