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 で定義されている特殊なスコープである会話スコープについて紹介する。
[前多 賢太郎]