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 タイプ The ClassFile 新しいクラスファイルを作成するために使用され、 FieldInfo クラスに新しいフィールドを追加するために使用されます

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つで説明した既存のクラスにコンストラクターを追加できます。

そして、を呼び出すことでパラメーターなしのコンストラクターを追加できますからの方法 java.lang.Object クラス:

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プロジェクトであるため、そのままインポートして実行するのは簡単です。