CDIでアプリケーション設定をインジェクション

この記事は Java EE Advent Calendar 2013 の12/8分の記事です。

昨日はHirofumi Iwasakiさんの WebLogicをGlassFishの商用版として使う方法 です。
明日は@emaggameさんです。よろしくお願いします。


CDICommons Configrationと組み合わせて、アプリケーション独自の設定をインジェクションすることを考えてみます。

Java EE のアプリケーションでは、アプリケーションが使用する独自コンフィグ情報(よくあるのは接続先IPなど)を以下のような手段で作成することができます。

web.xmlに定義する

  • context-paramに定義してServletContext経由でロード

XMLの定義

<context-param>
  <param-name>key</param-name>
  <param-value>value</param-value>
</context-param>

Javaで読み込み

ServletContext context = getServletConfig().getServletContext();
String val = context.getInitParameter("key");

ejb-jar.xmlに定義する

  • env-entryに定義して@ResouceでEJBにインジェクション

XMLの定義 

<env-entry>
  <env-entry-name>key</env-entry-name>
  <env-entry-type>java.lang.String</env-entry-type>
  <env-entry-value>value</env-entry-value>
</env-entry>

@Resourceでインジェクション

@Resouce(name = "key")
private String parameter;

独自XMLに定義し、読み込んだコンフィグ情報をシングルトン等に格納する

  • 実装省略。Web上に色々と例があると思います。

Java EE で用意されているweb.xmlejb-jar.xmlのコンフィグ化の仕組みでは、XMLであってもデータの階層構造を示しにくい形式であり、アプリケーションのコンフィグ化にはまだまだ独自ファイルを作成してロードしている人も多いと思います。しかし、シングルトンはテスト時に取得する値を書き換えににくい、書き込みを許すとグローバル変数化する等、その依存性の強さからあまり好まれる実装ではありません。ここでは、CDIを使ってパラメータがインジェクションできないか考えてみます。

やりたいこと

コンフィグ例

階層構造を表したいので、XMLにしてみる。クラスパス中にファイルは置く。

<?xml version="1.0" encoding="UTF-8"?>
<distination>
  <machine>
    <name>Distination Machine 1</name>
    <ipaddr>192.168.0.1</ipaddr>
  </machine>
</distination>
コンフィグファイルのパス指定

どこにAP独自のコンフィグファイルを置いてあるのか、何らかの方法で指定することが必要です。ハードコーディングするのも、コンフィグの位置をweb.xml等から取得するのも嫌なので、CDIのProducerメソッドで指定してみました。

public class ConfigPathResolver {
    @Produces @ConfigPath
    public String configFilePath() {
        return "servlet/appConf.xml";
    }
}
コンフィグのインジェクション

以下のように自作CDI修飾子 @Config に引数を与えてコンフィグのキーを指定します。
以下の例ではmachine.ipaddrと指定すると、前述したコンフィグから文字列『192.168.0.1』が取得できる。

@Inject @Config("machine.ipaddr")
private String ipaddr;

実装する

@Configを作る

@Nonbindingを忘れるとアノテーションのパラメータも修飾子の対象に含まれ、@Config("key1")と@Config("key2")は別の修飾子として扱われるので注意。

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, PARAMETER, METHOD, TYPE})
public @interface Config {
  @Nonbinding String value() default "";
}
@ConfigPathを作る

こちらは非常にシンプルです。

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

コンフィグの読み込みをProducerメソッドで作る

javax.ejb.Singletonを使って実装してみました。java.inject.Singletonでは@Startupが使えないので、アプリケーションのデプロイ時にコンフィグがロードできず、初回アクセス時までロード処理ができません。

ファイルのパスは前述したように、CDIのProducerの仕組みを使って注入しています。また、コンフィグの処理自体はCommons Configurationを使いました。

@Singleton
@Startup
public class ConfigProducer {

  @Inject @ConfigPath
  private String filepath;
 
  private Configuration configration;

  @PostConstruct
  public void initialize() {
    try {
      configration = new XMLConfiguration(file path);
    } catch (ConfigurationException e) {
      throw new InjectConfigrationException(e);
    }
  }

  @Produces @Config
  public String getString(InjectionPoint ip) {
    Config configAnnotation = ip.getAnnotated().getAnnotation(Config.class);
    String configKey = configAnnotation.value();

    if ("".equals(configKey)) {
      throw new IllegalArgumentException(
        "クラス " + ip.getMember().getDeclaringClass().getName() + " の" +
        "フィールド " + ip.getMember().getName() + " にコンフィグキー名が未設定です。" +
        "@Config(key)の形式でインジェクションするプロパティのキーを設定してください。");
    }

    String val = configration.getString(configKey);
    if (val == null) {
      throw new IllegalArgumentException(
        "クラス " + ip.getMember().getDeclaringClass().getName() + " の" +
        "フィールド " + ip.getMember().getName() + " において" +
        "@Configに指定されたキー " + configKey + " が ファイル" + filepath + " に存在しません。");
    }

    return val;
  }
}

試しに作ってみたソースコードgithubにあげてあります。

最後に

今回の設定ファイルの位置をCDIの@Producesを使って注入したように、何らかの汎用的な設定をJavaコードで書いてCDIで注入するやり方はもっと色々なことができそうです。Producerメソッドの仕組みを使うと、GuiceのAbstructModuleのように、Java EE 環境でも設定をJavaで書くことができます。XML地獄が良いか、アノテーションだらけになるか、Javaコードで設定も書くか、悩ましいところです。

参考にした記事

Commons Configrationについては以下の記事を参考にしました。
ありがとうございます。
http://d.hatena.ne.jp/daisuke-m/searchdiary?word=%2A%5Bcommons%5D