Introduction to GluonJ
千葉 滋 (訳: 西澤 無我)
Table of Contents
1. なぜ GluonJ が必要か?
2. Glue クラスを定義する
3. クラスの改良 (Refinement) その 1
4. クラスの改良 (Refinement) その 2
5. ポイントカット (Pointcut) とアドバイス (Advice)
6. Glue クラスの拡張
1. なぜ GluonJ が必要か?
GluonJ (グルーオン・ジェー) は Java 用のシンプルな AOP (Aspect-Oriented Programming) システムです。Java の文法で AOP の機構を提供しているので、アスペクトを書くために独自の文法を覚える必要はありません。
AOP の応用はすでにいくつも知られています。例えば、ソフトウェアのテストプログラムを書くのに AOP は役立つと言われています。以下にそのテストプログラムの例を示します。
package demo; import java.math.BigDecimal; public class Bank { public BigDecimal transfer(Account src, Account dest, BigDecimal amount) { dest.deposit(amount); return src.withdraw(amount); } }
package demo; import java.math.BigDecimal; public class Account { protected BigDecimal balance; public BigDecimal deposit(BigDecimal amount) { return balance.add(amount); } public BigDecimal withdraw(BigDecimal amount) { return balance; // incomplete! } }
上記の Account
クラスの定義は、完成していません。フィールド balance
の値は初期化されていませんし、withdraw
メソッドは何も行いません。ところが、時々開発者は、こういった完成していないクラスを含んだプログラムを、テストしなければならないこともあります。以下は Bank
クラスの transfer
メソッドの実装をテストするためのテストプログラムです。
package demo; import junit.framework.TestCase; import java.math.BigDecimal; public class BankTest extends TestCase { public void testTransfer() { Account mine = new Account(); Account yours = new Account(); BigDecimal pay = new BigDecimal(20000); BigDecimal balance = new BigDecimal(10000); Bank bank = new Bank(); assertEquals(balance, bank.transfer(yours, mine, pay)); } }
もしこのテストプログラムをそのまま実行したら、当然テストは失敗してしまうでしょう。テストメソッドの最後の行で呼ばれている transfer
メソッドは、Account
クラスの withdraw
メソッドを呼び出します。ところが withdraw
メソッドは完全には実装されていないのです。
このテストの失敗と transder
メソッドの実装とは無関係です。テストの対象である transfer
メソッドの実装自体にバグがなければ、このテストプログラムは期待通りに終了されるべきです。我々はこのテストの問題点を解決し、テストを成功させるべきです。
GluonJ を利用すれば、この問題を簡単に解決することができます。以下のような glue クラスを書けばよいのです。
package demo; import javassist.gluonj.*; import java.math.BigDecimal; @Glue public class Mock { @Refine static class Account extends Account { protected BigDecimal balance = new BigDecimal(30000); public BigDecimal withdraw(BigDecimal amount) { return balance.subtract(amount); } } }
もし上記の全てのプログラム (Bank
, Account
,BankTest
, そして Mock
) をいっしょに実行すれば、テストプログラム BankTest
は期待した通りに終了し、テストは成功します。DI (dependency injection) フレームワークのように、glue クラスは balance
フィールドへ初期値 30000 を代入します。また、glue クラスは、元の Account
クラスの withdraw
メソッド実装を仮の実装に置き換えます (完全な実装では、amount
の値が大きすぎたときに、例外を投げるべきです)。なお詳しい説明は後でおこないます。
Account
を完全に実装し終えた後でも、上述した glue クラスを、有効に利用することができます。単体テストでは、そのテスト結果は、テスト対象プログラムが呼び出している他のコンポーネント、モジュール、やクラスに依存しているべきではありません。Bank
クラスの単体テストを行っている間、上記の glue クラスは Bank
クラスから Account
クラスの実装を分離します。すなわち、Banck
クラスから Account
クラスの実装への依存度を下げているのです。withdraw
メソッドの実装が完成した後も、glue クラスは単体テストの間、withdraw
の実装をテスト用の仮の実装に戻してしまいます。
上記のプログラムは簡単に実行することができます。JDK 1.5 もしくはそれ以降の JDK を利用しているのであれば、コマンドラインから java コマンドを実行するときに、以下のオプションを追加します。
-javaagent:gluonj.jar=demo.Mock
ここでは、gluonj.jar
をカレントディレクトリに置いているものとしています。もし Java 用の統合開発環境 eclipse を使用しているのであれば、上記のオプションを VM 引数として JVM に渡します。VM 引数は、Launch Configurations ダイアログ (構成および実行ダイアログ) によって指定できます。
2. Glue クラスを定義する
Glue は、既存のアプリケーション・クラスに対する様々な拡張を集めたものです。Glue は、@Glue
によって注釈されたクラスによって表現されます (このクラスを以下、@Glue
クラスと呼びます)。@Glue
クラス内で行える拡張は、クラスの改良 (refinement)、もしくはポイントカットとアドバイス (pointcut-advice) です。これらは好きな数だけ @Glue
クラスの中に含めることができます。
Refinement は、@Glue
クラス内に含まれる static
な入れ子クラスです。この refinement を @Refine
クラスと呼びます。Refinement は AspectJ のインタータイプ宣言に相当しますが、より強力な拡張性をもっています。
ポイントカットとアドバイスは、@Glue
クラス内で宣言された Pointcut
型のフィールドです。このフィールドは @Before
, @After
, もしくは @Around
によって注釈を付けられていなければなりません。
以下に、@Glue
クラスの例を示します。
package test; import javassist.gluonj.*; @Glue class Logging { @Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("test.Hello#say(..)"); @Refine static class Afternoon extends Hello { public String header() { return "Good afternoon, "; } } }
この @Glue
クラスは以下の test.Hello
クラスを拡張しています。
package test; public class Hello { public String header() { return "Good morning, "; } public void say(String toWhom) { System.out.println(header() + toWhom); } public static void main(String[] args) { new Hello().say("Bill"); } }
もし test.Hello
クラスを @Glue
クラスである Logging
抜きに実行すると、その出力結果は以下となるでしょう。
Good morning, Bill
一方、もし @Glue
クラスといっしょに test.Hello
クラスを実行した場合、出力結果は下記のように変更されます。
call! Good afternoon, Bill
@Glue
クラス Logging
内のポイントカットとアドバイス (@Before
で注釈されたフィールド) は、say
メソッドが呼ばれる直前で、"call!"
を出力します。また、@Refine
クラスは、header
メソッドが "Good afternoon, "
を返すように test.Hello
クラスを改良します。詳しい説明は後ろの章を見てください。
織り込み
他のクラスに @Glue
クラスを適用するためには、織り込み (weaving) を実行しなければなりません。この織り込みとはコンパイル後のプログラム変換です。GluonJ では、2 つのタイプの織り込み (実行前の織り込みとロード時の織り込み) をサポートしています。
実行前の織り込みを支援する Ant タスク
まず実行前の織り込みについて説明します。GluonJ は、宣言した @Glue
クラスに従い、コンパイルされたクラスファイルを変換する Ant タスクを提供しています。以下に、その Ant タスクを定義し、呼び出す build.xml
ファイルの例を示します。
<?xml version="1.0"?> <project name="hello" basedir="."> <taskdef name="weave" classname="javassist.gluonj.ant.taskdefs.Weave"> <classpath> <pathelement location="./gluonj.jar"/> </classpath> </taskdef> <target name="weave"> <weave glue="test.Logging" destdir="./out" debug="false" > <classpath> <pathelement path="./classes"/> </classpath> <fileset dir="./classes" includes="test/**/*" /> </weave> </target> </project>
ここでは、先ほどと同様に gluonj.jar
をカレントディレクトリに置いているものとして、build.xml
ファイルを記述します。また、Java コンパイラによって生成されたクラスファイルを ./classes
ディレクトリに置いているものとします。さらに、クラスファイルが変換された後、それらがセーブされる場所は ./out
だとします。@Glue
クラスは test.Logging
です。その @Glue
クラスは、fileset
要素によって指定されているクラスファイルを変換します。上記の例では、./classes
/test
ディレクトリ以下にあるすべてのクラスファイルが変換の対象となります (test
で始まるパッケージ名のクラスのクラスファイルが対象となります)。
weave
タスクは debug
属性を持ちます。この属性の値が true
なら、GluonJ は詳細なログメッセージを出力します。この属性の値を明示的に指定しない場合、値は false
となります。
コマンドラインによる実行前の織り込み
織り込みはコマンドラインからおこなうこともできます。
java -jar gluonj.jar test.Logging test/Hello.class
上の java
コマンドを実行する前には、
./classes
ディレクトリに移動していなければなりません。クラスファイルはカレントディレクトリになければなりません。パス名はそれぞれ、./test/Logging.class
と ./test/Hello.class
となります。
2 番目の引数 test.Logging
は @Glue
クラスの完全修飾名です。もし複数のクラスファイルを変換しなければならないときは、test.Logging
に続く 3 番目、4 番目、... の引数として与えます。それらの引数はクラスファイルのパス名です。クラス名でないことに注意してください。
もしなにも引数を与えないと、gluonj.jar
は GluonJ
のバージョン番号とコピーライトを表示します。
java -jar gluonj.jar
ロード時の織り込み
もう片方のロード時の織り込みについて説明します。Java virtual machine (JVM) が各クラスファイルをロードするタイミングで、GluonJ はそれらのクラスを変換することができます。この織り込みを行うためには、JVM にコマンドラインのオプション -javaagent
を利用します。例えば、
java -javaagent:gluonj.jar=test.Logging -cp "./classes" test.Hello
再度、gluonj.jar
をカレントディレクトリに置いているものとし、./classes
ディレクトリにクラスファイルを置いているものとします。@Glue
クラスは、test.Logging
です。ここで、-javaagent:
... は VM 引数です。もし Ant で上記のコマンドを実行するのなら、以下のように build.xml
ファイルに書きます。
<java fork="true" classname="test.Hello"> <classpath> <pathelement path="./classes"/> </classpath> <jvmarg value="-javaagent:./gluonj.jar=test.Logging"/> </java>
もし詳細なログメッセージを出力させたい場合は、debug
オプションを指定します。例えば、
java -javaagent:gluonj.jar=test.Logging,debug -cp "./classes" test.Hello
のようにします。なおカンマと debug
の間に空白を入れてはいけません。
Apache Tomcat を使ったロード時の織り込み
GluonJ のロード時織り込みは、Tomcat 5.x または 6.x
上のプログラムに対しておこなうこともできます。
そのためには、まず gluonj.jar
を
$CATALINA_HOME/lib
にコピーします。
そして環境変数 JAVA_OPS
を、$CATALINA_HOME/bin/setenv.sh
(Linux 上の Tomcat 6.x の場合)
など、適当な初期化スクリプトの中で次のように設定します。
JAVA_OPTS="-javaagent:${CATALINA_HOME}/lib/gluonj.jar=javassist.gluonj.loader.tomcat.WeaverGlue -Djavassist.gluonj.classpath=${CATALINA_HOME}/lib/* ${JAVA _OPTS}"
このように設定すると、JVM は Tomcat サーバに GluonJ を組み込んで動かします。
最後に、各 Web アプリケーションごとに @Glue
クラスを定義します。
@Glue
クラスは WEB-INF/classes
の下の
gluonj.properties
ファイルで定義します。
内容は下のような 1 行だけです。
gluename=test.Logging
このように書くと、対応する Web アプリケーションのプログラムに
test.Logging
が織り込まれます。
もし gluonj.properties
ファイルが存在しないか、あるいは空
(あるいは # から始まるコメントしか含まれない) の場合は、@Glue
クラスはまったく織り込まれません。
複数の @Glue
クラスを利用する織り込み
実行前の織り込みとロード時の織り込みのどちらの場合でも、指定できる @Glue
クラスは 1 つです。そのような例はすでに上で述べました。もし複数の @Glue
クラスを他のクラスに適用したいときには、1 つの @Glue
クラスの定義に、他の @Glue
クラスを含めます。
例えば、以下の @Glue
クラスの定義は、他の 2 つの @Glue
クラスを含んでいます。
package test; import javassist.gluonj.*; @Glue class AllGlues { @Include Logging glue0; @Include Tracing glue1; }
@Glue
クラス test.Logging
と test.Tracing
は、子供の @Glue
クラスとして @Glue
クラス AllGlues
に含まれています。AllGlues
は親と呼びます。
@Include
は、他の @Glue
クラスを指定するために使われます。もしフィールド宣言が @Include
によって注釈されていれば、GluonJ はそのフィールドの型が @Glue
クラスだと解釈します。そのクラスは、元の @Glue
クラスの子供の @Glue
クラスになります。GluonJ は、@Include
によって注釈されたフィールド (すなわち @Glue
クラス) を、フィールドの名前の辞書順でソートします。そして、そのソートした順番通りに @Glue
クラスを他のクラスに適用します。親の @Glue
クラス (@Include
で注釈されたフィールドをもっているクラス) は最後に適用されます。
Copyright (C) 2006-2007 by Shigeru Chiba and Muga Nishizawa. All rights reserved.