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.Loggingtest.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.

[次のページ]