読者です 読者をやめる 読者になる 読者になる

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

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

JavaEE7をはじめよう(12) - CDI Beanのインジェクション

JavaEE CDI

前回は、CDIの定義方法を解説した。 今回は、CDI Beanをインジェクションする方法を紹介する。

インジェクションが可能なクラスの種類

インジェクションを行うには、@Injectアノテーションを使用する。

@Injectが使用可能なクラス、すなわちCDI Beanをプロパティなどに注入できるクラスには以下のものがある。

  • 自作のCDI Bean - CDIコンテナによって生成されたものに限る
  • EJB
  • Servlet, Filter, Listener, JSP
  • JSFのバッキングBean, View, コンバータ, バリデータ(最後の2つは Java EE 7から)
  • JAX-RSのRESTリソースクラス
  • Bean Validationのカスタムバリデータ(Java EE 7から)

基本的に、Java EEの開発で使用するほとんどのクラスに適用可能と考えてよいだろう。

ただし、自分でコンストラクタを呼んで生成したオブジェクトに対してはインジェクションが行われないことに注意が必要だ。 このため、自作したCDI Beanについてはコンストラクタを呼ばす、CDIコンテナに生成を任せる必要がある。 その他のJava EE関連のクラスについては、Java EEのバージョンに応じて対象範囲が異なることに注意する必要がある。

また、@RequestScoped, @SessionScoped, @ConversaionScopoedサーブレットコンテナがHTTPリクエストを処理中の場合のみ有効である。そのため、リモート呼び出しのEJBなどでは、これらのスコープのCDI Beanはインジェクションできない。 CDIコンテナの起動時にインジェクション不可能なプロパティが見つかった場合、CDIコンテナはアプリケーションのデプロイを失敗させる。

インジェクションの指定方法

インジェクションの指定方法は3種類ある。

方法1:フィールドインジェクション

フィールドに@Injectを指定する方法である。対象のフィールドはprivateでもよい。

@Dependent
public class HogeBean {
    @Inject
    private Bean bean;
    ///
}
方法2:コンストラクタインジェクション

コンストラクタの引数にCDI Beanを注入する方法である。

@Dependent
public class HogeBean {
   private Bean bean;
      
  @Inject
   public HogeBean(Bean bean) {
       // BeanもCDI Beanである必要がある
       this.bean = bean;
   } 
}  

前回の記事で説明したように、基本的にCDI Beanはデフォルトコンストラクタを持つ必要がある。しかし、上のHogeBeanクラスのように、コンストラクタの引数がインジェクション対象になっている場合は、CDI Beanにすることができる。

方法3:setter インジェクション

setterに@Injectを指定する方法である。

@Dependent
public class HogeBean {
    private Bean bean;
     
   @Inject
    public setBean(Bean bean) {
        this.bean = bean;
    } 
}

インジェクションの指定には3つの方法があるが、どの場合でもインジェクションを行うタイミングは、注入する側のクラス(上の例ではHogeBean)のコンストラクタを呼び出すタイミングになる。

特に理由がないなら、フィールドを見ればどのフィールドがインジェクション対象かがわかるため、フィールドインジェクションを使用するのがよいだろう。

インジェクションの実行タイミング

実際にインジェクションが行われるタイミング、すなわちCDIコンテナがCDI Beanのコンストラクタを呼び出すタイミングは、指定したスコープによって異なる。

例えば、アプリケーションスコープではアプリケーションの起動時、セッションスコープではセッションの開始時、リクエストスコープではリクエストの開始時となる。

いずれの方法でもインジェクションが終わった後に@PostConstructアノテーションが付与された初期処理メソッドが呼び出される。CDI Beanに対して何らかの初期処理を行いたいなら、@PostConstructを付与したメソッドを使用するのが適切なやり方である。

なお、実際にインジェクションされるのは、CDI Beanそのものではなく、CDI Beanを継承したクライアントプロキシと呼ぶオブジェクトである。詳細については、本記事の最後の節で解説する。

異なるスコープのCDI Beanの多段注入

ここまで述べてきたように、CDI Beanに対してもインジェクションを行うこと、すなわち多段注入が可能となっている。その際にはスコープが異なっていても問題なく動作する。

コード例を使って説明しよう。

前々回の記事で取り上げたサーブレットCDIのサンプルを修正し、サーブレットにインジェクションするBeanに対して、さらにインジェクションを行うようにする。
その際、@RequestScopedのBeanからは@SessionScopedのBeanを、@SessionScopedのBeanからは@RequestScopedのBeanをインジェクションするようにする。ついでに、@DependentのBeanをそれぞれのBeanにインジェクションする。

@DependentのBeanのコードは次の通りである。

@Dependent
public class DependentBean implements Serializable {
    
    public String getHashCode() {
        return Integer.toHexString(this.hashCode());
    }
}

@RequestScoped@SessionScopedのBeanのコードは次の通り。
それぞれのBeanでお互いを@Injectすることに加えて、上記の@DependentなBeanも@Injectしている。

@RequestScoped
public class RequestBean {

    @Inject
    private SessionBean sbean;

    @Inject
    private DependentBean depBean;

    public String getHashCode() {
        return Integer.toHexString(this.hashCode());
    }

    public String format() {
        return "this(Request):" + getHashCode() 
                + " Session:" + sbean.getHashCode() 
                + " Dependent:" + depBean.getHashCode();
    }
}
@SessionScoped
public class SessionBean implements Serializable {

    @Inject
    private RequestBean rbean;

    @Inject
    private DependentBean depBean;

    public String getHashCode() {
        return Integer.toHexString(this.hashCode());
    }

    public String format() {
        return "this(Session):" + getHashCode() 
                + " Request:" + rbean.getHashCode() 
                + " Dependent:" + depBean.getHashCode();
    }
}

そして、3つのBeanをサーブレットにインジェクションする。

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

    // CDI Beanのインジェクション
    @Inject
    private RequestBean rbean;
    @Inject
    private SessionBean sbean;
    @Inject
    private DependentBean depBean;
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        try (PrintWriter out = response.getWriter()) {
            out.println(rbean.format());
            out.println(sbean.format());
            out.println("DependentBean on Servlet:" + depBean.getHashCode());
        }
    }
}

実行結果は、以下のようになる。

1回目

this(Request):77ceecf6 Session:1436e3b5 Dependent:44c26299
this(Session):1436e3b5 Request:77ceecf6 Dependent:814d2cb
DependentBean on Servlet:14e3719e

2回目

this(Request):6554df8b Session:1436e3b5 Dependent:616b47f9
this(Session):1436e3b5 Request:6554df8b Dependent:814d2cb
DependentBean on Servlet:14e3719e

リクエストスコープのBean内のセッションスコープのBeanは、セッション単位で保持されているため、2回のハッシュ値は同じになっている。逆に、セッションスコープのBean内のリクエストスコープのBeanはリクエストごとにハッシュ値が違うので毎回生成されている。

また、@DependentスコープのBeanは、インジェクションしたBeanのライフサイクルにしたがっていることがわかる。すなわち、サーブレットはアプリケーション内でインスタンスが1つのため、サーブレット@DependentスコープのBeanをインジェクションすると、アプリケーションスコープとして振舞う。

実行サンプルは以下で確認可能だ。

http://java-ee-example.herokuapp.com/java_ee_example/

インジェクションされるオブジェクトの正体

インジェクション時に実際にプロパティに設定されるのは、CDI Bean自身のオブジェクトではなく、クライアントプロキシと呼ぶ、動的に生成したオブジェクトである。

クライアントプロキシはインジェクション対象のCDI BeanをCDIコンテナが継承して作成したクラスである。 クライアントプロキシは実際のメソッドの実行をフックし、実行スレッドごとに適切なBeanをCDIコンテナから探し出して、本来のメソッドを実行する。クライアントプロキシを作成する都合上、CDI Beanのクラス定義にはfinal修飾子を指定できない。

クライアントプロキシを使う理由は、スコープの異なるオブジェクトを効率的にかつ安全にインジェクションするためだ。

例えば、アプリケーションスコープのCDI BeanにリクエストスコープのCDI Beanをインジェクションする場合、アプリケーションスコープのBean はアプリケーション内で1つだけだが、そのプロパティであるリクエストスコープのBeanはリクエストごとに異なるインスタンスを作成する必要がある。 リクエストのたびにインジェクションを行うと、効率が落ちるだけでなく、スレッドセーフに関する考慮も必要になってしまう。

クライアントプロキシを導入することで、インジェクションの回数を減らし、かつスコープに応じた動的なCDI Beanの取得をCDIコンテナに任せることができるようになる。 CDIコンテナは、スコープに応じた Bean の生成やキャッシュの管理、スレッドセーフの考慮などをしているので、異なるスコープのインジェクションを効率的かつ安全に行える。

ただし、クライアントプロキシを注入するのは@Dependent以外のスコープを指定した場合のみである。@Dependentの場合はクライアントプロキシではなく、当該クラスのオブジェクトが直接設定される。
これは@Dependentを指定した場合はスコープが同一になり、実行効率やスレッドセーフの考慮をする必要がないため、クライアントプロキシ方式を採用するメリットがないからである。

前節の動作例の中のセッションスコープのBean内のリクエストスコープのBeanはリクエストのたびにハッシュコードの値が変わるため、新しいオブジェクトを生成しているように見える。 しかし、実際にはクライアントプロキシによって、そのような振る舞いを行うように見せかけているだけである。

またSessionBeanクラスの中ではrbean.getHashCode()を実行しているが、このときrbeanにインジェクションされているのはRequestBeanのクライアントプロキシである。 クライアントプロキシのgetHashCodeメソッドは、CDIコンテナを使用してスコープの内容に応じてRequestBeanインスタンスの取得または生成を行い、元のgetHashCodeメソッドを呼び出すように動作する。

SessionBeanRequestBeanはお互いのクラスのプロパティを持ち合っている。 インジェクションが再帰的に行われて、無限にインジェクションが起きると思われるかもしれないが、そのような心配はない。 インスタンス生成時にCDIコンテナが行うのは、プロパティにクライアントプロキシを設定するだけで、プロパティの内部には触れないためである。

詳細はこちらなどを参照。

CDIを普通に使う分には実装の詳細を知る必要はあまりないが、CDIを使用したアプリケーションで例外発生時のスタックトレースに、HogeBean_$$WeldProxy.someMethod()のような自分で作ったクラスでないものが現れるのはこのような実装に基づくことを知っておくとよいだろう。

まとめ

前回と今回の2回に分けて、インジェクションの基本的な部分について説明した。

スコープ指定を利用することで、セッション領域の管理を自動的かつ型安全に行える。 ただし、やみくもに色々なクラスにスコープを設定すると収集がつかなく恐れがある。

CDIを利用するなら、クラスの役割分担やスコープの設定の仕方など、設計の時点でどのようにCDIを利用するかを検討しておく必要があるだろう。

次回は、継承や実装が絡む場合や、インジェクションするオブジェクトを切り替える方法について解説する。

[前多 賢太郎]