Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

バイトコードを動的に操作 Instrumentation

 
 
Tiger アスペクト指向には欠かせない
 
 

最近、よく目にするのがアスペクト指向という言葉。

あるプログラムの実行中に、指定されたところで別のコードの断片 (アスペクト) を挿入して自動的に実行してまうというのが基本的な考え方だと思います。たとえば、JBoss は任意のオブジェクト (Plain Old Java Object 略して POJO なんて呼ばれています) を EJB に必要なコードを組み込んで、自動的に EJB に変換してしまうなんてことをやってくれるようです。

こういうことを Martin Fowler は Dependency Injection (依存性の注入) と呼んでいます。 コンテナに必要な処理を無理やり POJO に埋め込んでしまうわけです。

さて、こういったことをやるにはバイトコードを変更してしまうという方法もあります。今までバイトコード操作といえば Apache の BCELJavaasist などがありました。

ここまで書いてくると Tiger でもそういったバイトコードの操作ができるようになったのか、と思われるかもしれません。

ところが、残念ながらできません。

じゃ、何ができるようになったのでしょう。

Tiger では動的にバイトコードを操作するための枠組みが提供されるようになりました。枠組みだけなので、実際にバイトコード操作をするわけではありません。

いうなれば JAXP のようなものです。JAXP は XML のパーサを作るための枠組みで、パーサは含まれていません。実際にはそれだけだと使えないので、パーサが含まれていますが、他の JAXP に対応した XML パーサに変更することもできます。

じゃ、その枠組みの使い方を見ていきましょう。

 

 
 
Tiger どうやって使うのだろう
 
 

バイトコードを動的に変換するのですが、いつでもどこでも変換できるわけではありません。プログラムの実行中にいきなり動作が変更してしまったりしたら困りますからね。

バイトコードを変換できるのはクラスをロードする前に限られています。また、変換できるのは Tiger が標準で提供しているクラス以外の自作したクラスになります。J2SE で提供されているクラスを変更することはライセンス違反になるので、これは当然でしょう。

ロードする前に変換を行うのですが、そのためのプログラムを指定してあげなくてはいけません。これには java のオプションで指定します。

    java -javaagent:JARファイル クラス

のように -javaagent というオプションで指定します。ここで指定する JAR ファイルのマニュフェストファイルには次の 3 つの項目を書かなくてはなりません。

Premain-Class バイトコード操作を行うクラスを指定する
Boot-Class-Path バイトコード操作に必要なライブラリやクラスなどがあれば、ここに記述する。
省略可能
Can-Redefine-Classes バイトコードを操作してクラスの再定義を許すかどうかを指定する (true or false)
省略可能

最初の Premain-Class だけは最低限書かなくてはいけない項目です。

そして、Premain-Class で指定されたクラスは premain というメソッドを持たなくてはいけません。普通のプログラムの main メソッドのようなものです。

    public static void premain(String agentArgs, Instrumentation inst);

Instrumentation は java.lang.instrument パッケージで定義されているインタフェースです。

よし分かったということでさっそく作ってみましょう。

サンプルのソース InstrumentationSample1.java
MANIFEST1.MF

Instrument インタフェースには getAllLoadedClasses メソッドが定義されているので、これを使ってみたいと思います。

package samples;
 
import java.lang.instrument.Instrumentation;
 
public class InstrumentationSample1 {
    public static void premain(String agentArgs, Instrumentation instrumentation) {
 
        Class[] classes = instrumentation.getAllLoadedClasses();
        for (Class cls : classes) {
            System.out.println(cls);
        }    
    }
}

getAllLoadedClasses メソッドはロードされたクラスの一覧が取れるので、それを表示するだけのプログラムです。

マニュフェストファイルは次のようになっています。

Premain-Class: samples.InstrumentationSample1

クラスの再定義をするわけではないので、Can-Redefine-Classes は記述していません。また、必要なライブラリもないので、Boot-Class-Path も省略しています。

C:\examples>javac samples\InstrumentationSample1
 
C:\examples>jar cvfm sample1.jar MANIFEST1.MF samples\*.class
マニフェストが追加されました。
samples/InstrumentationSample1.class を追加中です。(入 = 624) (出 = 394)(36% 収
縮されました)

C:\examples>

これで JAR が作成されましたので実行してみたいのですが... 実際に実行するクラスが作っていなかったですね。

単に Hello, World! を出力するだけのクラスです。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

こちらもコンパイルしておいてください。

さて、実行です。

C:\examples>java -javaagent:sample1.jar HelloWorld
class java.lang.SystemClassLoaderAction
class java.net.URLClassLoader$1
class java.net.URL
    <<途中省略>>
class [S
class [J
class [F
class [D
Hello, World!

C:\examples>

ずらずらとクラス名が出力された後に Hello, World! が出力されました。こんな感じで使うということは感じていただけたでしょうか。

 

 
 
Tiger バイトコードを操作する前に...
 
 

premain メソッドがコールされることは分かったのですが、実際にバイトコードの操作を行うにはどうしたらいいのでしょうか。

java.lang.instrument パッケージにはインタフェース 2 つにクラスが 1 つしか定義されていません。1 つきりのクラスである ClassDefinition クラスは final クラスで、単に Class クラスとそのバイトコードをバイト配列で保持するクラスのようです。

そうすると残ったインタフェースのうち、先ほど使った Instrumentation インタフェース以外のものしかありません。そのインタフェースは ClassFileTransformer インタフェースです。

確かにインタフェース名はバイトコード操作をするのにふさわしそうですが、実際にどういう風に使えばいいのかよく分かりません。いろいろと探してみたのですが、結局 ClassFileTransformer インタフェースを実装したクラスを探し出すことはできませんでした。

ならば調べるまでということで次のようなサンプルを作ってみました。

サンプルのソース InstrumentationSample2.java
MANIFEST2.MF

やってみたことは ClassFileTransform インタフェースで唯一定義されている transform メソッドの引数を出力しているだけです。

public class InstrumentationSample2 {
    public static void premain(String agentArgs, Instrumentation instrumentation) {
 
        instrumentation.addTransformer(new ClassFileTransformer() {
                public byte[] transform(ClassLoader loader,
                                        String className,
                                        Class<?> classBeingRedefined,
                                        ProtectionDomain protectionDomain,
                                        byte[] classfileBuffer) {
                    System.out.println("ClassLoader: " + loader);
                    System.out.println("ClassName: " + className);
                    System.out.println("RedefinedClass: " + classBeingRedefined);
                    System.out.println();

                    return null;
                }
            });
    }
}

java.security.ProtectionDomain はセキュリティ関連の情報なので、とりあえず無視です。また、classfileBuffer はバイト配列なので、こちらも出力しないようにしました。また、バイトコードを操作しないのであれば戻り値は null にせよと Javadoc に書いてあったので、戻り値は null です。

これを実行すると...

C:\examples>java -javaagent:sample2.jar HelloWorld
ClassLoader: sun.misc.Launcher$AppClassLoader@11b86e7
ClassName: HelloWorld
RedefinedClass: null
 
Hello, World!

C:\examples>

RedefinedClass というのは再定義されたクラスである場合以外は null なのだそうです。ClassFileTransformer オブジェクトはいくつでも Instrumentation オブジェクトに登録できるので、ある ClassFileTransformer オブジェクトでクラスを再定義していればここに代入されるのでしょう。

なんとなく、使い方が分かってきたような気がします。

 

 
 
Tiger バイトコードを操作しよう!
 
 

バイトコードを操作しようとはいうものの、java.lang.instrument パッケージでは結局バイトコードを操作するための API は提供されていないことが分かりました。

でも、ここでバイトコードを操作するための API を自作しましょう... そんな簡単にできるわけないですね。

ということで、既存のバイトコード操作のためのライブラリを使用したいと思います。今回、使用したのは Javasiist です。Javassist は BCEL に比べると、バイトコードの知識もほとんどいらず、簡単にバイトコードを操作することができます。詳しくは参考文献を参考にしてください。

Javassist の一般的な使い方は ClassPool オブジェクトをファクトリメソッドで生成、ClassPool オブジェクトから CtClass オブジェクトを生成、CtClass オブジェクトに対して様々な処理を加える、という方法で行われます。

普通は CtClass オブジェクトを生成するには単にクラス名が分かっていればよく、バイト配列を指定する必要はありません。でも、ClassFileFormater#transfer メソッドではバイト配列が渡されるのでこれを使ってみました。

サンプルのソース InstrumentationSample3.java
MANIFEST3.MF

単純な例ということで、Hello, World! の前に、Hello, Tiger! という文字を出力させてみます。

 1: public class InstrumentationSample3 {
 2:     private static ClassPool classPool;
 3:  
 4:     public static void premain(String agentArgs, Instrumentation instrumentation) {
 5:         classPool = ClassPool.getDefault();
 6:  
 7:         instrumentation.addTransformer(new ClassFileTransformer() {
 8:                 public byte[] transform(ClassLoader loader,
 9:                                  String className,
10:                                  Class<?> classBeingRedefined,
11:                                 ProtectionDomain protectionDomain,
12:                                 byte[] classfileBuffer) throws IllegalClassFormatException {
13: 
14:                    if (className.equals("HelloWorld")) {
15:                        try {
16:                            ByteArrayInputStream stream = new ByteArrayInputStream(classfileBuffer);
17:                            CtClass ctClass = classPool.makeClass(stream);
18:                            
19:                            CtMethod ctMethod = ctClass.getDeclaredMethod("main");
20:                            ctMethod.insertBefore("System.out.println(\"Hello, Tiger!\");");
21:                            
22:                            return ctClass.toBytecode();
23:                        } catch (Exception ex) {
24:                            IllegalClassFormatException e =  new IllegalClassFormatException();
25:                            e.initCause(ex);
26:                            throw e;
27:                        }
28:                    } else {
29:                        return null;
30:                    }
31:                }
32:            });
33:    }
34:}

ClassPool オブジェクトは 1 つあれば十分なので、static 変数にしています。それを 5 行目で生成しています。

transform メソッドの中を見ていきましょう。14 行目の if 文は変更するクラスかどうかを判定している部分です。HelloWorld クラスであれば、CtClass オブジェクトを生成するようにしています。

CtClass オブジェクトの生成は makeClass メソッドを使用するのですが、直接バイト配列を引数にとるものはありません。しかし、InputStream オブジェクトをとるものがあったので、バイトの配列を ByteArrayInputStream オブジェクトでくるんで使用しました (16, 17 行)。

18 行目は main メソッドを探している部分です。ちょうとリフレクションで Method オブジェクトを生成するときのようですね。

そして、19 行目で main メソッドを実行する前に insertBefore メソッドの引数で指定するコードを実行するように指定しています。

最後に CtClass から toBytecode メソッドを使用してバイト配列を取り出し、戻り値にしています。

クラスの再定義を行っているのでマニュフェストファイルの Can-Redefine-Classes は true にしておきます。また、Javassist の JAR ファイルもここで指定しておきましょう。Javassist の JAR ファイルは javassist.jar だけで、ここではカレントディレクトリにおいておくことにします。

Premain-Class: samples.InstrumentationSample3
Boot-Class-Path: javassist.jar
Can-Redefine-Classes: true

ここまでできたら実行です。

C:\examples>java -javaagent:sample3.jar HelloWorld
Hello, Tiger! 
Hello, World!

C:\examples>

おぉ、いとも簡単にクラスの再定義をすることができました。なんかすごく簡単なのでひょうしぬけするぐらいです。

 

 
 
Tiger おわりに
 
 

今までバイトコードに触れるなんてことはほとんど考えたことがなかったと思うのですが、だんだん時代が変わってきたのでしょう。Spring や JBoss などのコンテナではバイトコードを操作することはごくごく普通に行われています。

今まではバイトコードを操作する仕組みに標準はなかったのですが、java.lang.intrument パッケージができたことで、バイトコード操作の呼び出しの部分は統一できるかもしれません。

これから目を離せない技術であることは確かですね。

 

今回使用したサンプルはここからダウンロードできます。

 

参考文献

 

(Oct. 2004)

 
 
Go to Contents Go to Java Page