目次


Java プログラミングのダイナミックス

第 6 回 Javassist を使用したアスペクト指向の変更

バイトコードの検索・置換変換に Javassist を使用する

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Java プログラミングのダイナミックス

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Java プログラミングのダイナミックス

このシリーズの続きに乞うご期待。

このシリーズの第 4 回第 5 回では、バイナリークラスへのローカライズ変更のために、Javassist をどのように使用するかという方法を提示しました。今回は、バイトコード内の特定なメソッド、あるいはフィールドの全ての使用を発見する Javassist のサポートを利用して、より強力なフレームワークの使用方法を学習します。この機能は、Javassist のパワーにとってバイトコードを指定するソースコードのような方法のサポートと同じくらい重要です。さらに、選択的な置換操作のサポートにより、Javassist はアスペクト指向プログラミングの機能を標準な Java コードへ追加する素晴らしいツールになります。

第 5 回では、Javassist を使ってどのようにクラスローディング・プロセスをインターセプトするのかを見てきました。また、ロード時にバイナリークラス表現への変更を行うことも見てきました。この記事で説明するシステマチックなバイトコード変換は、static クラスファイル変換か実行時インターセプションのいずれかに使用することができますが、実行時に使用すると、特に有用となります。

バイトコード変更の処理

Javassist は、システマチックなバイトコード変更を処理する 2 つの方法を提供しています。javassist.CodeConverter クラスを使用する 1 つ目のテクニックは少し簡単ですが、達成できることに関しては、多くの制限があります。2 つ目のテクニックは、javassist.ExprEditor クラスのカスタムサブクラスを使用します。多少手間がかかりますが、追加された柔軟性には努力しただけの価値はあります。この記事で 2 つの方法の例題を見ていきます。

コード変換

システマチックなバイトコード変更する 1 番目の Javassist 技術は、javassist.CodeConverter クラスを使用します。この技術を利用するには、CodeConverter クラスのインスタンスを作成し、1 つ以上の変換アクションでそれを構成します。それぞれの変換は、その変換タイプを識別するメソッドの呼び出しを使用して構成されます。これらは、メソッド呼び出し変換、フィールドアクセス変換、新規オブジェクト変換という 3 つのカテゴリーに分類されます。

リスト 1 は、メソッド呼び出し変換を使用した例題です。この場合、この変換はメソッドが呼ばれているという通知を追加します。このコードでは、最初に、変換プログラム(以前の第 5 回のような)を使用するためにそれを構成して、至る所で使用するために javassist.ClassPool インスタンスをゲットします。そして、ClassPool によりメソッド定義のペアにアクセスします。この最初のメソッド定義は、モニターされる set- style メソッド (クラスやメソッド名形式のコマンドライン引数で) のためであり、2 番目のメソッド定義は、最初のメソッドへの呼び出しをリポートする TranslateConvert クラスにある reportSet() メソッドのためです。

一度メソッド情報を持つと、set メソッドへの各呼び出し前に、reporting メソッドへの呼び出しを追加する変換を構成する CodeConverterinsertBeforeMethod() を使用することができます。そして、しなければならないことは、1 つ以上のクラスにこのコンバーターを適用させることです。リスト 1 のコードでは、クラスオブジェクトの instrument() メソッドへの呼び出しを持つ ConverterTranslator 内部クラスの onWrite() メソッド内でこれを行います。これにより、ClassPool インスタンスからロードされるあらゆるクラスに変換が自動的に適用されるでしょう。

リスト 1. CodeConverter の使用
public class TranslateConvert
{
    public static void main(String[] args) {
        if (args.length >= 3) {
            try {
                // set up class loader with translator
                ConverterTranslator xlat =
                    new ConverterTranslator();
                ClassPool pool = ClassPool.getDefault(xlat);
                CodeConverter convert = new CodeConverter();
                CtMethod smeth = pool.get(args[0]).
                    getDeclaredMethod(args[1]);
                CtMethod pmeth = pool.get("TranslateConvert").
                    getDeclaredMethod("reportSet");
                convert.insertBeforeMethod(smeth, pmeth);
                xlat.setConverter(convert);
                Loader loader = new Loader(pool);
                // invoke "main" method of application class
                String[] pargs = new String[args.length-3];
                System.arraycopy(args, 3, pargs, 0, pargs.length);
                loader.run(args[2], pargs);
                } catch ...
            }
            } else {
            System.out.println("Usage: TranslateConvert " +
                "clas-name set-name main-class args...");
        }
    }
    public static void reportSet(Bean target, String value) {
        System.out.println("Call to set value " + value);
    }
    public static class ConverterTranslator implements Translator
    {
        private CodeConverter m_converter;
        private void setConverter(CodeConverter convert) {
            m_converter = convert;
        }
        public void start(ClassPool pool) {}
        public void onWrite(ClassPool pool, String cname)
            throws NotFoundException, CannotCompileException {
            CtClass clas = pool.get(cname);
            clas.instrument(m_converter);
        }
    }
}

構成するのにかなり複雑な操作ですが、一度セット・アップしてしまうと、簡単に動作します。リスト 2 は、テストケースとして使うコード・サンプルを示しています。ここでは、値にアクセスする BeanTest プログラムが使用する bean のような get や set メソッドを持つテストオブジェクトを Bean が提供しています。

リスト 2. bean テスター
public class Bean
{
    private String m_a;
    private String m_b;
    public Bean() {}
    public Bean(String a, String b) {
        m_a = a;
        m_b = b;
    }
    public String getA() {
        return m_a;
    }
    public String getB() {
        return m_b;
    }
    public void setA(String string) {
        m_a = string;
    }
    public void setB(String string) {
        m_b = string;
    }
}
public class BeanTest
{
    private Bean m_bean;
    private BeanTest() {
        m_bean = new Bean("originalA", "originalB");
    }
    private void print() {
        System.out.println("Bean values are " +
            m_bean.getA() + " and " + m_bean.getB());
    }
    private void changeValues(String lead) {
        m_bean.setA(lead + "A");
        m_bean.setB(lead + "B");
    }
    public static void main(String[] args) {
        BeanTest inst = new BeanTest();
        inst.print();
        inst.changeValues("new");
        inst.print();
    }
}

ここに、リスト 2 の BeanTest プログラムを直接実行した場合の結果出力があります。

[dennis]$ java -cp . BeanTest
Bean values are originalA and originalB
Bean values are newA and newB

リスト 1TranslateConvert プログラムを使用して BeanTest を実行し、モニタへ set メソッドのうちの 1 つを指定すれば、結果出力はこのようになるでしょう。

[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are originalA and originalB
Call to set value newA
Bean values are newA and newB

すべて前と同様に動作しますが、今回はプログラムが実行されている間、選択されたメソッドが呼ばれているという通知があります。

この場合、例えば、第 4 回での技術を使用した set メソッド本文へコードを追加すれば、これと同じ結果が簡単に達成できたかもしれません。ここでの違いは、使用ポイントでコードを追加することにより、柔軟性が得られるということです。例えば、ロードされているクラス名をチェックするように TranslateConvert.ConverterTranslatoronWrite() メソッドを修正し、リストに含まれているクラスの変換だけをするでしょう。set メソッドの本文に直接コードを追加すると、そのような選択的なモニタリングができません。

システマチックなバイトコード変換により提供される柔軟性は、それらを標準 Java コードのアスペクト指向の拡張を実装するための強力なツールにするものです。これに関して、この記事の残りでもっと見ていきます。

変換の制限

CodeConverter による変換は役に立ちますが、制限があります。例えば、ターゲットメソッドが呼ばれる前後で、モニタリングメソッドを呼び出したい場合、そのモニタリングメソッドは static void として定義される必要があり、ターゲットメソッドと同じ数、同じタイプのパラメーターに従って、 (ターゲットメソッドのクラスの) シングルパラメータを持たなければなりません。モニタリングメソッドが呼ばれると、ターゲットメソッドの全引数に従って、一番目 (最初の) 引数として実際のターゲットオブジェクトを渡します。

この厳密な構造は、モニタリングメソッドがターゲットクラスやメソッドに正確に一致する必要があることを意味します。では例として、異なるターゲットクラスで使用できるように、一般的な java.lang.Object パラメータを持つよう、リスト 1reportSet() メソッドの定義を変更しましょう。

    public static void reportSet(Object target, String value) {
        System.out.println("Call to set value " + value);
    }

コンパイルは上手くいきますが、実行するとブレークしてしまいます。

[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are A and B
java.lang.NoSuchMethodError: TranslateConvert.reportSet(LBean;Ljava/lang/String;)V
        at BeanTest.changeValues(BeanTest.java:17)
        at BeanTest.main(BeanTest.java:23)
        at ...

しかし、この制限をうまく回避する方法があります。その解決策は、ターゲットメソッドと一致するカスタム・モニタリングメソッドを実行時に生成することです。しかしながら、これを行うには多くの手間がかかりますので、この記事では行いません。幸運にも、Javassist が、システマチックなバイトコード変換処理の方法を提供します。この方法は、javassist.ExprEditor を使用するので、CodeConverter と比べてより柔軟でより強力なのです。

簡単なクラスのミューティレーション

javassist.ExprEditor を使用したバイトコード変換は、CodeConverter を使用する場合と同じ原則を持っています。しかし、ExprEditor の方法は、理解するのがちょっと難しいので、基本原則の実証から始め、その後で実際の変換について説明していきます。

リスト 3 は、アスペクト指向の変換において、ターゲットの基本アイテムをレポートするための ExprEditor の使用方法を示しています。ここでは、3 つの基本クラスメソッドをオーバーライドし、VerboseEditorExprEditor をサブクラス化します。すべてに edit() と名前を付けますが、異なったパラメータータイプを持っています。リスト 1 のコードのように、ClassPool インスタンスからロードされる全てのクラスに対してクラスオブジェクトの instrument() メソッドの呼び出しでインスタンスを渡し、DissectionTranslator クラスの onWrite() メソッド内からこのサブクラスを実際に使用します。

リスト 3. クラスのディセクター
public class Dissect
{
    public static void main(String[] args) {
        if (args.length >= 1) {
            try {
                // set up class loader with translator
                Translator xlat = new DissectionTranslator();
                ClassPool pool = ClassPool.getDefault(xlat);
                Loader loader = new Loader(pool);
                    // invoke the "main" method of the application class
                String[] pargs = new String[args.length-1];
                System.arraycopy(args, 1, pargs, 0, pargs.length);
                loader.run(args[0], pargs);
                } catch (Throwable ex) {
                ex.printStackTrace();
            }
            } else {
            System.out.println
                ("Usage: Dissect main-class args...");
        }
    }
    public static class DissectionTranslator implements Translator
    {
        public void start(ClassPool pool) {}
        public void onWrite(ClassPool pool, String cname)
            throws NotFoundException, CannotCompileException {
            System.out.println("Dissecting class " + cname);
            CtClass clas = pool.get(cname);
            clas.instrument(new VerboseEditor());
        }
    }
    public static class VerboseEditor extends ExprEditor
    {
        private String from(Expr expr) {
            CtBehavior source = expr.where();
            return " in " + source.getName() + "(" + expr.getFileName() + ":" +
                expr.getLineNumber() + ")";
        }
        public void edit(FieldAccess arg) {
            String dir = arg.isReader() ? "read" : "write";
            System.out.println(" " + dir + " of " + arg.getClassName() +
                "." + arg.getFieldName() + from(arg));
        }
        public void edit(MethodCall arg) {
            System.out.println(" call to " + arg.getClassName() + "." +
                arg.getMethodName() + from(arg));
        }
        public void edit(NewExpr arg) {
            System.out.println(" new " + arg.getClassName() + from(arg));
        }
    }
}

リスト 4 は、リスト 2BeanTest プログラム上でリスト 4 の Dissect プログラムの実行により生成された結果を示しています。これは、全メソッドの呼び出し、フィールド・アクセス、新しいオブジェクト生成をリスト表示しており、それぞれロードされたクラスのメソッド内で行われた内容の詳細です。

リスト 4. BeanTest の分析
[dennis]$ java -cp .:javassist.jar Dissect BeanTest
Dissecting class BeanTest
 new Bean in BeanTest(BeanTest.java:7)
 write of BeanTest.m_bean in BeanTest(BeanTest.java:7)
 read of java.lang.System.out in print(BeanTest.java:11)
 new java.lang.StringBuffer in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 read of BeanTest.m_bean in print(BeanTest.java:11)
 call to Bean.getA in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 read of BeanTest.m_bean in print(BeanTest.java:11)
 call to Bean.getB in print(BeanTest.java:11)
 call to java.lang.StringBuffer.append in print(BeanTest.java:11)
 call to java.lang.StringBuffer.toString in print(BeanTest.java:11)
 call to java.io.PrintStream.println in print(BeanTest.java:11)
 read of BeanTest.m_bean in changeValues(BeanTest.java:16)
 new java.lang.StringBuffer in changeValues(BeanTest.java:16)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
 call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:16)
 call to Bean.setA in changeValues(BeanTest.java:16)
 read of BeanTest.m_bean in changeValues(BeanTest.java:17)
 new java.lang.StringBuffer in changeValues(BeanTest.java:17)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
 call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
 call to java.lang.StringBuffer.toString in changeValues(BeanTest.java:17)
 call to Bean.setB in changeValues(BeanTest.java:17)
 new BeanTest in main(BeanTest.java:21)
 call to BeanTest.print in main(BeanTest.java:22)
 call to BeanTest.changeValues in main(BeanTest.java:23)
 call to BeanTest.print in main(BeanTest.java:24)
Dissecting class Bean
 write of Bean.m_a in Bean(Bean.java:10)
 write of Bean.m_b in Bean(Bean.java:11)
 read of Bean.m_a in getA(Bean.java:15)
 read of Bean.m_b in getB(Bean.java:19)
 write of Bean.m_a in setA(Bean.java:23)
 write of Bean.m_b in setB(Bean.java:27)
Bean values are originalA and originalB
Bean values are newA and newB

キャスト、テストのインスタンスをレポートするサポートを簡単に追加し、VerboseEditor に適切なメソッドを実装することで、ブロックの catch することができました。しかし、これらのコンポーネントアイテムに関する情報を単にリスト表示するだけでは退屈ですので、実際にアイテムの修正について見ていきましょう。

進行の分析

リスト 4 にあるクラスの分析は、基本コンポーネントの操作をリスト表示しています。アスペクト指向の機能を実装する時、これらがどれだけ役立つかということを確かめるのは簡単です。例えば、選択されたフィールドへの全ての書き込みアクセスをレポートするロガーは、多くのアプリケーションで適用する有用なアスペクト (様相) でしょう。結局、それは私が皆さんに見ようとしている方法なのです。

この記事のテーマにとって幸運にも、ExprEditor はどのような操作がコード中に存在しているのかを教えてくれるだけでなく、レポートされている操作の修正が可能です。様々な ExprEditor.edit() メソッドで渡されるパラメータタイプは、それぞれの replace() メソッドの定義を呼び出します。もし、通常の Javassist のソースコード形式にあるステートメント (第 4 回でカバーされた) をこのメソッドに渡すと、そのステートメントはバイトコードにコンパイルされ、オリジナル操作の置き換えに使われます。これは、バイトコードを容易にスライシングしたり、ダイシングしたりします。

リスト 5 は、コード置換の適用を示しています。ロギング操作ではなく、ここでは選択されたフィールドへ格納されている String 値を実際に修正することにします。FieldSetEditor では、フィールド・アクセスと一致するメソッド・シグネチャをインプリメントします。このメソッド内では、フィールド名は検索しているものか、操作は格納されているかという 2 つのことをチェックします。一致を見つけると、オリジナルの格納を実際の TranslateEditor クラス内にある reverse() メソッドへの呼び出し結果を使用するものに置き換えます。reverse() メソッドはオリジナルの string にある文字順序を単に逆にし、使用されたことを示すメッセージを出力します。

リスト 5. ストリングセットを逆にする
public class TranslateEditor
{
    public static void main(String[] args) {
        if (args.length >= 3) {
            try {
                // set up class loader with translator
                EditorTranslator xlat =
                    new EditorTranslator(args[0], new FieldSetEditor(args[1]));
                ClassPool pool = ClassPool.getDefault(xlat);
                Loader loader = new Loader(pool);
                // invoke the "main" method of the application class
                String[] pargs = new String[args.length-3];
                System.arraycopy(args, 3, pargs, 0, pargs.length);
                loader.run(args[2], pargs);
                } catch (Throwable ex) {
                ex.printStackTrace();
            }
            } else {
            System.out.println("Usage: TranslateEditor clas-name " +
              "field-name main-class args...");
        }
    }
    public static String reverse(String value) {
        int length = value.length();
        StringBuffer buff = new StringBuffer(length);
        for (int i = length-1; i >= 0; i--) {
            buff.append(value.charAt(i));
        }
        System.out.println("TranslateEditor.reverse returning " + buff);
        return buff.toString();
    }
    public static class EditorTranslator implements Translator
    {
        private String m_className;
        private ExprEditor m_editor;
        private EditorTranslator(String cname, ExprEditor editor) {
            m_className = cname;
            m_editor = editor;
        }
        public void start(ClassPool pool) {}
        public void onWrite(ClassPool pool, String cname)
            throws NotFoundException, CannotCompileException {
            if (cname.equals(m_className)) {
                CtClass clas = pool.get(cname);
                clas.instrument(m_editor);
            }
        }
    }
    public static class FieldSetEditor extends ExprEditor
    {
        private String m_fieldName;
        private FieldSetEditor(String fname) {
            m_fieldName = fname;
        }
        public void edit(FieldAccess arg) throws CannotCompileException {
            if (arg.getFieldName().equals(m_fieldName) && arg.isWriter()) {
                StringBuffer code = new StringBuffer();
                code.append("$0.");
                code.append(arg.getFieldName());
                code.append("=TranslateEditor.reverse($1);");
                arg.replace(code.toString());
            }
        }
    }
}

ここに、リスト 2BeanTest プログラム上で TranslateEditor を実行した結果があります。

[dennis]$ java -cp .:javassist.jar TranslateEditor Bean m_a BeanTest
TranslateEditor.reverse returning Alanigiro
Bean values are Alanigiro and originalB
TranslateEditor.reverse returning Awen
Bean values are Awen and newB

うまく、Bean.m_a フィールド (コンストラクタおよび set メソッドにあるもの)への各ストアにおいて追加コードへの呼び出しに移植することができました。フィールドからのロード上で同様の修正の実装により、この結果を打ち消すことができましたが、個人的には、最初に行ったことより、逆になった値にかなり興味がありますので、これらを続けていきます。

Javassist の要約

この記事では、Javassist を使用して、より簡単にシステマチックなバイトコード変換を行う方法を学習しました。これに最近の 2 つの記事を組み合わせると、別々の組み込みステップあるいは実行時に、Java アプリケーションのアスペクト指向な変換を実装する堅実な基礎を持つでしょう。

この方法のパワーのより良いアイデアを得るために、Javassist を中心として構築された JBoss アスペクト指向プログラミングプロジェクト (JBossAOP) を見てみたいと思うかもしれません。JBossAOP は、アプリケーション・クラスが行う様々な異なる操作を定義するために XML 構成ファイルを使用します。これらには、フィールド・アクセスあるいはメソッド呼び出しでのインターセプターの使用や既存クラスへのミックスインのインタフェース実装の追加を含んでいます。JBossAOP は、現在開発中の JBoss アプリケーションサーバのバージョンに組み込まれていますが、JBoss 以外のアプリケーションでもスタンドアローン・ツールとして利用可能です。

このシリーズの次の題材は、Apache Software Foundation の Jakarta Project のである Byte Code Engineering Library (BCEL) です。BCEL は Java の classworking を目的として、最も広く使用されているフレームワークのうちの 1 つです。BCFL は、Javassist の強さであるソースレベルの処理ではなく、個々のバイトコード命令に焦点を置きながら、最近の 3 つの記事で見てきた Javassist の方法とはかなり異なるバイトコードの使用方法を提供してくれます。次回、バイトコードのアセンブラレベルでの動作について十分な詳細をチェックしてください。


ダウンロード可能なリソース


関連トピック

  • この記事のサンプルコードをダウンロードして下さい。
  • Javassist は、東京工業大学 情報理工学研究科の千葉 滋によって発案され、近年、新しいアスペクト指向プログラミング機能の追加基盤となるオープンソースの JBoss アプリケーションサーバー・プロジェクトに加わりました。Sourceforge の JBoss project Files のページから Javassist の現在のリリースをダウンロード可能です。
  • Peter Haggar の「Java bytecode: Understanding bytecode makes you a better programmer」 (developerWorks 、2001 年 7 月) で、Java のバイトコード設計についてさらに深い知識を得ることができます。
  • アスペクト指向プログラミングをもっと知りたいですか?AspectJ 言語を使用する概要のために、Nicholas Lesiecki の「アスペクト指向プログラミングで、モジュール性を改善する」 (developerWorks、2002 年 1 月)をご覧下さい。さらに、Andrew Glover による最近の記事の「AOP が密結合の憂うつさを取り除く」 (developerWorks 、2004 年 2 月) では、AOP の機能設計概念である static crosscutting (静的な crosscutting) がどのようにしてからみ合う密結合コードを強力で拡張可能なエンタープライズアプリケーションに変えるのかが分かるでしょう。
  • オープンソースの Jikes Project は、Java プログラミング言語のための非常に高速で極めて従順なコンパイラを提供します。旧式の方法で Java ソースコードからバイトコードを生成するために使用してください。
  • Java に関連する何百ものタイトルを含む技術本の包括的なリストのある Developer Bookstore を訪れてみて下さい。
  • developerWorks Java technology ゾーンには、多数の Java テクノロジーに関する参考文献があります。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=219322
ArticleTitle=Java プログラミングのダイナミックス: 第 6 回 Javassist を使用したアスペクト指向の変更
publish-date=03022004