ASMによるJavaバイトコード操作の手引き

1前書き

この記事では、フィールドの追加、メソッドの追加、および既存のメソッドの動作の変更による、既存のJavaクラスの操作にhttp://asm.ow2.org/[ASM]ライブラリーを使用する方法について説明します。

2依存関係

ASMの依存関係を pom.xml に追加する必要があります。

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>6.0</version>
</dependency>
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm-util</artifactId>
    <version>6.0</version>
</dependency>

asm の最新バージョンを入手することができます。そしてMavenからのhttps://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.ow2.asm%22%20AND%20a%3A%22asm-util%22[asm-util]中央です。

3 ASM APIの基本

ASM APIには、変換と生成のためにJavaクラスと対話するための2つのスタイルがあります。イベントベースとツリーベースです。

3.1. イベントベースのAPI

このAPIは、 Visitor パターン に大きく基づいており、 XMLドキュメントを処理するSAX解析モデル 似ています。それは、その核となる以下のコンポーネントから構成されています。

  • ClassReader - クラスファイルを読みやすくするためのものです。

クラスを変換する ** ClassVisitor - クラスを変換するために使用されるメソッドを提供します。

生のクラスファイルを読んだ後 ** ClassWriter - クラスの最終製品を出力するために使用されます

変換

ClassVisitor には、特定のJavaクラスのさまざまなコンポーネント(フィールド、メソッドなど)にアクセスするために使用するすべての訪問者メソッドがあります。これを行うには、特定のクラスに変更を実装するために ClassVisitor ** のサブクラスを提供します。

Javaの規約と結果として得られるバイトコードに関して出力クラスの整合性を保つ必要があるため、このクラスは正しい出力を生成するために そのメソッドを呼び出す 厳密な順序を必要とします。

イベントベースのAPIの ClassVisitor メソッドは次の順序で呼び出されます。

visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute )**
( visitInnerClass | visitField | visitMethod )**
visitEnd

3.2. ツリーベースのAPI

このAPIは、よりオブジェクト指向のAPIであり、XML文書を処理するJAXBモデルに似ています。

まだイベントベースのAPIに基づいていますが、 ClassNode ルートクラスが導入されています。このクラスはクラス構造へのエントリポイントとして機能します。

4イベントベースのASM APIを使用した作業

ASMで java.lang.Integer クラスを変更します。そして、ここで基本的な概念を理解する必要があります。 ClassVisitor クラスは、クラスのすべての部分を作成または変更するために必要なすべての訪問者メソッドを含みます

変更を実装するために必要な訪問者メソッドをオーバーライドするだけです。必要条件コンポーネントを設定することから始めましょう。

public class CustomClassWriter {

    static String className = "java.lang.Integer";
    static String cloneableInterface = "java/lang/Cloneable";
    ClassReader reader;
    ClassWriter writer;

    public CustomClassWriter() {
        reader = new ClassReader(className);
        writer = new ClassWriter(reader, 0);
    }
}

これを基にして、ストックの Integer クラスに Cloneable インターフェースを追加し、さらにフィールドとメソッドを追加します。

4.1. フィールドを使った作業

Integer クラスにフィールドを追加するために使用する ClassVisitor を作成しましょう。

public class AddFieldAdapter extends ClassVisitor {
    private String fieldName;
    private String fieldDefault;
    private int access = org.objectweb.asm.Opcodes.ACC__PUBLIC;
    private boolean isFieldPresent;

    public AddFieldAdapter(
      String fieldName, int fieldAccess, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
        this.fieldName = fieldName;
        this.access = fieldAccess;
    }
}

次に、 visitField メソッドをオーバーライドしましょう。** 最初に、追加するフィールドがすでに存在するかどうかを確認し、ステータスを示すフラグを設定します。

  • メソッド呼び出しを親クラスに 転送する必要があります - これは visitField メソッドがクラス内のすべてのフィールドに対して呼び出されるときに発生する必要があります。 転送を失敗したということは、フィールドがクラスに書き込まれないことを意味します。

このメソッドを使うと、既存のフィールドの可視性や種類を変更することもできます。

@Override
public FieldVisitor visitField(
  int access, String name, String desc, String signature, Object value) {
    if (name.equals(fieldName)) {
        isFieldPresent = true;
    }
    return cv.visitField(access, name, desc, signature, value);
}

最初に、以前の visitField メソッドで設定されているフラグを確認し、再度 visitField メソッドを呼び出します。今回は、名前、アクセス修飾子、および説明を指定します。このメソッドは__FieldVisitorのインスタンスを返します。

  • visitEnd メソッドは、訪問者メソッドの順に** という最後のメソッドです。これはフィールド挿入ロジックを実行するための推奨位置です。

次に、このオブジェクトの visitEnd メソッドを呼び出して、このフィールドへのアクセスが終了したことを通知する必要があります。

@Override
public void visitEnd() {
    if (!isFieldPresent) {
        FieldVisitor fv = cv.visitField(
          access, fieldName, fieldType, null, null);
        if (fv != null) {
            fv.visitEnd();
        }
    }
    cv.visitEnd();
}
  • 使用されるすべてのASMコンポーネントが org.objectweb.asm パッケージに由来することを確認することが重要です。多くのライブラリは内部でASMライブラリを使用し、IDEはバンドルされたASMライブラリを自動挿入できます。

これで、 addField メソッドでアダプタを使用して、 追加されたフィールドを使用して java.lang.Integer の変換バージョンを取得しました。

public class CustomClassWriter {
    AddFieldAdapter addFieldAdapter;
   //...
    public byte[]addField() {
        addFieldAdapter = new AddFieldAdapter(
          "aNewBooleanField",
          org.objectweb.asm.Opcodes.ACC__PUBLIC,
          writer);
        reader.accept(addFieldAdapter, 0);
        return writer.toByteArray();
    }
}

visitField メソッドと visitEnd メソッドをオーバーライドしました。

フィールドに関して行わなければならないことはすべて、 visitField メソッドで起こります。これは、 visitField メソッドに渡された希望の値を変更することによって、既存のフィールドを変更することもできることを意味します(例えば、プライベートフィールドをパブリックに変換する)。

4.2. メソッドを使った作業

ASM APIでメソッド全体を生成することは、クラス内の他の操作よりも複雑です。これにはかなりの量の低レベルのバイトコード操作が含まれるため、この記事の範囲を超えています。

ただし、ほとんどの実用的な用途では、 既存のメソッドをよりアクセスしやすくするために 変更するか(オーバーライドまたはオーバーロードできるようにパブリックにする)、または** 拡張可能にするためにクラスを変更することができます。

toUnsignedStringメソッドをpublicにしましょう。

public class PublicizeMethodAdapter extends ClassVisitor {
    public PublicizeMethodAdapter(int api, ClassVisitor cv) {
        super(ASM4, cv);
        this.cv = cv;
    }
    public MethodVisitor visitMethod(
      int access,
      String name,
      String desc,
      String signature,
      String[]exceptions) {
        if (name.equals("toUnsignedString0")) {
            return cv.visitMethod(
              ACC__PUBLIC + ACC__STATIC,
              name,
              desc,
              signature,
              exceptions);
        }
        return cv.visitMethod(
          access, name, desc, signature, exceptions);
   }
}

フィールドの変更のために行ったように、単にvisitメソッドをインターセプトし、欲しいパラメータを変更するだけです。

この場合、 org.objectweb.asm.Opcodes パッケージのアクセス修飾子を使用して、メソッドの可視性を変更します。次に ClassVisitor を接続します。

public byte[]publicizeMethod() {
    pubMethAdapter = new PublicizeMethodAdapter(writer);
    reader.accept(pubMethAdapter, 0);
    return writer.toByteArray();
}

4.3. クラスを使った作業

メソッドの変更と同じ方法で、適切なビジターメソッドをインターセプトすることによってクラスを変更します。この場合、訪問者階層の最初のメソッドである visit をインターセプトします。

public class AddInterfaceAdapter extends ClassVisitor {

    public AddInterfaceAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }

    @Override
    public void visit(
      int version,
      int access,
      String name,
      String signature,
      String superName, String[]interfaces) {
        String[]holding = new String[interfaces.length + 1];
        holding[holding.length - 1]= cloneableInterface;
        System.arraycopy(interfaces, 0, holding, 0, interfaces.length);
        cv.visit(V1__8, access, name, signature, superName, holding);
    }
}

Integer クラスでサポートされるように、 visit メソッドをオーバーライドして Cloneable インターフェイスをインターフェイスの配列に追加します。アダプタの他のすべての用途と同じようにこれを接続します。

5修正クラスの使用

そのため、 Integer クラスを変更しました。今度は修正したバージョンのクラスをロードして使用できるようにする必要があります。

writer.toByteArray の出力をクラスファイルとして単純にディスクに書き込むことに加えて、カスタマイズされた Integer クラスとやり取りする方法が他にもいくつかあります。

5.1. TraceClassVisitor を使用する

ASMライブラリは TraceClassVisitor ユーティリティクラスを提供します。これを使用して、変更されたクラスをイントロスペクトします** 。したがって、変更が行われたことを確認できます。

TraceClassVisitor ClassVisitor なので、標準の ClassVisitor の代わりにドロップインとして使用できます。

PrintWriter pw = new PrintWriter(System.out);

public PublicizeMethodAdapter(ClassVisitor cv) {
    super(ASM4, cv);
    this.cv = cv;
    tracer = new TraceClassVisitor(cv,pw);
}

public MethodVisitor visitMethod(
  int access,
  String name,
  String desc,
  String signature,
  String[]exceptions) {
    if (name.equals("toUnsignedString0")) {
        System.out.println("Visiting unsigned method");
        return tracer.visitMethod(
          ACC__PUBLIC + ACC__STATIC, name, desc, signature, exceptions);
    }
    return tracer.visitMethod(
      access, name, desc, signature, exceptions);
}

public void visitEnd(){
    tracer.visitEnd();
    System.out.println(tracer.p.getText());
}

ここで行ったことは、 TraceClassVisitor を使用して、以前の PublicizeMethodAdapter に渡した ClassVisitor を調整することです。

すべての訪問はトレーサーによって行われます。トレーサーは変換されたクラスの内容を印刷して、それに対する変更を表示します。

ASMのドキュメントには、 TraceClassVisitor がコンストラクタに提供された PrintWriter に出力できると記載されていますが、これは最新バージョンのASMでは正しく動作しないようです。

幸いなことに、このクラスの基になるプリンタにアクセスでき、オーバーライドされた visitEnd メソッドでトレーサのテキストコンテンツを手動で印刷することができました。

5.2. Javaインスツルメンテーションの使用

これは、https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/package-summary.html[Instrumentation]を使用して、より詳細なレベルでJVMを操作できるようにする、より洗練されたソリューションです。]。

java.lang.Integer クラスをインスツルメントするために、JVMを使用してコマンドラインパラメータとして設定されるエージェントを作成します。エージェントには2つのコンポーネントが必要です。

  • premain という名前のメソッドを実装するクラス

  • の実装

ClassFileTransformer ここで、クラスの修正バージョンを条件付きで提供します。

public class Premain {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[]transform(
              ClassLoader l,
              String name,
              Class c,
              ProtectionDomain d,
              byte[]b)
              throws IllegalClassFormatException {
                if(name.equals("java/lang/Integer")) {
                    CustomClassWriter cr = new CustomClassWriter(b);
                    return cr.addField();
                }
                return b;
            }
        });
    }
}

Maven jarプラグインを使って、 premain 実装クラスをJARマニフェストファイルに定義します。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.4</version>
    <configuration>
        <archive>
            <manifestEntries>
                <Premain-Class>
                    com.baeldung.examples.asm.instrumentation.Premain
                </Premain-Class>
                <Can-Retransform-Classes>
                    true
                </Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

これまでのところ、コードを構築してパッケージ化すると、エージェントとしてロードできるjarファイルが生成されます。仮想の YourClass.class でカスタマイズした Integer クラスを使用するには、次のようにします。

java YourClass -javaagent:"/path/to/theAgentJar.jar"

6. 結論

ここでは変換を個別に実装しましたが、ASMでは複数のアダプタをチェーン化してクラスの複雑な変換を実現できます。

ここで検討した基本的な変換に加えて、ASMは注釈、総称、および内部クラスとの相互作用もサポートします。

私たちはASMライブラリの力のいくらかを見ました - それは私たちがサードパーティのライブラリとさらに標準的なJDKクラスで遭遇するかもしれない多くの制限を取り除きます。

ASMは、最も人気のあるライブラリ(Spring、AspectJ、JDKなど)の一部のもとで、その場で多くの「魔法」を実行するために広く使用されています。

この記事のソースコードはhttps://github.com/eugenp/tutorials/tree/master/asm[GitHubプロジェクト]にあります。