エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(10) - CDIの概要

今回から数回に分けてCDI(Contexts and Dependency Injection)を紹介する。

CDIJava EE 6から追加されたDI(Dependency Injection、依存性注入)のための仕様である。 Java EE 7におけるCDIのバージョンは1.1であり、またJava EE 7から CDIはデフォルトで有効化されている。

DIはクラス間の依存関係を、コンテナ(CDIの実行環境)が解消する技術である。 依存関係を疎に保つことによって、実装や外部リソースの切り替えを容易にし、コードの独立性やテストのしやすさを高めることができる。

C(Contexts)とは、オブジェクトのライフサイクル(スコープ)を定義することで、コンテナが適切にオブジェクトのライフサイクルを管理することを指す。ライフサイクルの中であればオブジェクトの状態(ステート)を自由に変更できる。

たとえば、あるオブジェクトがHTTPセッション中に有効であると定義すれば、そのオブジェクトはHTTPセッションの開始時に自動でインスタンス生成され、HTTPセッションが終了すると自動で破棄される。プログラマが明示的にオブジェクトを破棄する必要はない。

CDIは、以下に挙げるような様々な機能を備えており、今後のJava EE の中核となる技術であると言える。

  • DI機能
    • スコープの指定
    • 限定子(Qualifier)によるBeanの特定
    • Producerによる動的なBean生成
    • Alternative によるデプロイ時のBean切り替え
  • JSP, EL式, JSF View での使用
  • Servlet, JSF, EJB, JPA など Java EE の各機能やサーバーリソースに対するDI
  • インターセプター
  • 自律型トランザクションCDI 1.1より)
  • デコレーター(メソッドフック)
  • イベント通知
  • サードパーティライブラリ(Springなど)への拡張ポイント提供(CDIに対応していないライブラリをCDI対象にする仕組み)

CDIの目指すところは、自作のクラスや、Java EEの多くの仕様、さまざまなフレームワークなどを、疎結合に保ちつつ型安全で統合することにあると思われる。

例えば、サーブレットからデータベースを更新する処理を作成する場合、データベースとの接続やトランザクションの開始・終了といった操作が必要だ。 java.sql.Connection を使用してデータベースの操作を行う場合、JDBCドライバーの登録やデータベースとの接続などの初期設定を行う必要がある。あるいは、コネクションプールを使用している場合、コネクションを取得するための手順が必要だ。
このような初期設定や呼び出し手順が必要なAPIを使う場合は、いつ・どこでこれらの手順を実行するかを検討する必要がある。

あるいは、 JPAEJB、ロガーなどを組み合わせて使用するなど、異なる技術要素やライブラリを使う場合には、それらを連携させるコードを書く必要があるだろう。

そのような場所に、CDIが利用できる。CDIを使用するように対応させれば、異なるもの同士を柔軟かつ疎結合で連携でき、利用者は詳細な設定手順を知ることなく、型や最低限のルールのみを知っていればよくなる。

サンプル

基本的なサンプルを紹介する。

CDIで管理されるオブジェクトを、この記事ではCDI Beanと呼ぶことにする(他にも、CDI管理Bean、管理Bean、あるいは単にBeanと呼ばれることもある)。

詳細は次回以降で解説するが、大まかに覚えておくべきルールは以下の2つだ。

  1. CDI Beanには、スコープアノテーションを付与する。
  2. CDI Beanを利用するには、@javax.inject.Inject アノテーションを付与したフィールド等を宣言する。

CDI Beanの作成

スコープが異なるが同じメソッドを持つ2つのCDI Beanを作成する。

/**
 * リクエストスコープBean
 */
@javax.enterprise.context.RequestScoped
public class RequestBean {
    
    private int count = 0;
    
    public void countUp() {
        count++;
    }
    
    public int getCount() {
        return count;
    }
    
    public String getHashCode() {
        return Integer.toHexString(this.hashCode());
    }
}
/**
 * セッションスコープBean
 */
@javax.enterprise.context.SessionScoped
public class SessionBean implements Serializable {
    
    private int count = 0;
    
    public void countUp() {
        count++;
    }
    
    public int getCount() {
        return count;
    }
    
    public String getHashCode() {
        return Integer.toHexString(this.hashCode());
    }
}

クラス宣言にスコープアノテーションがある以外は、ただのJavaのクラスである。

@RequestScopedはリクエスト単位に生成・破棄されるCDI Beanで、@SessionScoped はHTTPセッション単位に生成・破棄されるCDI Bean である。 名前からわかるとおり、基本的にCDIJava EEアプリケーション(特にWeb)をターゲットにした技術であり、Java EEに対応したアプリケーション上で稼動する必要がある。

また、まぎらわしいのだが、 RequestScoped などのアノテーションは同名のアノテーションが別パッケージに存在する(javax.faces.bean.RequestScoped など)。 これらは CDI導入以前に、JSF (Java Server Faces)で用意されたもので、用途なども似通っている。 CDIの方が汎用的であり、JSF の方のスコープアノテーションは将来的に非推奨となり、代わりにCDIを使うことが推奨されている。間違えないように注意されたい。

CDI Beanを利用する

CDI Beanを利用するオブジェクトとして、今回はサーブレットを使用する。 サーブレットや、JSPJSFなどもCDIが利用できるようになっている。

/**
 * CDIサンプル
 */
@WebServlet(name = "simpleExample", urlPatterns = {"/simpleExample"})
public class SimpleCdiExampleServlet extends HttpServlet {

    // CDI Beanのインジェクション
    @Inject
    private RequestBean rbean;
    @Inject
    private SessionBean sbean;
    
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        
        // カウントアップの実行
        rbean.countUp(); // 毎回初期化される
        sbean.countUp(); // 呼び出しのたびにカウントアップ
        
        try (PrintWriter out = response.getWriter()) {
            
            out.printf("RequestScope:Count=%s, Hash=%s\n", 
                       rbean.getCount(), rbean.getHashCode());
            out.printf("SessionScope:Count=%s, Hash=%s\n", 
                       sbean.getCount(), sbean.getHashCode());
        }
    }
}

ここでは先ほど作成した CDI Bean をフィールドに定義し、@Injectアノテーションを付与している。

doGetでは、2つのBeanのカウントアップを行い、テキストとしてブラウザに出力しているだけで、 CDI Beanのインスタンス生成や、セッション格納などは行っていない。 また、CDI Beanに付与されているスコープが何であるかも考慮不要だ。

このサーブレットをデプロイして実行すると以下のような出力を得る。

RequestScope:Count=1, Hash=2b29f747
SessionScope:Count=1, Hash=50206f88

リロードすると以下の出力を得る。

RequestScope:Count=1, Hash=6b7c1d46
SessionScope:Count=2, Hash=50206f88

@RequestScopedを指定したCDI Beanはリクエストごとに生成されるので、カウントは毎回1となるし、ハッシュも異なる。
一方、@SessionScoped を指定したCDI Beanは同一のオブジェクトが毎回設定されるので、カウントが増えていく。

HTTPセッションの制御を手軽かつ型安全に扱えているのがわかるだろう。

これらのソースや、実行環境は以下で提供している。

次回

次回はスコープやDIの方法をより詳細に見ていく。

[前多 賢太郎]