Javassistの紹介
1. 概要
この記事では、 Javasisst(Javaプログラミングアシスタント)ライブラリについて説明します。
簡単に言うと、このライブラリを使用すると、JDKのAPIよりも高レベルのAPIを使用して、Javaバイトコードを操作するプロセスが簡単になります。
2. Mavenの依存関係
Javassistライブラリをプロジェクトに追加するには、javassistをpomに追加する必要があります。
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javaassist.version}</version>
</dependency>
<properties>
<javaassist.version>3.21.0-GA</javaassist.version>
</properties>
3. バイトコードとは何ですか?
非常に高いレベルでは、プレーンテキスト形式で記述されてバイトコードにコンパイルされるすべてのJavaクラス(Java仮想マシンで処理できる命令セット)。 JVMは、バイトコード命令をマシンレベルのアセンブリ命令に変換します。
ポイントクラスがあるとしましょう。
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.baeldung.javasisst.Point {
public com.baeldung.javasisst.Point(int, int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()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言語で指定されています。 それらの多くが利用可能です。
move()メソッドのバイトコード命令を分析してみましょう。
- aload_0 命令は、ローカル変数0からスタックに参照をロードしています
- iload_1はローカル変数1からint値をロードしています
- putfield は、オブジェクトのフィールドxを設定しています。 すべての操作は、フィールドyに類似しています。
- 最後の命令はreturnです
Javaコードのすべての行は、適切な命令でバイトコードにコンパイルされます。 Javassistライブラリを使用すると、そのバイトコードを比較的簡単に操作できます。
4. Javaクラスの生成
Javassistライブラリは、新しいJavaクラスファイルを生成するために使用できます。
java.lang.Cloneableインターフェイスを実装するJavassistGeneratedClassクラスを生成するとします。 そのクラスに id の分野 int タイプ
ClassFile cf = new ClassFile(
false, "com.baeldung.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.baeldung.javasisst.Point")
.getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();
List<String> 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.baeldung.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<String> fieldsList = Stream.of(fields)
.map(Field::getName)
.collect(Collectors.toList());
assertTrue(fieldsList.contains("id"));
7. クラスバイトコードへのコンストラクターの追加
addInvokespecial()メソッドを使用して、前の例の1つで説明した既存のクラスにコンストラクターを追加できます。
そして、を呼び出すことでパラメーターなしのコンストラクターを追加できます
ClassFile cf = ClassPool.getDefault()
.get("com.baeldung.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<String> 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プロジェクトにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。