Javassistの概要
1. 概要
この記事では、Javasisst (Java Programming Assistant)ライブラリについて説明します。
簡単に言うと、このライブラリは、JDKのAPIよりも高レベルのAPIを使用することで、Javaバイトコードを操作するプロセスを簡単にします。
2. メーベン依存
Javassistライブラリをプロジェクトに追加するには、javassistをpomに追加する必要があります。
org.javassist
javassist
${javaassist.version}
3.21.0-GA
3. バイトコードとは何ですか?
非常に高いレベルでは、プレーンテキスト形式で記述され、バイトコードにコンパイルされるすべてのJavaクラス(Java仮想マシンで処理できる命令セット)。 JVMは、バイトコード命令をマシンレベルのアセンブリ命令に変換します。
Pointクラスがあるとしましょう:
public class Point {
private int x;
private int y;
public void move(int x, int y) {
this.x = x;
this.y = y;
}
// standard constructors/getters/setters
}
コンパイル後、バイトコードを含むPoint.classファイルが作成されます。 javapコマンドを実行すると、そのクラスのバイトコードを確認できます。
javap -c Point.class
これにより、次の出力が印刷されます。
public class com.example.javasisst.Point {
public com.example.javasisst.Point(int, int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iload_1
6: putfield #2 // Field x:I
9: aload_0
10: iload_2
11: putfield #3 // Field y:I
14: return
public void move(int, int);
Code:
0: aload_0
1: iload_1
2: putfield #2 // Field x:I
5: aload_0
6: iload_2
7: putfield #3 // Field y:I
10: return
}
これらの命令はすべてJava言語で指定されています。 a large number of them are available。
move()メソッドのバイトコード命令を分析してみましょう。
-
aload_0命令は、ローカル変数0からスタックに参照をロードしています
-
iload_1はローカル変数1からint値をロードしています
-
putfieldは、オブジェクトのフィールドxを設定しています。 すべての操作は、フィールドyに類似しています。
-
最後の命令はreturnです
Javaコードのすべての行は、適切な命令でバイトコードにコンパイルされます。 Javassistライブラリーは、そのバイトコードの操作を比較的簡単にします。
4. Javaクラスの生成
Javassistライブラリを使用して、新しいJavaクラスファイルを生成できます。
java.lang.Cloneableインターフェースを実装するJavassistGeneratedClassクラスを生成するとします。 そのクラスにintタイプ.のidフィールドを持たせたいClassFileは新しいクラスファイルを作成するために使用され、FieldInfoはクラスへの新しいフィールド:
ClassFile cf = new ClassFile(
false, "com.example.JavassistGeneratedClass", null);
cf.setInterfaces(new String[] {"java.lang.Cloneable"});
FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);
JavassistGeneratedClass.classを作成した後、実際にはidフィールドがあると断言できます。
ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
assertEquals(fields[0].getName(), "id");
5. クラスのバイトコード命令のロード
既存のクラスメソッドのバイトコード命令をロードする場合は、クラスの特定のメソッドのCodeAttributeを取得できます。 次に、CodeIteratorを取得して、そのメソッドのすべてのバイトコード命令を反復処理できます。
Pointクラスのmove()メソッドのすべてのバイトコード命令をロードしましょう。
ClassPool cp = ClassPool.getDefault();
ClassFile cf = cp.get("com.example.javasisst.Point")
.getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();
List operations = new LinkedList<>();
while (ci.hasNext()) {
int index = ci.next();
int op = ci.byteAt(index);
operations.add(Mnemonic.OPCODE[op]);
}
assertEquals(operations,
Arrays.asList(
"aload_0",
"iload_1",
"putfield",
"aload_0",
"iload_2",
"putfield",
"return"));
上記のアサーションに示されているように、バイトコードを操作のリストに集約することにより、move()メソッドのすべてのバイトコード命令を確認できます。
6. 既存のクラスバイトコードへのフィールドの追加
intタイプのフィールドを既存のクラスのバイトコードに追加するとします。 ClassPollを使用してそのクラスをロードし、それにフィールドを追加できます。
ClassFile cf = ClassPool.getDefault()
.get("com.example.javasisst.Point").getClassFile();
FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);
リフレクションを使用して、idフィールドがPointクラスに存在することを確認できます。
ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
List fieldsList = Stream.of(fields)
.map(Field::getName)
.collect(Collectors.toList());
assertTrue(fieldsList.contains("id"));
7. クラスバイトコードへのコンストラクターの追加
addInvokespecial()メソッドを使用して、前の例の1つで説明した既存のクラスにコンストラクターを追加できます。
そして、java.lang.Objectクラスから<init>メソッドを呼び出すことにより、パラメーターなしのコンストラクターを追加できます。
ClassFile cf = ClassPool.getDefault()
.get("com.example.javasisst.Point").getClassFile();
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);
MethodInfo minfo = new MethodInfo(
cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);
バイトコードを反復処理することで、新しく作成されたコンストラクターの存在を確認できます。
CodeIterator ci = code.toCodeAttribute().iterator();
List operations = new LinkedList<>();
while (ci.hasNext()) {
int index = ci.next();
int op = ci.byteAt(index);
operations.add(Mnemonic.OPCODE[op]);
}
assertEquals(operations,
Arrays.asList("aload_0", "invokespecial", "return"));
8. 結論
この記事では、バイトコード操作を簡単にすることを目的に、Javassistライブラリーを紹介しました。
コア機能に焦点を当て、Javaコードからクラスファイルを生成しました。また、すでに作成されたJavaクラスのバイトコード操作も行いました。
これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。