Javassistを触る。

http://www-6.ibm.com/jp/developerworks/java/040206/j_j-dyn0916.html


最近、Dependensy Injectionや、アスペクト指向とかを実現するためにバイトコードをいじってどーにかする系のライブラリが増えている。
気になっていたのでDeveloper Worksで紹介されていたので触ってみることにする。

とりあえずは、
http://www.csg.is.titech.ac.jp/~chiba/javassist/
で 3.0 betaをダウンロード。

操作しているクラスを検索し制御するjavassist.ClassPool クラス
〜
クラス・プールはJavassist APIを通して、ロードしたクラスをデータとして使用可能にします。

つまり、Javassitようのクラスローダーってところか。
こいつにロードさればいろいろ制御させることが出来るようになる。と。


そんで、ClassPoolにロードされたクラスは、javassist.CtClassのインスタンスとして
取り扱えるようになる。
さらに、CtClassからは、CtMethod、CtField、CtConstructorがとれてそいつらを使っていろいろやる。と。

あとは、気になったことを。

挿入したコードの中で使用したいメソッドまたはコンストラクタのパラメーター、メソッドの返り値、
および他のアイテムを表わすために使用される、いくつかの特殊な識別名の追加くらいです。
これらの特殊な識別名はすべて$記号で始まります。

識別名?

追加されているステートメントまたはブロックの外部で宣言されたローカル変数を参照する方法がないということです。

ふむ。ってことは、追加されたステートメントまたはブロックの内部で宣言されたローカル変数は参照可能である。と。


とりあえず、記事内の、メソッドの時間計測のサンプルをもとに、実行対象のメソッドをインターセプトして、
メソッドの実行前後にログを出力するサンプルを作ってみる。

こいつがターゲットになるクラス。

package a.works.study.javassist;

public class Main {

	public static void main(String[] args) {
		Main m = new Main();
		String msg = m.hello();
		System.out.println("------------------------------------");
		System.out.println("Message is [" + msg + "]");
	}
	
	
	public String hello( ) {
		return "hello javassist";
	}	
}


これが、Javassistでhelloメソッドの実行内容を置き換えるクラス。
内容的には、

  • helloメソッドをhello$Implにリネーム
  • helloメソッドの内容を開始ログ出力→hello$Imple呼び出し→終了ログ出力を行いメソッドに変更

package a.works.study.javassist;

import java.io.IOException;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;

public class ModifyHelloMethod {
	public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
		ClassPool pool = ClassPool.getDefault();

		CtClass mainCtClass = pool.get("a.works.study.javassist.Main");
		
		// helloメソッドの取り出し
		CtMethod orgHelloMethod = mainCtClass.getDeclaredMethod("hello");
		
		// helloメソッドの名前変更
		orgHelloMethod.setName("hello$Impl");
		
		// インターセプト用のhelloメソッド作成
		CtMethod newHelloMethod = CtNewMethod.copy(orgHelloMethod, "hello", mainCtClass, null);
		
		// インターセプト用のメソッドの内容を置き換える。
		StringBuffer body = new StringBuffer();
		body.append("{\n");
		body.append("long startTime = System.currentTimeMillis();\n");
		body.append("System.out.println(\"LOG: Method Start.\");\n");
		body.append("String ret = hello$Impl();\n");
		body.append("long time = System.currentTimeMillis() - startTime;\n");
		body.append("System.out.println(\"LOG: Method End. ret:[\" + ret + \"] time:\" + time);\n");
		body.append("return ret;\n");
		body.append("}\n");

		newHelloMethod.setBody(body.toString());
		
		System.out.println("------------------------------------");
		System.out.println(body);
		System.out.println("------------------------------------");
		
		// インターセプト用のクラスを追加する。
		mainCtClass.addMethod(newHelloMethod);
		
		// 変更した内容をクラスファイルに書き込む
		// mainCtClass.writeFile(); //実行時のルートにクラスファイルを作成する。
		mainCtClass.writeFile("build/classes");
	}

}


まずは、素のままのMainクラスを実行。

c:\java\javassist>java a.works.study.javassist.Main
------------------------------------
Message is [hello javassist]


次に、ModifyHelloMethodクラスを実行する。

c:\java\javassist>java -classpath .;javassist.jar a.works.study.javassist.ModifyHelloMethod
------------------------------------
{
long startTime = System.currentTimeMillis();
System.out.println("LOG: Method Start.");
String ret = hello$Impl();
long time = System.currentTimeMillis() - startTime;
System.out.println("LOG: Method End. ret:[" + ret + "] time:" + time);
return ret;
}

------------------------------------

そして、再度Mainクラスを実行すると。

c:\java\javassist>java a.works.study.javassist.Main
LOG: Method Start.
LOG: Method End. ret:[hello javassist] time:0
------------------------------------
Message is [hello javassist]

"LOG:〜"の部分が、ModifyHelloMethodクラスによりバイトコードに追加された処理。

Javassistコンパイル時のコードのチェックを、Java言語仕様によって要求されているよりも、ルーズに実装します。

上記のModifyHelloMethodを2回連続で実行したあと、Mainクラスを実行するとこんな例外がでる。

java.lang.ClassFormatError: a/works/study/javassist/Main (Repetitive method name/signature)
	at java.lang.ClassLoader.defineClass0(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:537)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:123)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:251)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:55)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:194)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:187)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:289)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:274)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:235)
	at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:302)
Exception in thread "main" 

BCELバイトコードの中身を見てみたら、hello$Implメソッドが2つ作られていた。
なるほど、確かにルーズだ。


矛盾のないクラスを作るには自分が注意しないといけない。Javassistのチェックに頼りすぎてはいけないぞ。と。