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

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

JavaEE7をはじめよう(14) - Producerの効果的な利用方法

前回の記事では、インジェクションするクラスの候補が複数ある場合の様々な対処方法を紹介した。

今回は、Producerの効果的な利用方法を2つ紹介する。

リソースパラメータをProducerフィールドで一元管理する

まずはProducerフィールドを使って、データソースや永続性コンテキストなどのリソースを効果的にインジェクションする方法を紹介する。

CDIを使用しない場合やJava EE5以前の場合、リソースをインジェクションするには@Resource@PersistenceContextのような、目的に合わせたアノテーションを設定する必要がある。これらのアノテーションにはJNDI名のようなパラメータも必要となる。

// JPAのコンテナ管理永続性コンテキスト
@PersistenceContext(unitName = "default")
private EntityManager em;

// Java EE7から追加された Concurrency Utility
@Resource(lookup = "concurrent/__defaultManagedExecutorService")
private ManagedExecutorService executor;

上記のようなアノテーションの記述が様々なクラスに散らばるのはパラメータの変更時の影響が大きく、好ましくない。

このような場合、Producer フィールドを使用することでパラメータを一元管理できる。具体的なコードで説明しよう。

@RequestScoped
public class ResourceProducer {

    @Produces
    @PersistenceContext(unitName = "default")
    private EntityManager defaultEm;

    @Produces
    @Resource(lookup = "concurrent/__defaultManagedExecutorService")
    private ManagedExecutorService defalutExecutor;
}

上のコードでは、まず@PersistenceContext@Resourceによって、defaultEmおよびdefalutExecutorフィールドに対してリソースのインジェクションが行われる。これらのフィールドには@Producesを指定してあるため、該当リソースを利用したいクラスでインジェクションできる。

このように、@Producesはフィールドに対しても設定可能である。メソッド@Producesを指定した場合にはメソッドの戻り値がインジェクションされる値になるが、フィールドに指定した場合はフィールドの値がインジェクションされる値になる。 そのため、フィールドの値をインジェクションさせたい場合は、フィールドの値を返すProducerメソッドを作成するよりも、Producerフィールドを使用した方が容易だ。

上記のProducerがあれば、あらゆるクラスで、対象のリソースを@Injectでインジェクションできる。

@Transactional // 通常永続性コンテキストはトランザクション内で使う
public class Some {
    
    @Inject // Producerフィールド経由でインジェクション
    private EntityManger em;

    @Inject
    private ManagedExecutorService executor;

}

こうしておけばパラメーターに変更があった場合も、ResourceProducerのみを修正すればよくなる。また、インジェクションを使用する側は、@Injectアノテーションのみを使用すればよく、用途にあわせたアノテーションを使用する必要が無くなる。

限定子とProducerを組み合わせる

限定子とProducerを組み合わせることで、 Stringやコレクションのような既存の型に何らかの値を設定してインジェクション可能としたり、設定の異なる複数インスタンスをインジェクションすることが可能になる。

コレクションクラスをインジェクションする

例えば、設定ファイルやメッセージファイルといった、特定のプロパティファイルを読み込んでMapインスタンスを返す例を考えてみる。

まずは、限定子を用意する。

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Messages {}

次に、Producerメソッドを作成し、用意した限定子を指定する。

@ApplicationScoped
public class MessageFactory {
    
    @ApplicationScoped
    @Produces
    @Messages
    public Map<String, String> readMessageFile() {
        // クラスパス上のメッセージファイルを読込み、Mapに変換する。
        Properties prop = new Properties();
        try {
            prop.load(getClass()
          .getResourceAsStream("/messages.properties"));
        } catch(IOException e) {
            throw new RuntimeException(e);
        }
        
        return prop.entrySet().stream().collect(
                Collectors.toMap(e -> e.getKey().toString(), 
                                 e -> e.getValue().toString()));
    }
}

インジェクションを行う側は、限定子付きで@Injectを行えばよい。

public class SomeClass {
    @Inject @Messages // メッセージファイルの内容がインジェクションされる
    private Map<String, String> messages;

ここでは限定子をProducerメソッドに指定することで、特定の処理を行ったコレクションをインジェクションできるようにしている。

またreadMessageFileメソッドでは、メッセージファイルの内容が変わらないことを前提に、スコープを@ApplicationScopedとしている。これにより、読み込みは一度だけで済む。

上記のサンプルやソースコードこちらで公開している。

同一の型の複数リソースを扱う

限定子とProducerを組み合わせる方法を活用することで、同一の型の複数リソースも容易に扱えるようになる。 例えば、複数のデータベースを扱う場合(EntityManager複数ある場合)には、以下のように限定子を用意すればよい。

@Dependend
public class EMProducer {
    // 通常使用するスキーマ
    @Produces
    @PersistenceContext(unitName = "application")
    private EntityManager applEm;
    
    // 特別な場合のみに使用するスキーマ
    @Produces
    @SystemSchema // ソースは省略
    @PersistenceContext(unitName = "system")
    private EntityManager systemEm;

}

上記の例で、appEmにはあえて限定子を指定していない。これは、applEmで宣言しているスキーマはデフォルトであると想定しているためで、デフォルトの場合は限定子なしでインジェクションさせたいためだ。

つまり、デフォルトのスキーマを使用するなら、

  @Inject
  EntityManger em;

だけでよく、システムスキーマを使用するなら、

  @Inject
  @SystemSchema
  EntityManager em;

のようにする。
スキーマを常に明示させたいなら、デフォルトのスキーマにも@ApplicationSchemaのような限定子を宣言すればよい)。

Producerと限定子を組み合わせる方法の利用局面

Producerと限定子を組み合わせる方法は様々な用途で使用できるだろう。

先ほどは、設定ファイルの読み込みと、複数データベーススキーマの取り扱いを例に取り上げたが、他にもロガーなどが考えられる。 ロガーの取得・初期化方法はログのライブラリによって様々だが、Producerメソッドを使うことで一元化できる。 また、ログレベルに応じてログファイルを分割したいなど複数のロガーを取り扱わなければならない状況にも、限定子と組み合わせることで対応できる。

アノテーションを多用するので、最初は戸惑うかもしれないが、文字列によるパラメータ指定と比べると、スペルミスなどはコンパイルエラーとなるし、型のチェックも行ってくれるので安全である。

まとめ

今回まで CDI の依存性注入に関する基本的な内容を説明してきた。 次回以降は CDI の便利な機能や、依存性注入以外の機能を紹介していく予定である。 次回は、CDI で定義されている特殊なスコープである会話スコープについて紹介する。

[前多 賢太郎]