|
バイトコードを動的に操作 Instrumentation |
||||
|
||||
最近、よく目にするのがアスペクト指向という言葉。 あるプログラムの実行中に、指定されたところで別のコードの断片 (アスペクト) を挿入して自動的に実行してまうというのが基本的な考え方だと思います。たとえば、JBoss は任意のオブジェクト (Plain Old Java Object 略して POJO なんて呼ばれています) を EJB に必要なコードを組み込んで、自動的に EJB に変換してしまうなんてことをやってくれるようです。 こういうことを Martin Fowler は Dependency Injection (依存性の注入) と呼んでいます。 コンテナに必要な処理を無理やり POJO に埋め込んでしまうわけです。 さて、こういったことをやるにはバイトコードを変更してしまうという方法もあります。今までバイトコード操作といえば Apache の BCEL や Javaasist などがありました。 ここまで書いてくると Tiger でもそういったバイトコードの操作ができるようになったのか、と思われるかもしれません。 ところが、残念ながらできません。 じゃ、何ができるようになったのでしょう。 Tiger では動的にバイトコードを操作するための枠組みが提供されるようになりました。枠組みだけなので、実際にバイトコード操作をするわけではありません。 いうなれば JAXP のようなものです。JAXP は XML のパーサを作るための枠組みで、パーサは含まれていません。実際にはそれだけだと使えないので、パーサが含まれていますが、他の JAXP に対応した XML パーサに変更することもできます。 じゃ、その枠組みの使い方を見ていきましょう。
|
|
||||||||||||||||
バイトコードを動的に変換するのですが、いつでもどこでも変換できるわけではありません。プログラムの実行中にいきなり動作が変更してしまったりしたら困りますからね。 バイトコードを変換できるのはクラスをロードする前に限られています。また、変換できるのは Tiger が標準で提供しているクラス以外の自作したクラスになります。J2SE で提供されているクラスを変更することはライセンス違反になるので、これは当然でしょう。 ロードする前に変換を行うのですが、そのためのプログラムを指定してあげなくてはいけません。これには java のオプションで指定します。 java -javaagent:JARファイル クラス のように -javaagent というオプションで指定します。ここで指定する JAR ファイルのマニュフェストファイルには次の 3 つの項目を書かなくてはなりません。
最初の Premain-Class だけは最低限書かなくてはいけない項目です。 そして、Premain-Class で指定されたクラスは premain というメソッドを持たなくてはいけません。普通のプログラムの main メソッドのようなものです。
Instrumentation は java.lang.instrument パッケージで定義されているインタフェースです。 よし分かったということでさっそく作ってみましょう。
Instrument インタフェースには getAllLoadedClasses メソッドが定義されているので、これを使ってみたいと思います。
getAllLoadedClasses メソッドはロードされたクラスの一覧が取れるので、それを表示するだけのプログラムです。 マニュフェストファイルは次のようになっています。
クラスの再定義をするわけではないので、Can-Redefine-Classes は記述していません。また、必要なライブラリもないので、Boot-Class-Path も省略しています。
これで JAR が作成されましたので実行してみたいのですが... 実際に実行するクラスが作っていなかったですね。 単に Hello, World! を出力するだけのクラスです。
こちらもコンパイルしておいてください。 さて、実行です。
ずらずらとクラス名が出力された後に Hello, World! が出力されました。こんな感じで使うということは感じていただけたでしょうか。
|
|
||||||
premain メソッドがコールされることは分かったのですが、実際にバイトコードの操作を行うにはどうしたらいいのでしょうか。 java.lang.instrument パッケージにはインタフェース 2 つにクラスが 1 つしか定義されていません。1 つきりのクラスである ClassDefinition クラスは final クラスで、単に Class クラスとそのバイトコードをバイト配列で保持するクラスのようです。 そうすると残ったインタフェースのうち、先ほど使った Instrumentation インタフェース以外のものしかありません。そのインタフェースは ClassFileTransformer インタフェースです。 確かにインタフェース名はバイトコード操作をするのにふさわしそうですが、実際にどういう風に使えばいいのかよく分かりません。いろいろと探してみたのですが、結局 ClassFileTransformer インタフェースを実装したクラスを探し出すことはできませんでした。 ならば調べるまでということで次のようなサンプルを作ってみました。
やってみたことは ClassFileTransform インタフェースで唯一定義されている transform メソッドの引数を出力しているだけです。
java.security.ProtectionDomain はセキュリティ関連の情報なので、とりあえず無視です。また、classfileBuffer はバイト配列なので、こちらも出力しないようにしました。また、バイトコードを操作しないのであれば戻り値は null にせよと Javadoc に書いてあったので、戻り値は null です。 これを実行すると...
RedefinedClass というのは再定義されたクラスである場合以外は null なのだそうです。ClassFileTransformer オブジェクトはいくつでも Instrumentation オブジェクトに登録できるので、ある ClassFileTransformer オブジェクトでクラスを再定義していればここに代入されるのでしょう。 なんとなく、使い方が分かってきたような気がします。
|
|
|||||||
バイトコードを操作しようとはいうものの、java.lang.instrument パッケージでは結局バイトコードを操作するための API は提供されていないことが分かりました。 でも、ここでバイトコードを操作するための API を自作しましょう... そんな簡単にできるわけないですね。 ということで、既存のバイトコード操作のためのライブラリを使用したいと思います。今回、使用したのは Javasiist です。Javassist は BCEL に比べると、バイトコードの知識もほとんどいらず、簡単にバイトコードを操作することができます。詳しくは参考文献を参考にしてください。 Javassist の一般的な使い方は ClassPool オブジェクトをファクトリメソッドで生成、ClassPool オブジェクトから CtClass オブジェクトを生成、CtClass オブジェクトに対して様々な処理を加える、という方法で行われます。 普通は CtClass オブジェクトを生成するには単にクラス名が分かっていればよく、バイト配列を指定する必要はありません。でも、ClassFileFormater#transfer メソッドではバイト配列が渡されるのでこれを使ってみました。
単純な例ということで、Hello, World! の前に、Hello, Tiger! という文字を出力させてみます。
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 だけで、ここではカレントディレクトリにおいておくことにします。
ここまでできたら実行です。
おぉ、いとも簡単にクラスの再定義をすることができました。なんかすごく簡単なのでひょうしぬけするぐらいです。
|
|
||||
今までバイトコードに触れるなんてことはほとんど考えたことがなかったと思うのですが、だんだん時代が変わってきたのでしょう。Spring や JBoss などのコンテナではバイトコードを操作することはごくごく普通に行われています。 今まではバイトコードを操作する仕組みに標準はなかったのですが、java.lang.intrument パッケージができたことで、バイトコード操作の呼び出しの部分は統一できるかもしれません。 これから目を離せない技術であることは確かですね。
今回使用したサンプルはここからダウンロードできます。
参考文献
(Oct. 2004) |
|