CDIとiBATIS2.3を組み合わせる

ネイティブクエリのみをシンプルに実行したい場合は、まだまだiBATIS(MyBATIS)を使う機会もあると思います。ここではiBATIS2.3とJava EE6のContest and Dependency Injection(CDI)を組み合わせて、JPAのEntityManagerのように、iBATISのSqlMapClientをインジェクションする方法を考えます。

まず、iBATISの設定ファイル(SqlMapConfig.xml)を読み込んで、初期化する部分のコードです。

// import文はポイントなる部分だけ抜粋
import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.enterprise.inject.Produces;

@Singleton
@Startup
public class SqlMapClientGenerator {
  private static final Logger logger
      = Logger.getLogger(SqlMapClientGenerator.class);
	
  private SqlMapClient sqlMapClient;
	
  @PostConstruct
  public void initialize() {
    try (Reader reader = Resources.getResourceAsReader("SqlMapConfig.xml")) {
      sqlMapClient = SqlMapClientBuilder.buildSqlMapClient(reader);
    } catch (IOException e) {
      logger.error("iBATISの設定ファイル読み込みに失敗しました");
    } 
    logger.info("iBATISの初期化が完了しました");
  }

  @Produces
  @PostgreSQL
  public SqlMapClient getSqlMapClient() {
    return sqlMapClient;
  }
}

ポイントは2つあります。

1. EJBのシングルトンセッションビーンを使う

Java EE6に含まれるEJB3.1より、新たなセッションビーンであるシングルトンセッションビーンが加わりました。名の通り、@Singletonと付けるだけで、シングルトンの実装が作成できる便利な機能です。設定ファイルのロード結果の管理はシングルトンにぴったりなので、使ってみます。

@Startupはデプロイ時に初期化させるシングルトンセッションビーンの機能です。これを付与しないと、@PostConstructでアノテートした初期化処理がデプロイ時ではなく、初回アクセス時に実行されます。

注意しなければいけないのは、@Singletonはjavax.ejb.Singleton(EJB)とjavax.inject.Singleton(CDI)の2種類があることです。

本来、トランザクショナルな処理でない設定ファイルのロードにEJBのSingletonを使うのは良くないかもしれません。CDIのSingletonはあくまで"スコープの一種"の扱いのようなので、@Startupのようなデプロイ時の初期化方法が見当たらず、EJBを使用しました。

2. CDIのProducerの仕組みを使う

  @Produces
  @PostgreSQL
  public SqlMapClient getSqlMapClient() {
    return sqlMapClient;
  }

この部分のコードにはアノテーションが2つ付与されています。
2つのアノテーションが1セットになって、後述する@Injectによるインジェクションを実現しています。

まず@Producesですが、これはインジェクションしたいオブジェクトを生成する(Produce)メソッドに付与する、CDIアノテーションです。
Java EE6チュートリアル*1では、以下のようなシチュエーションで使用することを紹介しています。

  1. CDIのBean(引数なしコンストラクタを持つクラス)ではないオブジェクトをインジェクションしたいとき
  2. ランタイム(実行時)に複数の種類の具象クラスをインジェクションしたいとき
  3. コンストラクタだけでない、独自のカスタム初期処理が必要なとき

今回のケースでは、主に1と3に当てはまります。SqlMapClientインタフェースの実装は引数なしコンストラクタで初期化できませんし、初期処理もSqlMapClientBuilderクラスで行っていて、正にカスタム初期処理のようです。

今回の処理では、SqlMapClientインタフェースのJavaDoc*2に『A thread safe client』と書かれていたので、@PostConstructの初期処理で生成したオブジェクトを毎回使い回して返却する処理をプロデューサメソッドとして実装しています。

次に@PostgreSQLです。
このアノテーションCDI仕様で規定されている、限定子(Qualifier)と呼ばれる自作アノテーションです。実際には以下のようなコードで自分でアノテーションを作ります。

@Qualifier
@Target({ TYPE, METHOD, PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
public @interface PostgreSQL {}

限定子*3とは、インジェクション対象の同じ型を持つオブジェクトが複数ある場合に、どの実装をインジェクションするのか識別するために使うものです。

ポイントは@javax.inject.Qualifierで、このアノテーションPostgreSQLアノテーションの定義に付けると、限定子としてCDIの実行環境が認識してくれます。今までのJava EEアノテーションそのものが多く提供されてきましたが、CDIではアノテーションに付けるアノテーションが提供されています。

この例では1つのSqlMapConfig実装を返すプロデューサは1メソッド(getSqlMapClient)しかないため、@PostgreSQLの実装は必須ではありません。しかし、複数のSqlMapConfig.xmlをロードして異なるデータソースに接続する場合には、接続先によってインジェクション対象のSqlMapClient実装を選択するために限定子が必要になってきます(例えば@Oracleを追加する など)。今回の例では、コードを見たときに直感的にPostgreSQLへの接続が理解しやすいので、限定子を作成しています。

これらの仕組みを作ることにより、以下のようにEntityManager風にSqlMapClientをインジェクションすることができます。

@Inject @PostgreSQL
SqlMapClient sqlMapClient;

/** 人の一覧を返すビジネスメソッド */
public List<Person> referPersons() throws BusinessException {
    try {
      return (List<Person>)sqlMapClient.queryForList("Person.selectAll");
    } catch (SQLException e) {
      logger.error("人の一覧が取れなかった", e);
      throw new BusinessException(e);
    }
}

プロデューサメソッドに付与した@PostgreSQLは、インジェクションポイントにも同様に付与します。プロデューサとインジェクションポイント両方に同じ限定子を付与することで、括り付けが定義できます。

古い古いと言われるiBATISも、ちょっとは今風な雰囲気になったかと思います。

*1:Java EE6 Tutorial : http://docs.oracle.com/javaee/6/tutorial/doc/gkgkv.html

*2:iBATIS API http://ibatis.apache.org/docs/java/dev/com/ibatis/sqlmap/client/SqlMapClient.html

*3:Java EE6 Tutorial http://docs.oracle.com/javaee/6/tutorial/doc/gjbck.html