火曜日 8 28, 2012

jrunscript as a cross platform scripting environment

ちょっとした問題解決や作業の効率化をする時には、何らかのスクリプト言語を使ってツールを作ることがよくあります。私はshスクリプトが得意なので、UNIX環境を前提とする場合は、ほとんどの場合shスクリプトで済ませてしまいますが、作成したツールをお客様に使ってもらうときには、やはりWindows環境でも動作できる必要があります。

shスクリプトで簡単に表現できるfind、grep、sed、awkなどをWindows環境でどう置き換えたらいいかを考えると、頭を抱えてしまいます。そもそも、短時間に簡単に問題解決をするためにスクリプト言語を選択したのに、実現したいことが簡単にできなければ意味がありません。Windows環境にCygwinをインストールすれば同じshスクリプトをWindowsでも利用できますが、お客様にCygwin環境を整えてもらうのはちょっと大げさになるので、お願いするのは気が引けます。そんなとき、JDKに含まれるjrunscriptをベースにJavaScriptベースでのツール開発はこのような悩みを解決する1つの解になります。jrunscriptは以下のような方にお勧めなツールです。

  • Windows環境とUNIX環境の両方で動くコマンドラインツールを作りたい
  • find、grep、sed、awkなどを組み合わせてshスクリプトを書くのは得意だが、Windows Script Hostはスキルがない
  • Javaプログラミングは得意である
  • ツールは他の人にも使ってもらいたいので、事前にインストールしなければならないソフトウェアや環境設定は極力少なくしたい(スクリプトファイルと使用方法のメモだけの小さなファイルだけ渡せばよいというのが理想)

最近では、サーバサイドでもJDK 6を使用している場合が多くなっていますし、事務作業用のPCでは、開発グループの方でなくともJDK 6はPCにインストールしている場合が多いです。もしインストールされていなくても、JDKだけならインストールをお願いするのはそれほど面倒ではないでしょう。その意味では、jrunscriptをベースにツール開発するのは良い選択肢だと思います。

それでは、jrunscriptを使ってJavaScriptベースでツール開発するときの注意点について、少し掘り下げて議論してみようと思います。

1) Windows環境とUNIX環境の両方で使えるようにするには?

これはそれほど難しい作業ではありません。例えば、作成するツールの大部分のロジックはJavaScript形式でmytool.jsというファイルで用意した場合、このスクリプトをjrunscriptコマンド経由で実行するUNIX環境用のshスクリプトと、Windows環境用のbatファイルをそれぞれ追加で用意するだけです。

mytool.sh (UNIX用):

#!/bin/sh
bindir=$(cd $(dirname $0) && pwd)
case "`uname`" in
  CYGWIN*) bindir=`cygpath -w "$bindir"`
           ;;
esac
jrunscript "${bindir}/mytool.js" "$@"
mytool.bat (Windows用):
@echo off
set bindir=%~dp0
jrunscript "%bindir%mytool.js" %*

UNIX用のshスクリプトの方はCygwin環境の場合も考慮しています。このように、起動部分のスクリプトが準備できれば、後は本体のjsスクリプトの開発ではほとんどUNIXとWindowsの違いを意識せずに共通のロジックを定義していくことができます。

2) jrunscriptではcat, cp, find、grepなどが使える

jrunscriptでは、UNIX環境での標準的なコマンドに似せた組込関数がいくつか用意されています。

従って、UNIX系のshスクリプトに慣れている方であれば、ある程度UNIXコマンドを扱うのと同じような感覚で、ロジックを組み立てることができます。例えば、「srcディレクトリ内の全てjavaソースファイルについて、enumを使用しているjavaソースを調べる」ということをするには、以下のようなスクリプトを定義すればよいことになります。
find('src', '.*.java', function(f) { grep('enum', f); });

ただし、全てのUNIXコマンドが用意されているわけではなく、また、同じコマンドがあっても機能的に十分ではない場合がありますので、ある程度自分で有用な関数を用意してあげる必要はあります。例えば、組込関数cp(from, to)は、コピー元とコピー先が共にファイルでなければならないシンプルな実装しかありません。UNIX環境で簡単に記述できる

$ cp -r src/* tmp/
のようなディレクトリ丸ごとコピーには対応していません。しかしながら、先ほど紹介したfind()関数を使えば、cp -rに似たリカーシブ・コピーの関数を簡単に用意することができます。
function cpr(fromdir, todir, pattern) {
    if (pattern == undefined) {
        pattern = ".*";
    }
    var frdir = pathToFile(fromdir).getCanonicalPath();
    find(fromdir, pattern, function(f) {
        // relative dir of file f from 'fromdir'.
        var relative = f.getParentFile().getCanonicalPath().substring(frdir.length() + 1);
        var dstdir = pathToFile(todir + "/" + relative);
        if (!dstdir.exists()) {
            // Create the destination dir for file f.
            mkdirs(dstdir);
        }
        // Copy file f to 'dstdir'.
        cp(f, dstdir + "/" + f.getName());
    });
}
javaのファイルI/OのAPIは、Windows環境の場合でもパス区切りに"/"を使っても問題ないため、上記で示したロジックの範囲においてはUNIXとWindowsを意識することはありません。

また、もう一つの注意は、exec(cmd)関数です。例えば、jarコマンドを以下のように実行すると、日本語出力の部分が文字化けしてしまうことが分かります。

$ jrunscript
js> exec("jar xvf example.jar")
  META-INF/ ?????¬???????μ???B
 META-INF/MANIFEST.MF ???W?J???????μ???B
  com/ ?????¬???????μ???B
  com/example/ ?????¬???????μ???B
 com/example/Bar.class ???W?J???????μ???B
  com/example/dummy/ ?????¬???????μ???B
com/example/dummy/dummy.txt ?????o???????μ???B
com/example/dummy.properties ?????o???????μ???B
 com/example/Foo.class ???W?J???????μ???B
また、exec()関数から実行するコマンドが標準エラー出力に出力を行う場合、コマンドの実行が途中で止まってしまうことがあるという問題があります。特にWindows環境では標準エラー出力のI/Oバッファが小さいようで、問題が発生しやすいです。例えば、以下のようなBATファイルを用意し、

errmsg.bat:

for /L %%i in (1,1,50) do echo "Error Message count = %%i" 1>&2
jrunscriptからexec()関数で実行してみると、ループの18回目ぐらいでコマンドの実行が止まってしまうことが確認できると思います。
C:\tmp>jrunscript -e "exec('errmsg.bat')"

C:\tmp>for /L %i in (1 1 100) do echo "Error Message count = %i"  1>&2

C:\tmp>echo "Error Message count = 1"  1>&2

        :

C:\tmp>echo "Error Message count = 18"  1>&2   ← 止まる
実際に以下のようにしてexec()関数の実装を確認してみると、やはり、実行したコマンドの出力については標準出力しか読み取っていない実装になっており、その出力の扱いもDataInputStreamを使っていることが文字化けの原因と考えられます。
$ jrunscript
js> this["exec"].toString()

function exec(cmd) {
    var process = java.lang.Runtime.getRuntime().exec(cmd);
    var inp = new DataInputStream(process.getInputStream());
    var line = null;
    while ((line = inp.readLine()) != null) {
        println(line);
    }
    process.waitFor();
    $exit = process.exitValue();
}
この問題を回避するには、実行するコマンドの標準出力と標準エラー出力の両方をマルチスレッドで処理する修正版のexec()関数をスクリプトの中に用意し、exec()関数をオーバライドしてしまえば回避できます。以下が修正したexec()関数の例です。
function exec(cmd) {
    var process = java.lang.Runtime.getRuntime().exec(cmd);
    var stdworker = new java.lang.Runnable(
        {run: function() { cat(process.getInputStream()); }});
    var errworker = new java.lang.Runnable(
        {run: function() { cat(process.getErrorStream()); }});
    new java.lang.Thread(stdworker).start();
    new java.lang.Thread(errworker).start();
    return proc.waitFor();
}
文字化け問題の解決にはビルトイン関数のcat()を使用しています。関数cat()はInputStreamReaderクラスを使用した文字化けしない実装であるため、以下のようにシンプルに修正版を実装することができます。

3) JavaScript以外の言語を使用するのはどうか?

JavaScriptはJavaと同じようにシンタックスが冗長なため、できればJavaScript以外のより表現力豊かな、Ruby、Groovy、Scalaなどを使ってツール開発したいところです。しかし、これらスクリプト言語のランタイムのライブラリは10MBを越えてしまうため、開発チーム以外の利用者への配布を前提とした場合はやはりJavaScript以外の言語の使用は控えたいところです。数KBのスクリプトのために、十数MBのJARを同梱するのはちょっとバランスが悪いですしね。早く、JREやJDKの標準インストールだけで任意のスクリプト言語が自由に使えるようになるといいですね。

月曜日 8 13, 2012

Running BTrace custom script with GlassFish V3

GlassFish V3でBTraceのカスタムスクリプトを使用しようとすると、以下のようなNoClassDefFoundErrorが出て、正しく実行できないことがあります。

致命的: GRIZZLY0038: HTTP Processing error.
java.lang.NoClassDefFoundError: MyTracer
	at com.sun.enterprise.web.pwc.connector.coyote.PwcCoyoteRequest.$btrace$MyTracer$m(PwcCoyoteRequest.java)
	at com.sun.enterprise.web.pwc.connector.coyote.PwcCoyoteRequest.(PwcCoyoteRequest.java)
	at com.sun.enterprise.web.connector.coyote.PECoyoteConnector.createRequest(PECoyoteConnector.java:283)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:205)
	at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:174)
	at com.sun.grizzly.http.ProcessorTask.invokeAdapter(ProcessorTask.java:828)
	at com.sun.grizzly.http.ProcessorTask.doProcess(ProcessorTask.java:725)
	at com.sun.grizzly.http.ProcessorTask.process(ProcessorTask.java:1019)
	at com.sun.grizzly.http.DefaultProtocolFilter.execute(DefaultProtocolFilter.java:225)
	at com.sun.grizzly.DefaultProtocolChain.executeProtocolFilter(DefaultProtocolChain.java:137)
	at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:104)
	at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:90)
	at com.sun.grizzly.http.HttpProtocolChain.execute(HttpProtocolChain.java:79)
	at com.sun.grizzly.ProtocolChainContextTask.doCall(ProtocolChainContextTask.java:54)
	at com.sun.grizzly.SelectionKeyContextTask.call(SelectionKeyContextTask.java:59)
	at com.sun.grizzly.ContextTask.run(ContextTask.java:71)
	at com.sun.grizzly.util.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:532)
	at com.sun.grizzly.util.AbstractThreadPool$Worker.run(AbstractThreadPool.java:513)
	at java.lang.Thread.run(Thread.java:680)
Caused by: java.lang.ClassNotFoundException: MyTracer not found by org.glassfish.web.glue [288]
	at org.apache.felix.framework.ModuleImpl.findClassOrResourceByDelegation(ModuleImpl.java:787)
	at org.apache.felix.framework.ModuleImpl.access$400(ModuleImpl.java:71)
	at org.apache.felix.framework.ModuleImpl$ModuleClassLoader.loadClass(ModuleImpl.java:1768)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
	... 19 more

どうやらこの原因は、OSGiフレームワークのfelixとの兼ね合いによる問題のようです。felixの設定ファイルは、3.1.1まではglassfish/osgi/felix/conf/config.propertiesにあったのですが、3.1.2からはEquinoxと統合され、glassfish/conf/osgi.propertiesにあります。最新のGlassFish 3.1.2.2でosgi.propertiesの内容を確認すると、以下のようにBTrace関連のライブラリはデフォルトでfelixの管理外でクラスが読まれるようになっていますが、BTraceスクリプトも同じようにfelix管理外となるようにしなければならないようです。

glassfish/conf/osgi.properties:

org.osgi.framework.bootdelegation=${eclipselink.bootdelegation}, \
                                  com.sun.btrace, com.sun.btrace.*, \
                                  org.netbeans.lib.profiler, org.netbeans.lib.profiler.*

BTraceスクリプトのパッケージ名をbootdelegationに追加しても良いのですが、環境設定に手をいれると後々困ることもあるので、簡単に対応するには、BTraceスクリプトのパッケージ名の方をbootdelegationに列挙されているパッケージ名のいずれかになるように変更してしまってもよいでしょう。例えば、BTraceスクリプトのパッケージ名をcom.sun.btrace.scriptsと宣言してしまいます。

MyTrace.java:

package com.sun.btrace.scripts;
         :
@BTrace public class MyTrace {
         :

後は修正したスクリプトをbtracecでコンパイルすれば、実行中のGlassFishサーバにスクリプトをアタッチできます。

$ btracec MyTracer.java

$ jps
80075 Jps
79806 glassfish.jar

$ btrace 79806 com/sun/btrace/scripts/MyTracer.class

ただし、GlassFish本体のクラス群をトレースしたいときは、btraceコマンドでは手遅れだったりするため、domain.xmlのJVMオプションで起動時から組み込まれるようにする必要がある場合もあります。

glassfish/domains/domain1/config/domain.xml:

<jvm-options>-javaagent:/Users/foo/java/tool/btrace-1.2.1/build/btrace-agent.jar=script=/Users/foo/java/btscripts/com/sun/btrace/scripts/MyTracer.class</jvm-options>

水曜日 6 02, 2010

Java EE 6: Understanding Contexts and Dependency Injection (CDI), Part 2

まず最初にお知らせですが、6月1日より日本においてもサン・マイクロシステムズは日本オラクルに統合されました。私自身もオラクル社におけるコンサルティング・サービスの一員となります。基本的にはこれからもJavaアプリケーション・プラットフォームに関わるコンサルティングを続けていく予定ですので、今後ともよろしくお願いいたします。

それからもう一つ。前回のパート1を書いてからこのエントリを書くまでの間にNetBeans 6.9はRC1、RC2の2つアップデートがリリースされました。6.9ベータではCDI関連のメニューが一部英語のままだった部分が日本語化されていますので、まだ6.9ベータをご使用の方は6.9RC2をお試しください。

さて、Java EE 6仕様に含まれるCDI解説の第2回です。前回のエントリ(パート1)では、Java EE 5のリソース・インジェクションと比較して、主にインジェクション・ポイントの柔軟性、依存関係にあるオブジェクトの提供法の柔軟性について解説しました。また、基本的にCDIにおけるインジェクション・ポイントにおけるオブジェクトの提供方法は必ず一意に解決できなければならないことも述べました。前回までのCDIの例では、インジェクション・ポイントにおける提供オブジェクトの解決がオブジェクトの型だけでできる場合のみを示していました。しかし、実際にはオブジェクトの型だけでインジェクション・オブジェクトを決定することができない場合が存在します。CDI解説の2回目となる今回は、インジェクション・オブジェクトの候補が複数存在する場合に、その中の1つの候補に限定する方法について解説します。

複数のインジェクション・オブジェクトの候補から1つを選択する方法

以下にオブジェクトの型だけではインジェクション・オブジェクトを解決できない典型的な例を示します。

import javax.ejb.Stateless;
import javax.inject.Inject;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject
    CustomerDAO customerDAO;   // DAO実装1と実装2のどちらかに決められない。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
public class XmlCustomerDAOImpl implements CustomerDAO {...}

複数のオブジェクト候補の中から1つの候補をCDIランタイムに選択させ、インジェクションの曖昧さを解決するにはいくつかの方法があります。

  1. カスタムの限定子(@Qualifier)タイプを定義して関係づける
  2. 標準の限定子@Newを用いて、クラス名で関係づける
  3. 標準の限定子@Namedを用いて、名前(文字列)で関係づける
  4. @Alternativeアノーテーションを用いて、beans.xmlから指定する
  5. 上記1)〜4)の組み合わせにより特定する

以下では、それぞれの定義方法について説明します。

1) カスタムの限定子(@Qualifier)タイプを定義して関係づける

限定子(@Qualifier)は、インジェクションの依存関係を決定するために使用するアノーテーションベースのメタデータ定義です。以下の例では、カスタムの限定子@ProductionModeを定義し、インジェクション・ポイントとインジェクション対象クラスの両方に@ProductionModeを付与することで、インジェクションの依存関係を解決しています。

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.inject.Qualifier;

// 限定子 @ProductionMode の定義
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface ProductionMode {}
import com.example.metadata.ProductionMode;
import javax.ejb.Stateless;
import javax.inject.Inject;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @ProductionMode
    CustomerDAO customerDAO;   // ← DAO実装1JPACustomerDAOImplがインジェクションされる。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1(限定子@ProductionModeを付与)
@ProductionMode
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
public class XmlCustomerDAOImpl implements CustomerDAO {...}

上記のようにカスタム限定子を使用した方法は、アノーテーションの定義を伴うため、実際の開発でカスタム限定子を手書きするのはやや骨の折れる作業になります。しかしながら、NetBeansなどのIDE環境のエディタではコンプリーション(クラス名補完)機能を恩恵を受けることでタイプ・セーフな形でインジェクションの依存関係を定義でき、コンパイル時点でタイプミスによるバグの混入を防ぐことができるという利点があります。

また、CDIサポート機能が追加されたNetBeans 6.9では簡単にカスタム限定子を作成することができるため、カスタム限定子作成の労力を大幅に軽減しているのはうれしい限りです。

nb69_cdi_support3
nb69_cdi_support4

なお、1つのカスタム限定子は異なる複数のインジェクション・ポイントにおける依存性解決のための共通のメタデータとして使用することができます。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @ProductionMode
    CustomerDAO customerDAO;

    @Inject @ProductionMode
    CustomerFinder customerFinder;
        :
}

カスタム限定子は引数をとるように定義することもできますので、アプリケーションの中で使用するカスタム限定子が極端に多くなってしまう場合には、引数付きのカスタム限定子を定義し、限定子の数をできるだけ少なくするように工夫することもできます。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @Mode(PRODUCTION)  // 引数付き限定子
    CustomerDAO customerDAO;
        :
}

2) 標準の限定子@Newを用いて、クラス名で関係づける

@NewアノーテーションはCDIの標準APIとして用意されている限定子で、カスタム限定子を作成する代わりにインジェクション対象のクラス名を直接指定することでインジェクション候補を限定する方法です。この場合、@Newアノーテーションのメンバにクラス名を指定してインジェクションポイントに付与します。ビーン・コンストラクタやプロデューサには特別な情報を与える必要はありません。以下が限定子@Newを使用した場合の例です。

import javax.enterprise.inject.New;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @New(JPACustomerDAOImpl.class)
    CustomerDAO customerDAO;   // ← DAO実装1JPACustomerDAOImplがインジェクションされる。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
public class XmlCustomerDAOImpl implements CustomerDAO {...}

@New限定子はカスタム限定子を定義する場合に較べて定義方法が簡単になるのが利点です。また、インジェクション・ポイントを見れば一見してどの実装クラスがインジェクションされるかが分かる点も利点です。しかし、インジェクション元のクラス名を変更する場合は、インジェクション・ポイントにおける@Newアノーテーションのクラス名も同時に修正しなければならない点に注意します。

3) 標準の限定子@Namedを用いて、名前(文字列)で関係づける

CDIにはもう一つ@Namedという標準の限定子が用意されており、オブジェクトに名前を付け、その名前で依存関係を解決する方法も用意されています。以下が@Named限定子を使用した例です。

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.inject.Named;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject @Named("jpaDao")
    CustomerDAO customerDAO;   // ← DAO実装1JPACustomerDAOImplがインジェクションされる。
        :
}

// DAOの実装1(ビーン名"jpaDao"を付与)
@Named("jpaDao")
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2(ビーン名"xmlDao"を付与)
@Named("xmlDao")
public class XmlCustomerDAOImpl implements CustomerDAO {...}

@Named限定子を使用する方法は、@Newと同じように手軽に依存関係を定義できる点が利点です。しかし、JSR 299のスペックの中でも述べられているように、(JSF/JSPのELからアクセスされる場合を除いて)@Named限定子はなるべく使用するべきではありません。名前による関連付けはタイプミスによる関連付けのバグをコンパイル時に検出することができないため、実際に動かしてみるまでバグを検出できない可能性があります。

4) @Alternativeアノーテーションを用いて、beans.xmlから指定する

これまでの方法は、依存関係の絞り込みのメタ情報をアノーテーションを用いてソースコードに埋め込むタイプの方法でしたが、外部ファイルに定義する方法も用意されています。依存関係の決定をソースコードの外部に切り出すためには、@Alternativeアノーテーションをインジェクション候補のビーン・クラスやプロデューサ・メソッド(または、プロデューサ・フィールド)に付与します。

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.enterprise.inject.Alternative;

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    @Inject
    CustomerDAO customerDAO;   // ← bean.xmlの定義によって決まる。
        :
}

// DAOのインタフェース
public interface CustomerDAO {...}

// DAOの実装1(@Alternativeを付与)
@Alternative
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2(@Alternativeを付与)
@Alternative
public class XmlCustomerDAOImpl implements CustomerDAO {...}

上記の場合、実際にインジェクションされるクラスを指定するには、デプロイするモジュールに含めるビーン構成ファイルbeans.xmlを使用します。以下は、@Alternativeな候補の中からJPACustomerDAOImplクラスを選択させる場合の例です。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                           http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
    <alternatives>
        <class>com.example.dao.JPACustomerDAOImpl</class>
    </alternatives>
</beans>

@Alternativeなビーンはbeans.xmlの<alternatives>タグで明示されなければ選択候補として有効にならない点に注意してください。逆に、beans.xmlで明示された@Alternativeなビーンは他の選択候補を全て無効にします。この性質を利用すると、選択肢が2つしか存在しない場合は2つのうち一方のビーンにのみ@Alternativeアノーテーションを付与すればよいことになります。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    // beans.xmlに明示がない場合: DAO実装1(JPACustomerDAOImpl)が採用される。
    // beans.xmlに明示がある場合: DAO実装2(XmlCustomerDAOImpl)が採用される。
    @Inject
    CustomerDAO customerDAO;   // ← beans.xmlの定義によって決まる。
        :
}

// DAOの実装1
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2(@Alternativeを付与)
@Alternative
public class XmlCustomerDAOImpl implements CustomerDAO {...}

5) 上記1)〜4)の組み合わせにより特定する

インジェクション候補の中から1つのビーンだけが解決されるようにするためには、場合によっては複数の限定子をインジェクション・ポイントに与えてあげる必要があります。しかし、期待するビーンのクラスやプロデューサに付与されている全ての限定子を指定する必要はありません。候補が1つに絞られるために必要な最低限の限定子をインジェクション・ポイントに与えてあげれば大丈夫です。例えば、CustomerDAOに対して以下のように3つの候補が存在する場合を考えます。

// DAOの実装1
@Named("jpaDao") @ProductionMode
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装2
@Named("xmlDao") @ProductionMode
public class XmlCustomerDAOImpl implements CustomerDAO {...}

// DAOの実装3
@Named("csvDao")
@Alternative
public class CsvCustomerDAOImpl implements CustomerDAO {...}

この状態でJPACustomerDAOImplオブジェクトが選択されることを考えると、JPACustomerDAOImplクラスに付与されている限定子は@Named("jpaDao")と@ProductionModeですが、@Named("jpaDao")だけをインジェクション・ポイントに指定するだけでビーンを一意に特定できるため、@ProductionModeは必ずしも指定する必要はありません。

    @Inject @Named("jpaDao")
    CustomerDAO customerDAO;   // ← JPACustomerDAOImplオブジェクトがインジェクションされる。

依存関係の解決にどの方法を使用すべきか?

以上述べてきたように、複数のインジェクション・オブジェクトの候補から1つのビーンを選択する方法は様々な方法がありますが、実際の開発ではどの方法を採用すべきかを考えてみます。

Java EEアプリケーションにおいてCDIの仕組みを使用する代表例は、プレゼンテーション層、ビジネス層、インテクグレーション層などティアをまたがるオブジェクト間の祖結合のためと考えられます。この目的でCDIを使用する場合、アプリケーションが完成に近づいている状態では、ティア間の結合が変更されることはほとんどなく、インジェクションの依存関係はほぼ静的な関係であると考えられます。この場合、もし限定子を付与しなくても候補が1つに決定されるなら最低限の@Injectだけですませるのがいいでしょう。もし、候補が複数存在するなら、なるべく@Named限定子をさけ、カスタム限定子か@New限定子を用いて依存関係を解決するのが望ましいでしょう。@Named限定子をさける理由は、先程述べたようにCDIが持つtype-safeなアプローチの恩恵を得るためです。

また、開発の初期段階では、各ティア毎に独立にコンポーネントの単体テストを実施することが望まれます。この場合、関連する他のティアのクラスはモックのクラスを用意して単体テストをすることが一般的です。例えば、プロダクション用のCustomerDAOの実装を待たずに、CustomerDAOのモックを作成してビジネス層のCustomerManageSessionBeanを単体テストする場合などです。この場合は、@Alternativeとbeans.xmlを使用することが有効です。インジェクション・ポイントの限定子は最終的なプロダクション・モードの組み合わせを定義しておき、単体テスト用のモック・クラスに@Alternativeを付与します。

// セッション・ビーン
@Stateless
public class CustomerManageSessionBean {

    // beans.xmlにモックの明示がない場合: プロダクション用DAO実装が採用される。
    // beans.xmlにモックの明示がある場合: 単体テスト用モックのDAO実装が採用される。
    @Inject
    CustomerDAO customerDAO;   // ← beans.xmlの定義によって決まる。
        :
}

// DAOの実装(プロダクション用)
public class JPACustomerDAOImpl implements CustomerDAO {...}

// DAOの実装(単体テスト用のモック)
@Alternative
public class MockCustomerDAOImpl extends JPACustomerDAOImpl {...}

こうすれば、モックのDAOを使用してセッション・ビーンの単体テストを実施した後、プロダクション用のDAOとの結合テストに移行する際にソースコードの変更は必要ありません。beans.xmlの置き換えだけで結合テストを実施することができ、またいつでもモック利用の単体テストモードに戻ることができます。

以上、インジェクション候補の絞り込み方法についてまとめてみました。次回は、CDIと従来のリソース・インジェクションとの関係についてと、CDIにおけるもう一つの重要な概念であるスコープについて解説したいと思います。

火曜日 5 25, 2010

Java EE 6: Understanding Contexts and Dependency Injection (CDI), Part 1

Java EE 6には、汎用化されたDepencency Injection(DI)をアプリケーションから利用可能な JSR 299: Contexts and Dependency Injection (CDI)仕様が含まれています。今回はこのCDIをJava EEアプリケーションの中でどのように使用していけばよいかを議論してみたいと思います。

CDI: Contexts and Dependency Injectionとは?

現在広く利用されているJava EE 5仕様でもDI機能は盛り込まれていましたが、非常に制限された形で仕様化されていました。インジェクションするオブジェクトも、インジェクションされるオブジェクトもその種類が制限されており、インジェクションに使用するアノーテーションも、インジェクションするオブジェクトの種類によって異なるアノーテーションクラスを使用する必要がありました(例えば、EJBのインジェクションには@EJBを使用し、データソースのインジェクションには@Resourceを使うなど)。そのため、Java EE 5仕様におけるインジェクションの機能は、汎用のDIと区別するため、一般に「リソース・インジェクション」と呼ばれています。Java EE 5におけるリソースインジェクションの制限事項については、以下のエントリを参照してください。

Java EE 6に含まれるCDI (JSR 299)仕様は、Java EE環境におけるDIコンテナの機能をアノーテーションをベースにしたtype safeなアプローチで標準仕様として規格化したものです。JSR 299はドラフト段階では、Web Beansと呼ばれていたもので、標準化プロセスの初期段階ではその仕様の多くの部分はJBoss Seamをベースにしたものでした。その後、他の代表的なDIコンテナであるGoogle GuiceSpring Frameworkのそれぞれのアイデアを集積し、Java EEの他の仕様(JSFやEJBなど)との関係や整合性について合理的に整理して現在の最終仕様となりました。

CDI (JSR 299)の仕様書は全部で98ページとなっており、一般的なJSRの仕様書としては短いドキュメントですが、その仕様の内容は凝縮されており、かなり込み入ったものとなっています。CDIはその仕様の複雑さゆえ、単純に開発を簡単にするものとは言えないかもしれません。CDIを使いこなせるのはおそらく中上級の開発者であると思います。しかし、CDIはアプリケーションを構成するオブジェクト間の結合を静的にも動的にもコントロールすることができる強力なツールであり、大規模開発におけるアプリケーションの品質を高いレベルで保つためのツールであるとも考えられます。Java EE 6環境でのアプリケーションのアーキテクチャ設計、フレームワーク設計を考えている方は、是非CDIを理解してみてください。きっとプロジェクトに有効な様々なアイデアを思いつくと思います。

先ほど述べましたように、CDI仕様はとても奥が深い仕様になっているため、1回のブログエントリでは表面的にも語り尽くすことができません。そのため、何回かのエントリに分けて、CDIについて解説してみたいと思います。今回は、CDIの基本的な使い方と、インジェクションの柔軟性にスポットライトを当ててみたいと思います。

CDIの基本

CDIでは、インジェクションする側のオブジェクトにもされる側のオブジェクトにもほとんど制約がありません。例えば、ビジネス層のセッション・ビーンにインテグレーション層のDAOをインジェクションすることも、Java EE 5ではできませんでしたが、Java EE 6では問題なくできるようになっています。

import javax.inject.Inject;

// セッション・ビーン(EJB)
@Stateless
public class CustomerManageSessionBean {
    @Inject
    private CustomerDAO customerDAO;  // POJOなDAOクラスがインジェクションされる
        :
}

// DAOクラス(POJO)
public class CustomerDAO {
        :
}

上記の例では、セッション・ビーンCustomerManageSessionBeanが生成されるときに、DAOクラスであるCustomerDAOのデフォルトコンストラクタが呼び出され、CostomerDAOのインスタンスがメンバ変数customerDAOにインジェクションされます。このように、CDIによるインジェクション機能を利用するための最低限の条件は、以下の2つです。

  • オブジェクトをインジェクションしたい場所(インジェクション・ポイント)に@Injectアノーテーションを付与する
  • CDIを有効にするEJBモジュール、またはWebモジュールに標準のデプロイメント記述子beans.xmlを含める
  • CDIを利用するには、@Injectアノーテーションをインジェクション・ポイントに付与するだけでなく、beans.xmlをデプロイするモジュールに含めなければいけない点に注意してください。Java EE 6コンテナはモジュールにbeans.xmlが含まれているかどうかによって、CDIランタイムを起動するかどうかを判断する仕様になっています。

    モジュール内にbeans.xmlを配置する場所は、Webモジュールの場合WEB-INF/beans.xml、EJBモジュールの場合はMETA-INF/beans.xmlです。なお、beans.xmlは特筆すべき定義がなければ中身は空で構いません(XMLプロローグ宣言やルートタグ<beans>も記述する必要はありません)。上記のEJBモジュールの例におけるJARファイルの構成を示すと以下のようになります。

    .
    |-- META-INF/
    |   |-- MANIFEST.MF
    |   `-- beans.xml   ← CDIの有効化に必須(中身は空で構わない)
    `-- com/
        `-- example/
            |-- business/
            |   `-- CustomerManageSessionBean.class
            `-- dao/
                `-- CustomerDAO.class
    

    NetBeansを利用する場合は、最新のNetBeans 6.9(現在、Beta版がリリース)からCDIサポート機能が追加されましたので、Webモジュール、またはEJBモジュールのプロジェクトを作成するときのウィザード画面で「Enable Contexts and Dependency Injection」のチェックボックスをチェックすることで、自動的にbeans.xmlが作成されます。

    nb69_cdi_support1
    nb69_cdi_support2

    なお、CDIはEmbeddable EJB Container上でも有効になりますので、EJBに対するインジェクションの動作であれば、アプリケーションサーバにデプロイしなくても確認することができます。Embeddable EJB Containerについては、以下のエントリを参考にしてください。

    インジェクション・ポイントの自由度

    CDIでは、インジェクション・ポイントの自由度も広がっています。CDIではメンバ変数をインジェクション・ポイントとするフィールド・インジェクションだけでなく、任意のメソッドの引数を経由したパラメータ・インジェクションが可能になっています。

    Java EE 5までのリソース・インジェクションでは、インジェクション・ポイントの形式は、メンバ変数か、ビーン・プロパティ仕様に基づくsetterメソッドでなければなりませんでした。

    // Java EE 5仕様のリソース・インジェクションの例:
    @Stateless
    public class CustomerManageSessionBean {
        @EJB
        private CustomerFinder customerFinder;  // フィールド・インジェクション
    
        @Resource(name="jdbc/__default")        // setterインジェクション
        void setDataSource(DataSource ds) {
            this.ds = ds;
        }
        private DataSoruce ds;
            :
    }
    

    CDIによるインジェクションでは、@Injectアノーテーションを任意のメソッドに付与することができ、必ずしもメソッドの形式はsetterメソッドである必要はありません。また、インジェクションされる引数の数が複数あっても構いません。この場合、そのメソッドの全てのパラメータがインジェクション・ポイントになります。

    @Stateless
    public class CustomerManageSessionBean {
    
        private CustomerFinder customerFinder;
        private CustomerDAO customerDAO;
    
        @Inject // パラメータ・インジェクション
        void init(CustomerFinder ejb, CustomerDAO dao) {
            this.customerFinder = ejb;
            this.customerDAO = dao;
        }
    }
    

    上記の例では、セッション・ビーンCustomerManageSessionBeanが生成されるときに、init()メソッドの引数経由で、CustomerFinderとCustomerDAOのインスタンスがインジェクションされます。CDIではこのようなメソッドを「イニシャライザ・メソッド」と呼びます。上記の例では、単にメンバ変数を初期化するだけなので、わざわざイニシャライザ・メソッドとして記述する意味はほとんどありませんが、オブジェクトの初期化時にインジェクションされるオブジェクトに関連して、何らかのロジックを実行したい場合には、イニシャライザ・メソッドを定義するのが有効と思われます。

    インジェクション・オブジェクトの供給方法の自由度

    これまでの例では、インジェクション・ポイントの型(すなわち、メンバ変数の型やイニシャライザ・メソッドの引数の型)とインジェクション・オブジェクトを供給する側のクラスが完全に一致している場合だけを示していましたが、CDIコンテナはクラスの継承関係やインタフェースの実装関係から連想される実体のクラスを探し出してくれます。

    // セッション・ビーン(EJB)
    @Stateless
    public class CustomerManageSessionBean {
        @Inject
        private CustomerDAO customerDAO;  // CustomerDAOImplのインスタンスがインジェクションされる
            :
    }
    
    // DAOクラス(POJO)
    public class CustomerDAOImpl implements CustomerDAO {
            :
    }
    

    上記はインジェクション・ポイントの型がインタフェースの場合ですが、インジェクションするオブジェクトの親クラスがインジェクション・ポイントの型である場合も同様に機能します。ただし、インジェクション・ポイントの型から1つだけ実体のクラスが導出される場合だけ機能する点に注意してください。インジェクション・オブジェクトとして複数のクラスが候補として連想される場合は、他のメタデータを使用して、候補が1つだけになるようにしてあげる必要があります。インジェクション・オブジェクトの候補を絞り込む方法については、別途解説したいと思います。

    また、別の観点ではこれまでの例では、インジェクションされるオブジェクトの生成には、(EJBの例を除いて)該当クラスのデフォルトコンストラクタが使用されていました。しかし、インジェクション・オブジェクトを供給する手段としては必ずしもデフォルトコンストラクタでなければならない訳ではありません。引数付きのコンストラクタや、ファクトリ・パターンにおけるファクトリ・メソッドをインジェクション・オブジェクトの供給方法として定義することができます。

    1) ビーン・コンストラクタの選択

    引数のあるコンストラクタをオブジェクト生成に使用したい場合は、以下のように目的のコンストラクタに@Injectアノーテーションを付与します。

    // セッション・ビーン(EJB)
    @Stateless
    public class CustomerManageSessionBean {
        @Inject
        private CustomerDAO customerDAO; // ← コンストラクタCustomerDAO(DAOUtils)の結果が入る
            :
    }
    
    // DAOクラス(POJO)
    public class CustomerDAO {
        private DAOUtils util;
    
        // この場合、デフォルト・コンストラクタはCDIからは利用されない
        public CustomerDAO() {}
    
        @Inject // CDIビーン・コンストラクタとしてマーク
        public CustomerDAO(DAOUtils util) {
            this.util = util;
        }
            :
    }
    

    上記の例では、メンバ変数customerDAOには、デフォルトコンストラクタの代わりにコンストラクタCustomerDAO#CustomerDAO(DAOUtils)によって生成されたインスタンスが設定されます。なお、このコンストラクタを呼び出すためには引数DAOUtilsを解決しなければなりません。CDIコンテナは再帰的にDAOUtilsオブジェクトの生成方法を解決し、最終的なオブジェクトツリーを生成します。

    2) プロデューサ(@Produces)による依存関係の解決

    もし、インジェクション・オブジェクトの供給をファクトリ・メソッド経由で行いたい場合は、以下のようにファクトリメソッドに@Producesアノーテーションを付与します。

    // セッション・ビーン(EJB)
    @Stateless
    public class CustomerManageSessionBean {
        @Inject
        private CustomerDAO customerDAO; // ← DAOFactory#getCustomerDAO()の結果が入る
            :
    }
    
    import javax.inject.Produces;
    
    // ファクトリ・クラス
    public class DAOFactory {
        @Produces // プロデューサ・メソッドとしてマーク
        public static CustomerDAO getCustomerDAO() {
            return new CustomerDAO(new DAOUtils());
        }
    }
    
    // DAOクラス(POJO)
    public class CustomerDAO {
        private DAOUtils util;
    
        CustomerDAO(DAOUtils util) {
            this.util = util;
        }
            :
    }
    

    このように、メソッドの戻り値により依存関係にあるオブジェクトを供給するメソッドを「プロデューサ・メソッド」と呼びます。プロデューサ・メソッドは、イニシャライザ・メソッドやビーン・コンストラクタと同様に引数を持つことができます。この場合、プロデューサ・メソッドのそれぞれの引数はCDIランタイムによって一意に解決できる必要があります。@Producesアノーテーションはメンバ変数に付与することもできます。この場合、依存関係のあるオブジェクトはそのメンバ変数経由で依存先に供給することになります。このように@Producesアノーテーションがついたメンバ変数を「プロデューサ・フィールド」と呼びます。

    以上、述べてきたようにCDIではインジェクション・ポイントの定義が柔軟で、依存関係のあるオブジェクトの供給方法にも柔軟性があることが理解して頂けたと思います。次回のエントリでは、依存関係のオブジェクトの候補が複数存在する場合に適切なオブジェクトを一意に決定するためのクオリファイヤ(@Qualifier)について解説したいと思います。

    About

    Takashi Nishigaya
    Principal Consultant
    Technology Solution Consulting
    Oracle Consulting Services

    Search

    Categories
    Archives
    « 4月 2014
      
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
       
           
    今日