1. 序章

この記事では、 ASM ライブラリを使用して、フィールドを追加したり、メソッドを追加したり、既存のメソッドの動作を変更したりして、既存のJavaクラスを操作する方法を説明します。

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およびasm-utilはMavenCentralから入手できます。

3. ASMAPIの基本

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. イベントベースのASMAPIの操作

java.lang.IntegerクラスをASMで変更します。 そして、この時点で基本的な概念を理解する必要があります。 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);
    }
}

これをベースとして、 Cloneable インターフェイスをストック整数クラスに追加し、フィールドとメソッドも追加します。

4.1. フィールドでの作業

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メソッドは、visitorメソッドの順にと呼ばれる最後のメソッドです。 これは、フィールド挿入ロジックを実行するための推奨位置です。

次に、このオブジェクトの 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 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);
   }
}

フィールド変更の場合と同様に、単に訪問メソッドをインターセプトし、必要なパラメーターを変更します

この場合、 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);
    }
}

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

5. 変更されたクラスの使用

そこで、Integerクラスを変更しました。 次に、変更されたバージョンのクラスをロードして使用できるようにする必要があります。

writer.toByteArray の出力をクラスファイルとしてディスクに書き込むだけでなく、カスタマイズされたIntegerクラスを操作する方法がいくつかあります。

5.1. TraceClassVisitorを使用する

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

TraceClassVisitorClassVisitorであるため、標準の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());
}

ここで行ったことは、以前のPublicizeMethodAdapterに渡したClassVisitorTraceClassVisitorに適合させることです。

これで、すべての訪問がトレーサーで実行されます。トレーサーは、変換されたクラスのコンテンツを印刷して、それに加えた変更を表示できます。

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

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

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

これは、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プラグインを使用して、JARマニフェストファイルでpremain実装クラスを定義します。

<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が生成されます。 カスタマイズしたIntegerクラスを架空の「YourClass.class」で使用するには:

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

6. 結論

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

ここで検討した基本的な変換に加えて、ASMはアノテーション、ジェネリック、および内部クラスとの相互作用もサポートします。

ASMライブラリの機能の一部を確認しました。これにより、サードパーティのライブラリや標準のJDKクラスで発生する可能性のある多くの制限がなくなります。

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

この記事のソースコードは、GitHubプロジェクトにあります。